tc39 / proposal-temporal

Provides standard objects and functions for working with dates and times.
https://tc39.es/proposal-temporal/docs/
Other
3.3k stars 149 forks source link

Added and skipped days due to timezone changes crossing the International Date Line #2495

Closed maxnikulin closed 6 months ago

maxnikulin commented 1 year ago

It is quite close to the #1315 (daysInMonth) issue, so discussion may be continued there.

If I understand it correctly, Gregorian calendar (iso8601) is in use in Samoa. However there was no December, 30 2011 due to change of time zone involved crossing the International Date Line

zdump -v Pacific/Apia
...
Pacific/Apia  Fri Dec 30 09:59:59 2011 UT = Thu Dec 29 23:59:59 2011 -10 isdst=1 gmtoff=-36000
Pacific/Apia  Fri Dec 30 10:00:00 2011 UT = Sat Dec 31 00:00:00 2011 +14 isdst=1 gmtoff=50400
...

Does it mean that local calendar still should be provided to make "How many days until a future date" (cookbook) example working correctly?

Moreover the same timezone and Pacific/Samoa (not separated that time) had July,4 1892 twice due to crossing the International Date Line toward America:

zdump -v Pacific/Samoa
...
Pacific/Samoa  Mon Jul  4 11:22:47 1892 UT = Mon Jul  4 23:59:59 1892 LMT isdst=0 gmtoff=45432
Pacific/Samoa  Mon Jul  4 11:22:48 1892 UT = Mon Jul  4 00:00:00 1892 LMT isdst=0 gmtoff=-40968
...

To handle such case zonedDateTime.withPlainDate (and perhaps some other methods) may need disambiguation option similar to Temporal.ZonedDateTime.from.

I do not mind that local calendar should be defined to handle February, 30 in 1720 when they need an extra day to return to Julian calendar. From my point of view the case of Samoa is more subtle since formally standard calendar is used.

justingrant commented 1 year ago

Hi @maxnikulin - Thanks for the report, it's good to keep this issue separate. There are two related but separate concerns that you bring up:

I'll discuss both below. The discussion below relies heavily on the concept of "exact time", so if you haven't read https://tc39.es/proposal-temporal/docs/ambiguity.html, it might be helpful context.

Simplifying a bit, the concept of "calendar" in Temporal can be thought of as a bidirectional mapping function between a {year, month, day} in one calendar system and the same three fields in another calendar. Actual Temporal.Calendar instances do more than that (and also expose additional fields like monthCode, era, etc.), but the core is simply a way to map dates back and forth. Calendars in Temporal don't know about time zones, and in fact don't deal with times at all; they only know about dates.

Also simplifying, the concept of "time zone" in Temporal can be thought of as a bidirectional mapping function between an exact time (Temporal.Instant) and a clock time (Temporal.PlainDateTime) in the ISO 8601 calendar. Actual Temporal.TimeZone instances do more than that, but the core is simply to map exact times to clock times. Another way to look at it is that time zones in Temporal is a function that accepts either an exact time (Temporal.Instant) or a clock time (Temporal.PlainDateTime), and returns the UTC offset(s) corresponding to that input.

Because calendars and time zones in Temporal don't interact, there's not a different calendar used for the case of day-skipping/day-repeating time zones. What Temporal does do is ensure that skipped or repeated days (or hours) are disambiguated so that a Temporal.ZonedDateTime instance never returns a local date/time that doesn't exist in that time zone, and (in some cases) allows disambiguation between repeated local times.

Ironically, yesterday I was working on a docs PR to explain this exact Samoa case in the docs for ZonedDateTime.prototype.add and ZonedDateTime.prototype.subtract.

Here's an explanation of the issue.

Temporal.ZonedDateTime treats date units and time units somewhat differently, following RFC 5545:

