moment / luxon

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

Parsing utc date in iso format returns local zone #306

Closed tzachshabtay closed 6 years ago

tzachshabtay commented 6 years ago
DateTime.fromISO("2018-07-25T00:00:00Z");

The result has my local timezone, whereas I expected the result to be in UTC timezone, as clearly depicted by the ending "Z" character.

icambron commented 6 years ago

Please see Strings that specify an offset in the docs and the Set Zone section just after it.

tzachshabtay commented 6 years ago

@icambron specifying an offset and using the "Z" suffix is not the same thing. As offsets don't have a one-to-one relationships to timezones, it's completely reasonable for luxon to use the system's timezone. But the "Z" suffix is a special case as it's meant to be interpreted as UTC, see here: https://en.wikipedia.org/wiki/ISO_8601#Coordinated_Universal_Time_(UTC)

So I expect luxon to respect the ISO spec in that regard and use utc as a timezone and not the local system's timezone.

icambron commented 6 years ago

Luxon parses the Z out and uses it to interpret the time encoded in the string; the only question is whether it also sets the zone on the resulting datetime to UTC or not.

The point isn't whether Z is an offset or zone; check out the second example involving Europe/Paris. (Perhaps the section title should be changed to reflect that.) Luxon considers both a zone or offset specified in the string independent of zone in the resulting DateTime, unless you specifically tell it otherwise. It would be silly to do otherwise, because it would create really inconsistent behavior for a caller who doesn't know whether the incoming string contains "Z" or not.

jayarjo commented 4 years ago

Is there a way to explicitly tell Luxon that the given string is date in UTC, and prevent it from doing anything to it?

icambron commented 4 years ago

@jayarjo

DateTime.fromISO(“2014-08-06T09:10:16”, { zone: “utc”})

That will both interpret the time as utc and set utc as the datetime’s zone. If you only want the first behavior, add .toLocal()

inorganik commented 3 years ago

In my opinion, this is a flaw with Luxon, and a "gotcha" for developers who expect Luxon to respect ISO spec. Javascript's own Date() constructor properly interprets the Z:

const d = new Date('2018-07-25T00:00:00Z');
const l = new Date('2018-07-25T00:00:00');
d.toString()
// "Tue Jul 24 2018 18:00:00 GMT-0600 (Mountain Daylight Time)"
l.toString()
// "Wed Jul 25 2018 00:00:00 GMT-0600 (Mountain Daylight Time)"

Luxon is providing the added benefit of parsing offsets, but the presence of the "Z" at the end should override any offset and respect the spec.

icambron commented 3 years ago

@inorganik you're misreading the issue; Luxon respects the spec and interprets the Z correctly. In fact, its behavior is just like the native date's:

const d = luxon.DateTime.fromISO('2018-07-25T00:00:00Z');
const l = luxon.DateTime.fromISO('2018-07-25T00:00:00');
d.toString(); //=> "2018-07-24T20:00:00.000-04:00"
l.toString() //=> "2018-07-25T00:00:00.000-04:00"

The ticket is asking for Luxon to not just interpret the Z correctly (which it does), but to also set the zone property to UTC (which it does not).

inorganik commented 3 years ago

@icambron I think we're talking around eachother. In your example, you can see the issue:

"2018-07-25T00:00:00Z" gets formatted in your local time zone: "2018-07-24T20:00:00.000-04:00"

You have to specify the zone with Luxon, instead of just providing the "Z". Here's an example:

const time = '2018-07-25T00:00:00Z';

const utcA = DateTime.fromISO(time);
const utcB = DateTime.fromISO(time, { zone: 'utc' });

outputs:

utc A: 2018-07-24T18:00:00.000-06:00 utc B: 2018-07-25T00:00:00.000Z

I'm saying we would expect the zone to be set as UTC since we are providing it as UTC. But instead we have to use helper methods like the following to process all our Luxon dates:

const isoToUTC = (iso: string): DateTime => DateTime.fromISO(iso, { zone: 'utc' });
inorganik commented 3 years ago

To add to my response - you are right, it does behave the same as native Date. But where I got confused and I think @tzachshabtay was confused (and my co-worker) was that Luxon doesn't display that date in the UTC time zone and instead converted it to our local time zone.

icambron commented 3 years ago

Yes, that's the issue and I explained above why it's not a good idea to change it. I'll try again below. But it was your additional claim that Luxon is violating the ISO spec that caught my attention here; that's simply a mistaken claim. Even the example you gave of the native Date shows it returning the date in your local zone.

My final word on the original issue: how Luxon parses and how it displays are simply different things. Using the string to choose the zone is the unusual case. By default, if a program using Luxon is passed an ISO string that happens to use an offset of, say, +03:30, we don't want to force that program to convert the times to the zone it has already chosen to work in. We parse the string (of course taking the offset into account) and convert the date the default zone. Then the caller can have consistent expectations for the properties of that datetime. Of course the need to "echo" the string is a real, if less common, use case and so it's supported with the additional setZone parameter.

vaclav-stummer commented 2 years ago

Please see Strings that specify an offset in the docs and the Set Zone section just after it.

The link is dead

GuilhermeRossato commented 2 years ago

Steps to reproduce (only if you're on a negative timezone offset locally):

const luxonDate1 = DateTime.fromISO("2021-09-11T00:00:00Z");
luxonDate1.toISODate() === '2021-09-10';
luxonDate1.toFormat('yyyy-MM-dd') === '2021-09-10';

const luxonDate2 = DateTime.fromISO("2021-09-11T00:00:00Z");
luxonDate2.toISODate() === '2021-09-10';
luxonDate2.toFormat('yyyy-MM-dd') === '2021-09-10';

This is odd especially because of this, just tested on 2.3.0:

const luxonDate3 = DateTime.fromJSDate(new Date("2021-09-11T00:00:00Z"));
luxonDate3.toISODate() === '2021-09-10';
luxonDate3.toFormat('yyyy-MM-dd') === '2021-09-10';

According to documentation from 2.3.0, fromISO method follows ISO 8601:

image

And according to wikipedia ISO 8601 finishing in Z is UTC:

image

Either the documentation has to change to address this or there must be an implementation to interpret strings ending with Z to be UTC zone as output too by default.

inorganik commented 2 years ago

@GuilhermeRossato you're re-hashing an issue that has already been discussed in this thread. The key "gotcha" here that tripped up me and others is understanding that how a date is parsed and how a date is displayed (or formatted) are 2 different things.

When you parse an ISO date that ends in 'Z', luxon parses it as UTC, but when you display it by calling toISODate() it behaves like the native Date class and displays it in your local timezone. You have to explicitly tell luxon to display it in the UTC timezone like so:

const luxonDate1 = DateTime.fromISO("2021-09-11T00:00:00Z", { zone: 'utc' });
// or
const luxonDate1 = DateTime.fromISO("2021-09-11T00:00:00Z").toUTC();
alenl commented 7 months ago

I'm amazed to see how many words were wasted here to try to defend (an arguably bad (*)) design decision to make Luxon behave similar to native Date by default.... and none to actually answer the original question.

So, for the posterity - here's the real solution for everyone who might be wondering why Luxon is losing the timezone offset (regardless of whether it's UTC, or any other):

DateTime.fromISO('2024-01-10T15:00Z', {setZone:true}).toISO()

'2024-01-10T15:00:00.000Z'

(*) The reason we want to use Luxon is because the native Date support is full of broken idiosyncrasies like this one. We WANT it to behave more logically than the native.

webtweakers commented 7 months ago

@alenl Thanks for clarifying this just now, right when I stumble across this very issue. The {setZone:true} part indeed makes Luxon respect the Z in the input string.

So for example:

d = luxon.DateTime.fromISO('2023-12-17T06:08:09.933Z', {setZone:true})
d.hour

Outputs: 6, as expected.

Whereas:

d = luxon.DateTime.fromISO('2023-12-17T06:08:09.933Z')
d.hour

Will output 7 (in my timezone).

jayarjo commented 7 months ago

@alenl is it an undocumented feature? I cannot find it in the docs. Could you link the source?

alenl commented 7 months ago

@alenl is it an undocumented feature? I cannot find it in the docs. Could you link the source?

It's documented, just a bit cryptic:

opts.setZone boolean (default false) override the zone with a fixed-offset zone specified in the string itself, if it specifies one