Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Z designator (designating UTC) results in errors #133

Closed
mathieuprog opened this issue Feb 6, 2022 · 8 comments
Closed

Z designator (designating UTC) results in errors #133

mathieuprog opened this issue Feb 6, 2022 · 8 comments

Comments

@mathieuprog
Copy link

mathieuprog commented Feb 6, 2022

When giving PlainDateTime a datetime string with time zone in brackets, the time zone is simply ignored:

Temporal.PlainDateTime.from('2022-02-07T10:30:00[Europe/Brussels]')

Temporal.PlainDateTime {repr: 'Temporal.PlainDateTime <2022-02-07T10:30:00>'}

When giving PlainDateTime a datetime string with Z appended (UTC), an error occurs:

Temporal.PlainDateTime.from('2022-02-07T10:30:00Z')

ecmascript.mjs:1120 Uncaught RangeError: Z designator not supported for PlainDateTime
at Object.ToTemporalDateTime (ecmascript.mjs:1120:20)

When giving ZonedDateTime a datetime string with time zone in brackets:

Temporal.ZonedDateTime.from('2022-02-07T10:30:00[Europe/Brussels]')

Temporal.ZonedDateTime {repr: 'Temporal.ZonedDateTime <2022-02-07T10:30:00+01:00[Europe/Brussels]>'}

When giving ZonedDateTime a datetime string with Z appended (UTC), another error occurs:

Temporal.ZonedDateTime.from('2022-02-07T10:30:00Z')

ecmascript.mjs:322 Uncaught RangeError: Temporal.ZonedDateTime requires a time zone ID in brackets
at Object.ParseTemporalZonedDateTimeString (ecmascript.mjs:322:33)

It is very common for programming languages to append Z on a datetime in UTC. My Elixir backend returns a string like '2022-02-07T10:30:00Z' and Temporal doesn't seem to give me an easy way to parse it.
I don't see why time zones in brackets are allowed but not the Z designator.

@justingrant
Copy link
Contributor

@mathieuprog - What is your use case? I ask because the errors you ran into are intentional. Those errors prevent developers from initializing a Temporal instance from a string that does not match the data model of that Temporal type.

If the data model of your input string is a UTC timestamp, then Temporal.Instant is probably the right type to parse it with.

Temporal.Instant.from('2022-02-07T10:30:00Z')
// => 2022-02-07T10:30:00Z

I've written up some additional info below to explain the reasoning behind the behavior you encountered. Feel free to follow up with more questions/concerns.

Here's a few Temporal types with the data model of each:

  • Temporal.Instant represents an exact time. Its data model is a single 64-bit number: the number of nanoseconds since January 1, 1970 UTC. This type is unaware of time zones and does not provide a way to access calendar/clock units like month or hour. It's just a timestamp.
  • Temporal.ZonedDateTime represents an exact time from the perspective of a specific time zone. Its data model is a 64-bit timestamp, a time zone (usually an IANA zone), and a calendar. The time zone is needed because this Temporal type (and only this type) allows DST-safe creation of derived values. When you add 1 day to a Temporal.ZonedDateTime instance, the exact time will usually be 24 hours later but it may be 23 or 25 if the addition crossed a DST transition.
  • Temporal.PlainDate represents a timezone-less date: a local date without a time and without any reference to the time zone. Its data model is a year/month/day and a calendar.
  • Temporal.PlainDateTime represents a local date and time without any reference to the time zone. Its data model is year/month/day/hour/minute/second and a calendar. This type is rarely used, because the three types above cover most use cases. If you only care about the exact timestamp and don't care about time zones, then use Temporal.Instant. If you only care about the local date, then use Temporal.PlainDate. If you care about the exact time and the time zone, then use Temporal.ZonedDateTime. There are a few cases (see the docs) where Temporal.PlainDateTime is appropriate, but most of the time it's better to use a different type.

With those data models in mind, here's why you're running into the errors above:

Temporal.ZonedDateTime.from('2022-02-07T10:30:00Z')