Calendars and Temporal.PlainDate are ignorant of time zones. Therefore, Temporal.ZonedDateTime is also ignorant of the time zone when adding, subtracting, or otherwise transforming date units. So in the Samoa time zone, adding two days to December 29, 2011 returns December 31, 2011 (two days later in the ISO 8601 calendar), but adding 48 hours returns January 1, 2012: 48 hours later in real-world time.

start = Temporal.ZonedDateTime.from('2011-12-29[Pacific/Apia]'); // 2011-12-29T00:00:00-10:00[Pacific/Apia]

// Add two days (note that December 30, 2011 didn't exist in Samoa)
start.add({ days: 2 }); // => 2011-12-31T00:00:00+14:00[Pacific/Apia]

// Add 48 hours, which skips over the day that didn't exist
start.add({ hours: 48 }); // => 2012-01-01T00:00:00+14:00[Pacific/Apia]

The repeated day in 1899 is handled similarly. Date units move forward in the calendar, ignoring the time zone, while time units move forward in exact time.

start = Temporal.ZonedDateTime.from('1892-07-03[Pacific/Apia]'); // => 1892-07-03T00:00:00+12:33[Pacific/Apia]

// Add one day. The result is disambiguated to the first repeated July 4, 1892.
// We know it's the first repeated day because its offset is the same as `start`: +12:33.
start.add({ days: 1 }); // => 1892-07-04T00:00:00+12:33[Pacific/Apia]

// Add two days, skipping over a repeated July 4, 1892
start.add({ days: 2 }); // => 1892-07-05T00:00:00-11:27[Pacific/Apia]

// Add 24 hours. The result is midnight on the first of two repeated July 4 days.
// Note the same offset as `start`: +12:33.
start.add({ hours: 24 }); // => 1892-07-04T00:00:00+12:33[Pacific/Apia]

// Add 48 hours. The result is midnight on the second of two repeated July 4 days.
// Note the different offset on the second repeated day: -11:27.
start.add({ hours: 48 }); // => 1892-07-04T00:00:00-11:27[Pacific/Apia]

To handle such case zonedDateTime.withPlainDate (and perhaps some other methods) may need disambiguation option similar to Temporal.ZonedDateTime.from.

The current design of ZonedDateTime doesn't offer the disambiguation option anywhere except from and with.

In the case of add and subtract, we originally had a disambiguation option but opted to remove it for two reasons:

For withPlainDate and withPlainTime, if my memory is correct we didn't explicitly decide one way or the other whether to have a disambiguation option. So when those methods were added to the Temporal spec, no option was included. Had we recognized this inconsistency a few years ago, we might have added the option into these methods. But now that Temporal is so close to being finalized, the bar to make changes to the spec is very high. At this point, the only changes we're making are ones where the current state is actively broken. "Somewhat harder to use" isn't enough to justify a change at this point.

Note that there's an easy workaround, which is to use with instead of withPlain*. The latter methods are simply a more ergonomic way to avoid spelling out all the date or time fields, respectively. So anyone who really needs disambiguation behavior with those methods can just use with instead.

Anyway, sorry for such a long-winded explanation. Hopefully this clarifies why things work the way they do now.

Feel free to follow up with additional concerns or questions.

justingrant commented 1 year ago

To summarize the long comment above:

justingrant commented 1 year ago

Sorry, one more note: daysInMonth will ignore time zones too. It will return the number of days that are present in the calendar for that month, regardless of whether days are skipped or repeated in that time zone.

maxnikulin commented 1 year ago

Thank you for the detailed response, @justingrant. I am realizing that I am too late to the party, so treat this issue with low priority. Anyway I do not have a consistent concept, so I have to admit that a lot of arbitrary design decisions are unavoidable for a library dealing with date and time. Even if spec text can not be changed, comments and warnings in accompanying docs are important for developers and list of design flaws is invaluable for successors. For me it is enough to know that you are aware and decisions were conscious.

I'll discuss both below. The discussion below relies heavily on the concept of "exact time", so if you haven't read https://tc39.es/proposal-temporal/docs/ambiguity.html, it might be helpful context.

