moment / luxon

⏱ A library for working with dates and times in JS
https://moment.github.io/luxon
MIT License
15.33k stars 730 forks source link

toLocaleParts offby 1 Error on dates prior to "1884-01-01" #1506

Open UpsidePotential opened 1 year ago

UpsidePotential commented 1 year ago

Describe the bug .toLocaleParts returns a date offby 1 if used on a date prior to 1884-01-01. formatToParts works correctly.

To Reproduce

const { DateTime } = require("luxon")
const timeString = "1800-01-01T00:00:00.0000000";

{
    let time = DateTime.fromISO(timeString)
    time.setLocale('en-US')
    const parts = time.toLocaleParts({ year: 'numeric', month: '2-digit', day: '2-digit' })

    const partValues = parts.map((p) => p.value);
    console.log(partValues);
}

{
    const date = new Date(timeString);
    const options = { year: 'numeric', month: '2-digit', day: '2-digit' };
    const dateTimeFormat = new Intl.DateTimeFormat('en-US', options);

    const parts = dateTimeFormat.formatToParts(date);
    const partValues = parts.map((p) => p.value);
    console.log(partValues);
}
["12", "/", "31", "/", "1799"]
["01", "/", "01", "/", "1800"]

Actual vs Expected behavior Expect toLocaleParts to return the same values as formatToParts.

Desktop (please complete the following information):

icambron commented 1 year ago

Thanks for the report. This issue's root isn't in formatting, it's converting to the native date. The problem with the date is that it has an offset with a seconds component. You can see that here:

d  = new Date("1800-01-01T00:00:00.0000000")   // => 1800-01-01T04:56:02.000Z 

Those 2 seconds are there because the offset is 296 minutes and 2 seconds. The problem is that JS lies about this:

d.getTimezoneOffset()   //=> 296

The 2 seconds are gone. That's because the JS date API simply doesn't expose seconds. Luxon needs to convert the date into a native date to pass it to the Intl API, so it adds the current offset (296) to UTC time and calls new Date() on it. Unfortunately, that offset is 2 seconds short and, from the native date's perspective, leaves the date as 23:59:58 the day before. Then we feed the this native date into Intl and it happily prints out the wrong date.

This has come up a few times with historical dates with funky offsets like this. Ideally the browser would just tell us the offset in seconds, but it doesn't. One option we have is to fix this to have toJSDate() feed the native date the ISO string instead (I guess if and only if the DateTime is in the system zone?) . But that needs thought through carefully as there may be consequences I'm not thinking of.