Proposal-temporal: Difference between withCalendar() and with({calendar})

Created on 2 Jun 2020  ยท  7Comments  ยท  Source: tc39/proposal-temporal

It's clear what Temporal.Date{,Time}.prototype.withCalendar() does: create a new instance with the same ISO fields, and a different calendar, projecting the old instance's date into the new calendar. Note that YearMonth and MonthDay don't have withCalendar().

@sffc and I had a conversation some time ago about what date.with({...someFields, calendar}) should do. Should it throw? Should it discard the calendar property? Should it interpret the existing and passed-in fields as being in the old or the new calendar space? We did come to a conclusion but I'm having second thoughts about it because I think there are just too many possible outcomes, each of which is probably not what the programmer meant.

For some examples, let's take date as being today's date in the hebrew calendar.

  • [[ISOYear]]: 2020
  • [[ISOMonth]]: 6
  • [[ISODay]]: 1
  • [[Calendar]]: Temporal.Calendar.from('hebrew')
  • .year: 5780
  • .month: 9
  • .day: 9

Option 1, throw:

  • date.with({ year: 2021, calendar: 'iso8601' }) โ†’ throws
  • date.with({ calendar: 'iso8601' }) โ†’ throws
  • pro: least ambiguous
  • pro:
  • con: passing Temporal objects to with() no longer works, because they have calendar properties: date.with(new Temporal.YearMonth(2021, 6)) throws too

Option 2, discard:

  • date.with({ year: 2021, calendar: 'iso8601' }) โ†’ Hebrew date 2021-09-09 a.k.a. -001739-05-13[c=hebrew]
  • date.with({ calendar: 'iso8601' }) โ†’ same as date
  • pro: consistent with ignoring other unknown fields e.g. date.with({ year: 2021, age: 'aquarius' })
  • con: almost certainly not what you wanted, since you get something back that doesn't have the calendar you requested

Option 3, interpret existing and passed-in fields in the old calendar space:

  • date.with({ year: 2021, calendar: 'iso8601' }) โ†’ -001739-05-13 a.k.a. the Hebrew date 2021-09-09 but in the ISO calendar
  • date.with({ calendar: 'iso8601' }) โ†’ same as date.withCalendar('iso8601')
  • pro: date.with({calendar}) and date.withCalendar(calendar) behave the same, which is probably what you wanted
  • con: in cases where you pass in other fields along with calendar, almost certainly not what you wanted

Option 4, interpret existing and passed-in fields in the new calendar space:

  • date.with({ year: 2021, calendar: 'iso8601' }) โ†’ 2021-09-09
  • date.with({ calendar: 'iso8601' }) โ†’ 5780-09-09
  • pro: logical in a way to apply the fields, then interpret them as fields in the new calendar
  • con: in most cases probably not what you wanted

Option 5, interpret existing fields in the old calendar space, passed-in fields in the new calendar space:

  • date.with({ year: 2021, calendar: 'iso8601' }) โ†’ 2021-06-01
  • date.with({ calendar: 'iso8601' }) โ†’ 2020-06-01
  • pro: logical in a way to apply the calendar conversion, then apply the other fields
  • pro: in many cases probably what you wanted
  • con: in some cases certainly not what you wanted, e.g. Temporal.MonthDay.from({month: 1, day: 1, calendar: 'hebrew' }).with({calendar: 'iso8601'}) โ†’ 09-25

Option 6, interpret existing fields in the new calendar space, passed-in fields in the old calendar space:

  • con: makes no sense whatsoever

I believe what @sffc and I landed on last time was option 5, but I'm starting to lean away from that because I realized you either have to stick with option 1 (throwing) for YearMonth and MonthDay anyway and be inconsistent, or you have to accept that users can do a (faulty!) calendar conversion using with() where we didn't intend that to be possible.

For the initial version of the polyfill I intend to stick with option 5 and just disallow calendar properties for YearMonth and MonthDay for the time being. Let me know soon if you disagree with that as a provisional choice, otherwise we can revisit this with feedback.

calendar

Most helpful comment

Option 7: what you said at the bottom. Option 5 for Date and DateTime, Option 1 for MonthDay and YearMonth.

Option 7 is fine with me, and is consistent with not having a .withCalendar() method on those types. Option 1 is also fine with me.

All 7 comments

Option 7: what you said at the bottom. Option 5 for Date and DateTime, Option 1 for MonthDay and YearMonth.

Option 7 is fine with me, and is consistent with not having a .withCalendar() method on those types. Option 1 is also fine with me.

Option 7 sounds right.

Coincidentally, I think I chose the equivalent to Option 5 for the ZonedAbsolute proof-of-concept, because .with({timeZone}) has similar issues as with({calendar}). Here's the JSDoc for that behavior. Is this the same as Option 5?

* If the `timeZone` field is included, the result will convert all fields to
* the new time zone, except that fields included in the input will be set
* directly. Therefore, `.with({timeZone})` is an easy way to convert to a new
* time zone while updating the local time.
*
* To keep local time unchanged while changing only the time zone, call
* `getFields()`, revise the `timeZone`, remove the
* `timeZoneOffsetNanoseconds` field so it won't conflict with the new time
* zone, and then pass the resulting object to `with`. For example:
* ```
* const {timeZoneOffsetNanoseconds, ...fields} = ldt.getFields();
* const newTzSameLocalTime = ldt.with({...fields, timeZone: 'Europe/London'});
* ```

Is this the same as Option 5?

I believe so, yes.

My strong preference is for Option 7, because it is consistent with how with({timeZone}) is handled in LocalDateTime, and with({timeZone}) is actually required for a specific case: if you're changing the time zone and other LDT fields at the same time, what you want is to apply DST disambiguation at the end after you've played the new fields on top of the new time zone. If you split up into two method calls, then you can end up with disambiguation applied to the intermediate state which may make the final state one hour different than what you expected.

Also, with({timeZone, calendar}) is helpful and ergonomic. as opposed to requiring two method calls.

Conclusion from Sept. 11 meeting: we'll keep the existing behaviour, which is the status quo in the spec polyfill.

So, withCalendar can be removed from the polyfill?

My mistake, I meant "...status quo in the polyfill", so withCalendar should stay.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

justingrant picture justingrant  ยท  4Comments

thefliik picture thefliik  ยท  3Comments

felixfbecker picture felixfbecker  ยท  6Comments

justingrant picture justingrant  ยท  6Comments

pipobscure picture pipobscure  ยท  6Comments