Actually I noticed this link a few days ago (I believed that Date in JS is broken forever) and I am comparing it with the approach chosen in Python: PEP 495 – Local Time Disambiguation. I like that datetime object has the fold attribute. In Temporal it is only conversion property unavailable in ZonedDateTime instance. What I missed is that RFC 5545 describes some disambiguation rules. Taking into account that errata exists, zonedDateTime.add() docs may benefit from more precise links to particular sections of the RFC.

Simplifying a bit, the concept of "calendar" in Temporal can be thought of as a bidirectional mapping function between a {year, month, day} in one calendar system and the same three fields in another calendar.

I have realized that time zone aware calendar is more singular stuff than I thought at first. Certainly identity mapping to iso8601 calendar is not possible. However I believe that looking at a date picker or list of days, user should be aware that some day is missed or duplicated. And even localized calendar should have missed day at least in some cases, e.g. to specify birth date of a later arrived person. In various ID documents it is namely date, not date+time of birth.

a = zdt.add({ hours: 1 });
b = zdt.toInstant().add({ hours: 1 }).toZonedDateTime(zdt.timeZone);
a === b; // always true

For whole hours there are still corner cases with fractional changes of time zone offset, e.g. 15min one with skipped midnight:

zdump -v Asia/Kathmandu | grep '198[56]'
Asia/Kathmandu  Tue Dec 31 18:29:59 1985 UT = Tue Dec 31 23:59:59 1985 +0530 isdst=0 gmtoff=19800
Asia/Kathmandu  Tue Dec 31 18:30:00 1985 UT = Wed Jan  1 00:15:00 1986 +0545 isdst=0 gmtoff=20700

I am having in mind an application that present says split into 12 (usually) 2 hours chunks, but keeping midnight and noon as spit points is more important than even 1 or 3 hours long chunk.

Calendars and Temporal.PlainDate are ignorant of time zones. Therefore, Temporal.ZonedDateTime is also ignorant of the time zone when adding, subtracting, or otherwise transforming date units. So in the Samoa time zone, adding two days to December 29, 2011 returns December 31, 2011 (two days later in the ISO 8601 calendar), but adding 48 hours returns January 1, 2012: 48 hours later in real-world time.

Is there a way to get 1 day as interval length between 2011-12-31 and 2011-12-29? Is there a way to iterate date by day December 28, 29, 31, January, 1? There are cases when days are usually enough precision, 1 hour DST does not really matter to insist on 24 hours. Skipped/added day is still important. Consider e.g. generating list of dates for medical prescription for every other day. For patient's convenience 12:30 PM may still remain 12:30 PM, but extra day must be taken into account. That is why I asked about "How many days until a future date" (cookbook) example.

Note that there's an easy workaround, which is to use with instead of withPlain*. The latter methods are simply a more ergonomic way to avoid spelling out all the date or time fields, respectively. So anyone who really needs disambiguation behavior with those methods can just use with instead.

I see, existence of with() may be a rescue. However I would not call withPlainDate() ergonomic way. Ergonomics assumes convenience to do something in the right way. It is rather scratch pad or hackathon methods. Feel free to use them to rapidly create a prototype, but be ready to completely rewrite code as soon as you need correct behavior.

I hope, proper docs may help to mitigate proliferation of applications that can not even handle a change of time zone offset. Skipped/added date is an extreme case and unlikely supported at all.

ptomato commented 1 year ago

Is there a way to iterate date by day December 28, 29, 31, January, 1? There are cases when days are usually enough precision, 1 hour DST does not really matter to insist on 24 hours. Skipped/added day is still important. Consider e.g. generating list of dates for medical prescription for every other day. For patient's convenience 12:30 PM may still remain 12:30 PM, but extra day must be taken into account. That is why I asked about "How many days until a future date" (cookbook) example.

Yes, ZonedDateTime is still an exact time, so if you iterate by 1 day starting on 12:30 PM December 28 in that time zone, you'll get

