Repository: moment/luxon (js)

Issue:
Inconsistent duration calculations around some dates
**Describe the bug**

Luxon seems to get wrong results for some date diff operations.

**To Reproduce**

Try the code below (you can find it at https://runkit.com/bogdanb/luxon-year-100):

```
const z = luxon.DateTime.fromISO('0099-12-31', {zone: "utc"})
const a = luxon.DateTime.fromISO('0100-03-01', {zone: "utc"})
const b = luxon.DateTime.fromISO('0100-03-02', {zone: "utc"})

console.log("diffs:",  a.diff(z), b.diff(z))
console.log("diffs as days:", a.diff(z).as('days'), b.diff(z).as('days'))
console.log("day diffs:", a.diff(z, 'days'), b.diff(z, 'days'))
console.log("day diffs as days:", a.diff(z, 'days').as('days'), b.diff(z, 'days').as('days'))
```

**Actual vs Expected behavior**

I would expect the two methods to get the same results, and, for each method, the two diffs to be separated by one day. In practice, the first calculation method returns 60 & 61 days, while the second returns 60 and 62 days. (Unless I’m missing something, the first one should be the correct one.)

Note that if you use `0100-01-01` (or a later date) for `z`, then the two calculation methods agree.

**Desktop (please complete the following information):**

- OS: Windows (but I doubt it matters).
- Browser: Chrome & Firefox, plus I think RunKit runs directly in node.
- Luxon version: 3.2.1
- Timezone: UTC

**Additional context**

Note that year 100 is not a leap year, because even if it’s divisible by 4, it’s divisible by 100 but not by 400. I’m not sure this is the problem, I tried other years like that and I could not reproduce the issue.


Pull request description:
Handle dates in year 99 rolling over into year 100 better
Fixes #1389

From [#1389](https://github.com/moment/luxon/issues/1389#issuecomment-1443392279):

> The problem here is not `diff`, but actually `plus`:
`DateTime.fromISO('0099-12-31', {zone: "utc"}).plus({days: 61}).toISODate()` produces `0100-03-01`, not `0100-03-02`. What is happening is that deep inside Luxon it calls `Date.UTC` with a computed date object (day, month, etc.). `Date.UTC` however treats years before 100 as 19XX. So year 99 turns into 1999. Luxon works around this by checking if the year is < 100 and if so, using `setUTCFullYear` afterwards. Here is the problem: year 2000 is a leap year, year 100 is not. So if you do a plus operation that goes from year 99 into year 100, the native JavaScript Date object thinks it is crossing from year 1999 into year 2000 - and year 2000 is a leap year. Luxon then goes in and fixes it back to year 100 - but the damage is done, the calculation was done with "this is a leap year" in mind.

Patch:
diff --git a/src/impl/util.js b/src/impl/util.js
index c07136f6..81bffc6e 100644
--- a/src/impl/util.js
+++ b/src/impl/util.js
@@ -166,7 +166,10 @@ export function objToLocalTS(obj) {
   // for legacy reasons, years between 0 and 99 are interpreted as 19XX; revert that
   if (obj.year < 100 && obj.year >= 0) {
     d = new Date(d);
-    d.setUTCFullYear(d.getUTCFullYear() - 1900);
+    // set the month and day again, this is necessary because year 2000 is a leap year, but year 100 is not
+    // so if obj.year is in 99, but obj.day makes it roll over into year 100,
+    // the calculations done by Date.UTC are using year 2000 - which is incorrect
+    d.setUTCFullYear(obj.year, obj.month - 1, obj.day);
   }
   return +d;
 }


Test patch:
diff --git a/test/datetime/math.test.js b/test/datetime/math.test.js
index 848d5ba6..aab17683 100644
--- a/test/datetime/math.test.js
+++ b/test/datetime/math.test.js
@@ -107,6 +107,13 @@ test("DateTime#plus works across the 100 barrier", () => {
   expect(d.day).toBe(2);
 });
 
+test("DateTime#plus works across the 100 barrier when passing through February", () => {
+  const d = DateTime.fromISO("0099-12-31").plus({ day: 61 });
+  expect(d.year).toBe(100);
+  expect(d.month).toBe(3);
+  expect(d.day).toBe(2);
+});
+
 test("DateTime#plus renders invalid when out of max. datetime range using days", () => {
   const d = DateTime.utc(1970, 1, 1, 0, 0, 0, 0).plus({ day: 1e8 + 1 });
   expect(d.isValid).toBe(false);