This is an error because the data model of Temporal.ZonedDate time is an exact time AND a time zone that can be used to derive other values via addition, subtraction, with(), etc. A UTC timestamp tells us the exact time, but it doesn't tell you how to add or subtract in a DST-safe way. So there's not enough information for Temporal.ZonedDateTime's data model, and the call throws.

If you want to use the ZonedDateTime type, you'll need to tell Temporal what the time zone is, e.g. Temporal.ZonedDateTime.from('2022-02-07T10:30:00Z[Europe/Paris]').

Temporal.PlainDateTime.from('2022-02-07T10:30:00Z')

To understand why this is an error, first let's look at similar code using the Temporal.PlainDate type: Temporal.PlainDate.from('2022-02-07T10:30:00Z'). This code, if allowed, would be the source of a very common bug where developers parse a UTC timestamp string as if it were a local date, resulting in off-by-one-day errors if that date is shown in a UI or otherwise assumed to be the user's local date. Instead, the right way to turn a UTC timestamp into a local date is to supply a time zone:

Temporal.Instant.from('2022-02-07T20:30:00Z').toZonedDateTimeISO('Asia/Tokyo').toPlainDate();
// => 2022-02-08 (note that this is different from the UTC "date" !

Now let's get back to your original question: why does Temporal.PlainDateTime return the same error? The reasoning is similar to Temporal.PlainDate above: it's too easy for developers to assume that the date/time in the string is a meaningful local-timezone data and time. Developers and users often mistakenly think of it as a local date and time, instead of what it really is: the date and time in Greenwich, England!

For this reason, the only way to get UTC ("Z") timestamp strings into Temporal is via Temporal.Instant. This ensures that developers are required to think about what time zone is being used before trying to performing operations like fetching the day or clock hour, adding or subtracting days, or other timezone-sensitive activities.

If you want to get the local date/time in a particular time zone, you'll need to supply the time zone:

Temporal.Instant.from('2022-02-07T20:30:00Z').toZonedDateTimeISO('Asia/Tokyo');
// => 2022-02-08T05:30:00+09:00[Asia/Tokyo]