let z = Temporal.ZonedDateTime.from('2011-12-28T12:30[Pacific/Apia]');
while(z.year === 2011) { z = z.add({days: 1}); console.log(z); }
// Temporal.ZonedDateTime <2011-12-29T12:30:00-10:00[Pacific/Apia]>
// Temporal.ZonedDateTime <2011-12-31T12:30:00+14:00[Pacific/Apia]>
// Temporal.ZonedDateTime <2012-01-01T12:30:00+14:00[Pacific/Apia]>
justingrant commented 1 year ago

Yes, ZonedDateTime is still an exact time, so if you iterate by 1 day starting on 12:30 PM December 28 in that time zone, you'll get

let z = Temporal.ZonedDateTime.from('2011-12-28T12:30[Pacific/Apia]');
while(z.year === 2011) { z = z.add({days: 1}); console.log(z); }
// Temporal.ZonedDateTime <2011-12-29T12:30:00-10:00[Pacific/Apia]>
// Temporal.ZonedDateTime <2011-12-31T12:30:00+14:00[Pacific/Apia]>
// Temporal.ZonedDateTime <2012-01-01T12:30:00+14:00[Pacific/Apia]>

The code above has a subtle issue: it might change the local time if the time you started with happens to be in the middle of an hour skipped by a DST transition. This problem can be avoided like this:

let z = Temporal.ZonedDateTime.from('2011-12-28T12:30[Pacific/Apia]');
const time = z.toPlainTime();
while(z.year === 2011) { z = z.add({days: 1}).withPlainTime(time); console.log(z); }
// Temporal.ZonedDateTime <2011-12-29T12:30:00-10:00[Pacific/Apia]>
// Temporal.ZonedDateTime <2011-12-31T12:30:00+14:00[Pacific/Apia]>
// Temporal.ZonedDateTime <2012-01-01T12:30:00+14:00[Pacific/Apia]>

There's yet another subtle issue: if the day were repeated instead of skipped, then the code above would skip a real-world day.

This is an interesting problem. We'll give it more thought to see if there's a better pattern to recommend that handles these cases more smoothly.

maxnikulin commented 1 year ago

The code above has a subtle issue: it might change the local time if the time you started with happens to be in the middle of an hour skipped by a DST transition. This problem can be avoided like this:

while(z.year === 2011) { z = z.add({days: 1}).withPlainTime(time); console.log(z); }

There's yet another subtle issue: if the day were repeated instead of skipped, then the code above would skip a real-world day.

Just an explicit example of this issue. I expect to get in some way 1, 2, 3, 4 for "duration":

var z2 = Temporal.ZonedDateTime.from('1892-07-03T13:00:00[Pacific/Apia]');

for (var zdt = z2, i = 0, pt = z2.toPlainTime(); i < 4;
     ++i, zdt = zdt.add({days: 1}).withPlainTime(pt))
   console.log("%d %s duration %s", i, zdt, zdt.since(z2).round('day').total('day'));
/*
0 1892-07-03T13:00:00+12:33[Pacific/Apia] duration 0
1 1892-07-04T13:00:00+12:33[Pacific/Apia] duration 1
2 1892-07-05T13:00:00-11:27[Pacific/Apia] duration 3
3 1892-07-06T13:00:00-11:27[Pacific/Apia] duration 4
*/

for (var zdt = z2, i = 0, pt = z2.toPlainTime(); i < 4;
     ++i, zdt = zdt.add({hours: 24}).withPlainTime(pt))
  console.log("%d %s duration %s", i, zdt, zdt.since(z2).round('day').total('day'));
/*
0 1892-07-03T13:00:00+12:33[Pacific/Apia] duration 0
1 1892-07-04T13:00:00+12:33[Pacific/Apia] duration 1
2 1892-07-04T13:00:00+12:33[Pacific/Apia] duration 1
3 1892-07-04T13:00:00+12:33[Pacific/Apia] duration 1
*/

for (var zdt = z2, i = 0, pt = z2.toPlainTime(); i < 4;
     ++i, zdt = zdt.add({hours: 24}))
  console.log("%d %s duration %s", i, zdt, zdt.since(z2).round('day').total('day'));
/*
0 1892-07-03T13:00:00+12:33[Pacific/Apia] duration 0
1 1892-07-04T13:00:00+12:33[Pacific/Apia] duration 1
2 1892-07-04T13:00:00-11:27[Pacific/Apia] duration 2
3 1892-07-05T13:00:00-11:27[Pacific/Apia] duration 3
*/

So with() with some kind of disambiguation has to be used if local time should be preserved.

maxnikulin commented 1 year ago

An attempt to define a function that determines real number of days in month:

function trueDaysInMonth(zdt) {
  const hour = 13; // May it cause day switch due to disambiguation?
  const zdtFirst = zdt.with({day: 1, hour}, { disambiguation: "earlier"});
  console.assert(zdtFirst.day === 1, "First day changed from %s %s", 1, zdtFirst);
  const day = zdt.daysInMonth;
  const zdtLast = zdt.with({day, hour }, { disambiguation: "later"});
  console.assert(zdtLast.day === day, "Last day changed from %s %s", day, zdtLast);
  return 1 + zdtFirst.until(zdtLast).round('day').total('day');
}
trueDaysInMonth(Temporal.ZonedDateTime.from("2011-12-28T12:30:00[Pacific/Apia]"))
// 30
trueDaysInMonth(Temporal.ZonedDateTime.from("1892-07-03T12:30:00[Pacific/Apia]"))
// 32

Likely there are corner cases that may fool such function.

Consider an expedition to the North or to the South pole where usual notion of days, nights and time zones is not applicable. Participants prefer to keep 24 hours activity cycle in sync with main land, however if final point differs from departure location they may need to adjust some day by arbitrary number of hours. Such custom time zone may be rather tricky for function counting days or iterating over dates.

justingrant commented 1 year ago

Likely there are corner cases that may fool such function.

Here's a function that I believe may work with all custom and built-in calendars and all custom and built-in time zones. There's probably more than one way to solve this problem, but the solution I used below was to adjust the number of calendar-reported days in the month by the days where midnight-to-midnight was skipped or repeated by offset transitions during that month. An added complexity was defending against calendars that might have skipped the first of the month.

function zdtDaysInMonth(zdt) {
  const { timeZone } = zdt;
  let daysDelta = 0;
  const oneDayNanoseconds = 86_400_000_000_000;

  // Get first calendar day of month. If the calendar skips days at the start of
  // the month, it may have clamped the result back into the previous month. Add a
  // day to get to the first valid day of this month in this calendar.
  let start = zdt.toPlainYearMonth().toPlainDate({ day: 1 });
  if (start.year !== zdt.year && start.month !== zdt.month && start.calendar.id !== zdt.calendar.id) {
    start = start.add({ days: 1 });
  }
  let next = start.add({ months: 1 }).with({ day: 1 });
  if (next.year === zdt.year && next.month === zdt.month && next.calendar.id === zdt.calendar.id) {
    next = next.add({ days: 1 });
  }

  const startZDT = start.toZonedDateTime(timeZone);
  const nextMonthStartZDT = next.toZonedDateTime(timeZone);
  let nextTransition = startZDT;

  while (true) {
    nextTransition = nextTransition.timeZone
      .getNextTransition(nextTransition.toInstant())
      ?.toZonedDateTimeISO(timeZone);
    if (!nextTransition || nextTransition.epochNanoseconds >= nextMonthStartZDT.epochNanoseconds) {
      // No transitions remaining in the month
      return zdt.daysInMonth + daysDelta;
    }

    // There was at least one time zone transition. Transitions only affect the
    // count of days in the month if the transition repeated or skipped an entire
    // calendar day.
    let transitionNanoseconds = startZDT.offsetNanoseconds - nextTransition.offsetNanoseconds;
    const nanosecondsAfterMidnight = nextTransition
      .toPlainTime()
      .since('00:00', { largestUnit: 'nanoseconds' }).nanoseconds;

    // If the transition doesn't start at midnight, then a 24-hour change may
    // not add or remove an entire calendar day.
    if (Math.abs(transitionNanoseconds) >= oneDayNanoseconds && nanosecondsAfterMidnight !== 0) {
      if (transitionNanoseconds > 0) {
        // Forward (skipping) transitions: start counting skipped days from the next midnight
        // by subtracting the time between now and midnight.
        transitionNanoseconds -= oneDayNanoseconds - nanosecondsAfterMidnight;
      } else {
        // Backward (repeating) transitions: start counting at midnight of the current day.
        // Note that `transitionNanoseconds` is negative so adding will reduce its magnitude.
        transitionNanoseconds += nanosecondsAfterMidnight;
      }
    }

    daysDelta += Math.trunc(transitionNanoseconds / oneDayNanoseconds);
  }
}

zdt2011 = Temporal.ZonedDateTime.from('2011-12-01[Pacific/Apia]')
// => 2011-12-01T00:00:00-10:00[Pacific/Apia]
zdtDaysInMonth(zdt2011)
// => 30

zdt1892 = Temporal.ZonedDateTime.from('1892-07-01[Pacific/Apia]')
// => 1892-07-01T00:00:00+12:33[Pacific/Apia]
zdtDaysInMonth(zdt1892)
// => 32

_EDIT 2023-02-08T13:57[America/LosAngeles]: The original version of this function had a bug. Now fixed.

maxnikulin commented 1 year ago

Thanks. I have no experience with alternative calendars, so I need more time to study your variant of the function.

In the meanwhile I have noticed the following: https://tc39.es/proposal-temporal/docs/calendar.html

Days in a month are not always continuous. There can be gaps due to political changes in calendars and/or time zones. For this reason, instead of looping through a month from 1 to date.daysInMonth, it's better to start a loop with the first day of the month (.with({day: 1})) and add one day at a time until the month property returns a different value.

I am surprised to see mention of time zones here, since I believed that calendar is independent of time zone.

  let start = zdt.toPlainYearMonth().toPlainDate({ day: 1 });
  if (start.year !== zdt.year && start.month !== zdt.month && start.calendar.id !== zdt.calendar.id) {

Shouldn't || be used here instead of && here?

It seems startOfMonth function (similar to the startOfDay method) may be handy.

  let next = start.add({ months: 1 });
  if (next.year === zdt.year && next.month === zdt.month && next.calendar.id === zdt.calendar.id) {
    next = next.add({ days: 1 });

And maybe nextStartOfMonth as well. I suspect an issue if start.day != 1. Should start be converted to PlainYearMonth before adding a month? I have in mind a pathological case when next month has fewer days than start.day.

I have not decided if lack of explicit disambiguation options may have undesired effects.

I do not think, it is possible to define startOfMonth() that suits for all, but at least it should handle usual subtleties for TimeZone and Calendar combination. I can imagine cases when custom code is required. Consider a custom time zone that follows a nomad who decided to move from a Samoa island to another one crossing the International Date Line around December, 31 and January, 1. Start of year may be arbitrary chosen in some range. A pilot of a jet having several flights between these islands will have schedule with alternating dates. In such cases it is possible to provide help utilities that alleviate the issue, but not solve the problem completely.

I like that you code does not have "magic" local time like 13:00 in my variant. Working with dates representing as timestamps may be call to problems. I heard about some bug with ICQ (perhaps an alternative client) and incorrect notification dates about birthdays because they were stored as timestamps while people are spread around the whole globe. Unfortunately it is impossible to get rid of time completely when counting of days involves time zones.

ptomato commented 1 year ago

I am surprised to see mention of time zones here, since I believed that calendar is independent of time zone.

Well, that is still the case :smile: ZonedDateTime has both calendar and time zone, and either one may cause a day to be skipped, but they are still independent of each other.

maxnikulin commented 1 year ago

I am surprised to see mention of time zones here, since I believed that calendar is independent of time zone.

Well, that is still the case smile ZonedDateTime has both calendar and time zone, and either one may cause a day to be skipped, but they are still independent of each other.

@ptomato, I agree that days may be added or skipped in the case of ZonedDateTime (it is the topic of this issue). My point is that discussion of missed and repeated days due to time zone is confusing in the context of Temporal.Calendar, especially because it is not explicitly stated that namely ZonedDateTime should be used for looping. Perhaps "Writing Cross-Calendar Code" note deserves a separate documentation page similar to disambiguation.

Iteration over days is still tricky with ZonedDateTime. Adding a day may change time due to skipped interval or 2 days (48 hours) may be added despite .add({days: 1}) request.

maxnikulin commented 1 year ago

@justingrant, I can not say that I fully agree with your approach to take into account time transitions while counting days. I beg you pardon if I interpreted the code in a wrong way.

  1. I would consider 23 or 22 hours transition as an added or skipped day. I am unsure what limit should be set. On the other hand, 25 transitions by 1 hour should not change day count (consider a person traveling around the globe).
  2. I believe that ~24 hours transition should change day count even it it happens at e.g. 04:00 or 22:00 (December, 29 04:00 → December, 30 04:00 or July, 4 22:00 → July 3, 22:00).

Perhaps it is not possible to define day count suitable for all cases.

justingrant commented 1 year ago

Yep, the count of days reported will depend on the definition of "what is a day?"

The definition that we're planning to use in the Temporal spec is this:

The overall idea is that we're only considering it a "skipped day" or "repeated day" if an entire day (starting from midnight) is skipped. A 24-hour skipped or repeated period won't count if the transitions aren't at midnight, which we think is OK because when full-day transitions happen they seem to be aligned on midnight boundaries... and because (as you observe above) if you choose some smaller-than-full-day period then it's hard to know when to set the cut-off length.

Are you aware of any time zones in the IANA database that have 24-hour skipped or repeated periods that are not aligned at midnight?

BTW, we also changed guidance in the docs for calendar authors to make "earliest nanosecond that has that month and year and the latest nanosecond that has that month and year" more deterministic by requiring calendar authors to prefer their overflow: 'constrain' to keep dates inside the same month, if possible.

maxnikulin commented 1 year ago

Are you aware of any time zones in the IANA database that have 24-hour skipped or repeated periods that are not aligned at midnight?

No, I am not. I had in mind a custom timezone to represent schedule of a traveled person or passengers and crew of a ship crossing the International date line. Another consideration is that besides prescriptions to developers, some recommendations may be given to politics and authorities who decide to arrange such time transition. Follow some rules to minimize amount of bugs that would be faced.

The previous comment made it more apparent for me that there are at least 2 concepts of day depending on application:

If a 24 hours forward transition occurred at 06:00 such date should be available for selection in calendar dialogs (often even fully skipped date should be available), perhaps emphasized in some way. However it should not be counted for duration expressed in days to determine intervals e.g. related to medicine.

ptomato commented 6 months ago

I believe most of this was solved in https://github.com/tc39/proposal-temporal/pull/2503. There's still one confusing bit in the docs that says that skipped calendar days may be caused by time zone changes, which is false; I'll remove that soon.

maxnikulin commented 6 months ago

Are you aware of any time zones in the IANA database that have 24-hour skipped or repeated periods that are not aligned at midnight?

zdump -v -c 1867,1868 America/Sitka
...
America/Sitka  Sat Oct 19 00:31:12 1867 UT = Sat Oct 19 15:29:59 1867 LMT isdst=0 gmtoff=53927
America/Sitka  Sat Oct 19 00:31:13 1867 UT = Fri Oct 18 15:30:00 1867 LMT isdst=0 gmtoff=-32473

https://github.com/eggert/tz/blob/b1e07fb0779a8075c7f9d862c784dfbd31965c1a/northamerica#L569-L584