If for some reason (I'd love to learn more about these reasons; please share if you have them) you really, really want to read the year/month/day/hour/minute/second fields from a Z string, you can always hack it using the UTC time zone:

const zdt = Temporal.Instant.from('2022-02-07T20:30:00Z').toZonedDateTimeISO('UTC');
const { day, hour, minute } = zdt 
// => { day: 7, hour: 20, minute: 30 }
Temporal.PlainDateTime.from('2022-02-08T05:30:00+09:00[Asia/Tokyo]')

The reason why this case was not an error is because the data model of this string (local date, local time, UTC offset, and time zone) is compatible with the (local date, local time) data model of Temporal.PlainDateTime. In more simple terms: if the developer only cares about the local date/time of the data (and doesn't care about its time zone) then it can safely parse this string because it represents a local time in the time zone where it was originally stored. It's not the local time in Greenwich, England where few users live!

@mathieuprog
Copy link
Author

First let me start my answer with some observation:

Temporal.ZonedDateTime.from('2000-02-29 23:00:07-04:00 AMT America/Manaus')

It says "invalid ISO 8601 string". The error makes it then clear that ZonedDateTime requires a string in the ISO 8601 format.

Now let's try to pass "2000-02-29 23:00:07-04:00[America/Manaus]":

Temporal.ZonedDateTime.from('2000-02-29 23:00:07-04:00[America/Manaus]')

This works, however "2000-02-29 23:00:07-04:00[America/Manaus]" is not an ISO 8601 string. The ISO 8601 doesn't allow to specify a time zone, as written here:
https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators

Time zones in ISO 8601 are represented as local time (with the location unspecified), as UTC, or as an offset from UTC.

So the error message for the first input seems incorrect. What do you think about that? Actually we've already mentioned that a format such as "2000-02-29 23:00:07-04:00[America/Manaus]" is not an ISO 8601 string (user thojanssens) at tc39/proposal-temporal#716 and tc39/proposal-temporal#741 and tc39/proposal-temporal#703

Either there is some extension to ISO 8601 that I'm not aware of, or we have to rewrite the error message to explain what format ZonedDateTime.fromreally accepts.

Now about the Z designator:

The problem I'm having with the Z designator is that there are some languages that seem to consider Z to be 0 offset in UTC zone (and ZonedDatetime needs just that, an offset and a time zone). However (correct me if I'm wrong), Temporal doesn't consider Z to be UTC time zone, it only interprets it as 0 offset, that's why Temporal doesn't accept only Z.

Example in Elixir:

DateTime.from_naive!(~N[2000-01-01 00:00:00], "Etc/UTC") |> DateTime.to_string()

"2000-01-01 00:00:00Z"

With other time zones than "Etc/UTC", to_string outputs strings like:

"2000-02-29 23:00:07+01:00 CET Europe/Warsaw"

which is not even the format chosen by Temporal, because again, there are no standard representation (which seems to me really a pity by the way).

In conclusion, it depends how we interpret the meaning of Z designator:

  • offset 0
  • offset 0 in UTC

Wikipedia https://en.wikipedia.org/wiki/ISO_8601#Time_zone_designators states:

If the time is in UTC, add a Z directly after the time without a space.

Do we then consider it as a UTC timestamp without zone information, or do we consider it is a datetime in the UTC time zone? Why not be pragmatic and let ZonedDateTime accept Z? There seems to be no reason to take a strong stance for one of those interpretations, as it is so subtle anyway. But you addressed some possible (common) bugs from developer, which I mention below, is hard to grasp for me.

I could use Instant for this case I guess, and as you've shown, if I need the ZonedDateTime API I can convert.

I've browsed through my code, I use these utc timestamps for comparing, and Instant provides a compare function.

The error for PlainDate, PlainDateTime, ... also depend on that interpretation of Z.

a very common bug where developers parse a UTC timestamp string as if it were a local date

if the developer only cares about the local date/time of the data (and doesn't care about its time zone) then it can safely parse this string because it represents a local time in the time zone where it was originally stored. It's not the local time in Greenwich, England where few users live!

Overall I agree with you that I don't see use cases for a UTC date to be used other than logging, comparing, sorting, ...

As a final note, I have to say I use PlainDateTime quite extensively, and that's because I work on a calendar app and appointments, and in general it makes more sense for me to think about "wall clock datetimes" when working with appointments. The other reason being, I store a plain datetime and time zone as two fields in the database.

@justingrant
Copy link
Contributor

justingrant commented Feb 6, 2022

Either there is some extension to ISO 8601 that I'm not aware of, or we have to rewrite the error message to explain what format ZonedDateTime.fromreally accepts.

We call them ISO 8601 strings out of habit, but the string format used in Temporal (which is also used by java.time, Joda/Noda time, etc.) will be likely be standardized by IETF, not ISO. We're working with IETF to standardize the time zone and calendar extensions to RFC 3339. Once the standard is approved, we'll update the docs and error messages of polyfills.

As a final note, I have to say I use PlainDateTime quite extensively, and that's because I work on a calendar app and appointments, and in general it makes more sense for me to think about "wall clock datetimes" when working with appointments. The other reason being, I store a plain datetime and time zone as two fields in the database.

Yep, "Representing timezone-specific events where the time zone is not stored together with the date/time data." is the first use case mentioned in the Temporal.PlainDateTme docs link provided above. Timezone-free date/times are also mentioned there.

Do we then consider it as a UTC timestamp without zone information, or do we consider it is a datetime in the UTC time zone?

The former. Temporal places a lot of importance on the difference between an "offset" and a "time zone". The former (like "Z" or "+01:00") tells you the exact time of that string but cannot make any claims about other times related to that string. However, a time zone like "America/Manaus" or "Europe/Brussels" is an opt-in assertion by the caller that you can use this string to get an exact time, but you can also derive new values in a DST-safe way, e.g. by subtracting one day.

For this reason, Temporal.ZonedDateTime.from requires a time zone annotation like "[Europe/London]" or "[UTC]". For Temporal.ZonedDateTime.from, "Z" is not a special case: "2020-01-01T00:00Z" and other offset-only strings like "2020-01-01T00:00+01:00" will also throw.

Why not be pragmatic and let ZonedDateTime accept Z?

The reason is because the data model of Temporal.ZonedDateTime requires a time zone, which you can think of as an assertion by the developer that:

  • The date/time in the string represent a calendar date and wall-clock time in a real place
  • The caller can safely use this string to derive new exact or wall-clock times in this place using addition, subtraction, or other manipulations

If the string doesn't clearly have this assertion (in the form of a bracketed time zone annotation) then it's rejected.

This rejection has the effect of forcing developers to provide the time zone in a different way, e.g. Instant.toZonedDateTimeISO. We see this as a good outcome because whether the time zone is part of the string or called separately, requiring it enables developers to know that any Temporal.ZonedDateTime APIs will always return time-zone-aware results. If you don't need timezone-aware results, then another Temporal type can be used.

I could use Instant for this case I guess, and as you've shown, if I need the ZonedDateTime API I can convert.

This seems like the right outcome. If you are only comparing timestamps, then Temporal.Instant is the right API to use because its API is limited to properties and methods that aren't sensitive to time zones and/or calendars.

However, when you want to do things that do require a time zone and/or calendar (e.g. hour, month, add, subtract, with) then you'll need to add a time zone to convert it to a Temporal.ZonedDateTime.

@mathieuprog
Copy link
Author

Thank you for the comprehensive replies:)
Key points are that Z designates an offset only, and UTC datetimes or timestamps are not used for date manipulations anyway (hence the smaller API for an Instant).

One more thing: you wrote the time zone UTC in brackets as [UTC], but the time zone should be [Etc/UTC]. If we plan to accept [UTC] (for the reason that it is more convenient/less verbose/...), we might not be in line with the future IETF standard if they won't allow this, and then we might continue to accept non-standard strings for compatibility reasons. We can't technically call the string input as IETF strings in that case. That would be a pity for such a thing.

But you might just have written a typo 😅

@ptomato
Copy link
Contributor

ptomato commented Feb 7, 2022

UTC is an alias for Etc/UTC in the TZDB, so it is valid as part of that string. Although the new information that I learned while looking this up, is that it is listed in the backward file, which I believe means it's not preferred, though I'm not 100% sure.

However, both Firefox and V8 canonicalize Etc/UTC to UTC:

> new Intl.DateTimeFormat('en', {timeZone: 'Etc/UTC'}).resolvedOptions()
{
  locale: 'en',
  calendar: 'gregory',
  numberingSystem: 'latn',
  timeZone: 'UTC',
  year: 'numeric',
  month: 'numeric',
  day: 'numeric'
}

This is the result of an explicit algorithm step in CanonicalizeTimeZoneName, which Temporal also uses. So even if UTC isn't the preferred alias in the TZDB, there's a strong browser precedent here.

@gilmoreorless
Copy link
Contributor

UTC is an alias for Etc/UTC in the TZDB, so it is valid as part of that string. Although the new information that I learned while looking this up, is that it is listed in the backward file, which I believe means it's not preferred, though I'm not 100% sure.

Fittingly there was a clarification update made to the documentation a couple of weeks ago. The description of backward now states:

The source file backward defines links for backward compatibility; it does not define zones.
Although backward was originally designed to be optional, nowadays distributions typically use it and no great weight should be attached to whether a link is defined in backward or in some other file.

@mathieuprog
Copy link
Author

UTC is an alias for Etc/UTC

Oh I see! I never noticed that or forgot about it! 👍

@mathieuprog
Copy link
Author

We're working with IETF to standardize the time zone and calendar extensions to RFC 3339. Once the standard is approved, we'll update the docs and error messages of polyfills.

That will take years right? The IETF mentions "JAVAZDT" to refer to the format Java Time (and Temporal) are using: https://www.ietf.org/archive/id/draft-ryzokuken-datetime-extended-02.html. I thought that maybe we can change ISO8601 to JAVAZDT in the error messages, and then replace it by the IETF standard.

Feel free to close this issue by the way, if it no longer requires attention. Thank you for all the clarifications!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants