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

Should built-in calendars be subclasses? #847

Closed
Ms2ger opened this issue Aug 25, 2020 · 24 comments · Fixed by #1195
Closed

Should built-in calendars be subclasses? #847

Ms2ger opened this issue Aug 25, 2020 · 24 comments · Fixed by #1195
Assignees
Labels
documentation Additions to documentation non-prod-polyfill THIS POLYFILL IS NOT FOR PRODUCTION USE! question spec-text Specification text involved

Comments

@Ms2ger
Copy link
Collaborator

Ms2ger commented Aug 25, 2020

» Temporal.Calendar.from("iso8601").constructor
function ISO8601() { ... }

We should spec those or make them invisible to JS.

@ptomato
Copy link
Collaborator

ptomato commented Aug 25, 2020

What would be the way to hide the implementation from JS? I think that is better (though I don't really have any justification for that, and I would be fine to go the other way too.)

@ljharb
Copy link
Member

ljharb commented Aug 25, 2020

Absolutely they should be subclasses of a common base class - Temporal needs to establish a convention for userland calendar objects to follow.

@ptomato
Copy link
Collaborator

ptomato commented Aug 27, 2020

Thinking about this some more — there is an inconsistency in the way we do time zones and calendars which has bothered me for a while.

In pseudocode, Calendars:

class Calendar {
  constructor(id) {
    this.[[Identifier]] = id;
  }
  method() {
    throw new Error('not implemented');
  }
}
class ISO8601 extends Calendar {
  constructor() { super('iso8601'); }
  method() {
    /* ISO calendar implementation of method */
  }
}
class Japanese extends Calendar {
  constructor() { super('japanese'); }
  method() {
    /* Japanese calendar implementation of method */
  }
}

But TimeZones:

class TimeZone {
  constructor(id) {
    if (new.target === TimeZone)
      throwIfIdentifierIsNotBuiltin(id);
    this.[[Identifier]] = id;
  }
  method() {
    if (isOffsetTimeZoneIdentifier(this.[[Identifier]]))
      /* offset time zone implementation of method */;
    else
      /* named time zone implementation of method */;
  }
}

So, Calendar does expose subclasses to JS, and TimeZone does not. (I guess I answered my own question from #847 (comment) with that.)

It seems to me that we should go with either one or the other (either hide the Calendar subclasses from JS, or expose OffsetTimeZone and NamedTimeZone to JS.)

I think I prefer the latter, as Jordan pointed out it establishes the proper convention. I'm not too concerned about adding more types to Temporal in this way, as I otherwise would be, since these subclasses will mostly be invisible and normal user code doesn't have to care about them.

@ptomato ptomato added this to the Stable API milestone Aug 27, 2020
@ljharb
Copy link
Member

ljharb commented Aug 27, 2020

Adding them as, eg, TimeZone.Offset, may also keep them neatly tucked away.

@Ms2ger
Copy link
Collaborator Author

Ms2ger commented Aug 28, 2020

How many calendars are we specifying, in that case?

@ptomato
Copy link
Collaborator

ptomato commented Aug 28, 2020

I think we are only specifying ISO8601, at least in ECMA-262; see also #541

@littledan
Copy link
Member

We previously concluded that there would be just one built-in calendar class in #300 . Did anything change to motivate this additional complexity?

@ptomato
Copy link
Collaborator

ptomato commented Sep 17, 2020

That is right, we did, and I forgot about that.

One reason for the additional complexity is that some calendars will inherit from the ISO calendar (e.g. gregory, japanese) and some will not (e.g. hebrew, islamic). Since these are all built-in calendars, that doesn't matter (they could inherit internally but not have the classes be exposed) but given the split in built-in calendars I'd expect a similar split to occur in userland calendars.

This is probably not something that we have to resolve in order to call all design decisions complete, as it's more of an implementation detail, but it needs to be resolved before we can call the proposal stable.

@ptomato ptomato added documentation Additions to documentation spec-text Specification text involved non-prod-polyfill THIS POLYFILL IS NOT FOR PRODUCTION USE! labels Sep 18, 2020
@justingrant
Copy link
Collaborator

I have no opinion about the subclass-vs-not topic in this issue, but a very strong opinion about how built-in calendars should be exposed into the Temporal public API namespace.

If we're putting built-in calendars into the Temporal namespace, then seems much better to make them static properties on the Calendar class, e.g. Temporal.Calendar.ISO8601, Temporal.Calendar.Islamic vs. having all N built-in calendars exposed directly off Temporal, e.g. Temporal.IslamicCalendar. If we put calendars on the Calendar object, then we won't pollute IDE and browser-devtools auto-complete with a big list of calendars (most of which most developers will never use) which would make it harder for users to find the Temporal types that they use frequently.

I think this is similar feedback that @ljharb gave in #847 (comment). I disagree with Jordan about time zones because there's no Offset subclass of TimeZone and also there's no statically-exposed list of built-in time zones. It's all data-driven and will change at runtime. But I strongly agree re: calendars because they're subclasses and also because (unlike time zones) there's a static and small list of built-in calendars.

Regardless of how this is decided, remember to update the TS types so the new API will be discoverable!

@littledan
Copy link
Member

I don't see why we'd include any of these as properties; I thought Temporal.Calendar was enough.

@ptomato
Copy link
Collaborator

ptomato commented Oct 19, 2020

@littledan Would you find the reason I gave above in #847 (comment) (many userland calendars would likely inherit from a specific built-in calendar) sufficient for including them as properties?

@littledan
Copy link
Member

I thought the idea was that all of the built-in calendars, including japanese, hebrew, islamic, buddhist, etc would be instances of Temporal.Calendar, not subclasses.

@ptomato
Copy link
Collaborator

ptomato commented Oct 19, 2020

That was originally the idea, but that seems inconvenient for userland calendars who want to inherit most of the behaviour from one built-in calendar. (For example, it's my understanding from interviews that it's common to have a Hijri calendar that allows a correction of one or two days in either direction for when the moon sighting doesn't agree with the tabular data.)

@littledan
Copy link
Member

@ptomato Couldn't those just call super('hijiri') in their constructor to make sure that their internal slots are filled with that behavior?

@ptomato
Copy link
Collaborator

ptomato commented Oct 22, 2020

That would give you the wrong value in your internal [[Identifier]] slot and then you'd have to override toString() in your subclass. (I don't know if that's a big objection or not)

@littledan
Copy link
Member

@ptomato How would this be different if we created different subclasses?

@Ms2ger
Copy link
Collaborator Author

Ms2ger commented Oct 23, 2020

Presumably you'd do something like

class MyHijiriCalendar extends Temporal.HijiriCalendar {
  constructor() {
    super('my-hijiri')
  }
}

Once you're subclassing, implementing toString doesn't seem like a huge burden, though. (We'd need to double-check that the spec doesn't use [[Identifier]] anywhere besides the superclass toString; I don't think it does.)

@ptomato
Copy link
Collaborator

ptomato commented Oct 23, 2020

I think maybe the question will be clearer if I provide full code examples of doing it one way and doing it the other way, that we can compare side-by-side; I'll do that within the next few days.

@ptomato
Copy link
Collaborator

ptomato commented Nov 2, 2020

OK, as promised, the example use case of implementing a Hijri calendar with date correction:

// Subclassing way:

const HijriCalendar = Temporal.Calendar.from('islamic').constructor;
// or there is some object called e.g. Temporal.Calendar['islamic']

class AdjustedHijriCalendar extends HijriCalendar {
  #deltaDays;
  constructor(deltaDays) {
    this.#deltaDays = deltaDays;
    super('adjusted-hijri');
  }

  #adjustDate(date) {
    return date.subtract(this.#deltaDays);
  }

  day(date) {
    return super.day(this.#adjustDate(date));
  }
  // etc.
}

Here, super.day() is Temporal.Calendar.from('islamic').constructor.prototype.day() which returns the Hijri calendar day from the internal [[ISOYear]], [[ISOMonth]], and [[ISODay]] slots.

// Switching on identifier way:

class AdjustedHijriCalendar2 extends Temporal.Calendar {
  #deltaDays;
  constructor(deltaDays) {
    this.#deltaDays = deltaDays;
    super('islamic');
  }

  #adjustDate(date) {
    return date.subtract(this.#deltaDays);
  }

  day(date) {
    return super.day(this.#adjustDate(date));
  }
  // etc.

  toString() {
    return 'adjusted-hijri';
  }
}

Here. super.day() is Temporal.Calendar.prototype.day(), which internally looks something like this in pseudocode:

day(date) {
  const id = this.[[Identifier]];
  switch (id) {
    case 'iso8601':
    case 'gregory':
      return date.[[ISODay]];
    case 'islamic':
      return calculateIslamicDay(date.[[ISOYear]], date.[[ISOMonth]], date.[[ISODay]]);
    // etc.
  }
}

Since the [[Identifier]] slot is used for determining the internal behaviour, the super() call in the constructor effectively takes over the role of the extends clause in the class definition. I find this weird and not in line with the way subclassing works elsewhere in JS that many developers would be familiar with.

Note that you also have to override toString(), otherwise you get new AdjustedHijriCalendar2().toString() === 'islamic', but that seems relatively harmless, though surprising.

@littledan littledan self-assigned this Nov 11, 2020
ptomato added a commit that referenced this issue Nov 15, 2020
We had originally decided to go with a scheme where the value of the
[[Identifier]] internal slot determines the behaviour of the calendar
methods, and there are not individual parent classes for each built-in
calendar. This implements that scheme.

Merges most of the spec text of ISO8601Calendar into Calendar.

Closes: #847
See: #300
@justingrant
Copy link
Collaborator

What implications (if any) does this change have on time zone subclassing? Does a custom time zone subclass require calling super with a valid ID for a built-in time zone? Or is any ID ok? I'm asking specifically in regards to my NYSE Time Zone cookbook sample PR #1177.

@ptomato
Copy link
Collaborator

ptomato commented Nov 16, 2020

Oh, glad you reminded me that it should have implications for time zones as well, I forgot all about that. I'll update the PR.

For the NYSE time zone, the only change you would need to make is to add constructor() { super('America/New_York'); }.

@justingrant
Copy link
Collaborator

For the NYSE time zone, the only change you would need to make is to add constructor() { super('America/New_York'); }.

But the behavior of that time zone is not consistent with America/New_York when the market is closed. Is that ok? What about a time zone that doesn't behave like any built-in time zone?

@ljharb
Copy link
Member

ljharb commented Nov 16, 2020

It seems like there should be a base class that accepts any arbitrary ID, or none at all, otherwise you couldn’t make a reasonable subclass that was anything beyond a slight deviation from a builtin calendar.

ptomato added a commit that referenced this issue Nov 16, 2020
This makes a small change to the constructor of Temporal.TimeZone so that
it doesn't accept any arbitrary ID, only a recognized built-in one, to
bring it in line with calendars.

See: #847
See: #300
@justingrant
Copy link
Collaborator

justingrant commented Nov 16, 2020

If a subclass's behavior deviates from the base class, then should it use the parent's ID or some other arbitrary ID when initializing super?

For a specific example, in the NYSE time zone in #1177, @ptomato you recommended that I shouldn't implement getOffsetStringFor and instead rely on the base class implementation call my custom getOffsetNanosecondsFor and turn its result into a string? Or will the base class emit the string using America/New_York instead of my custom offset value?

The general version of this question is which methods can a subclass safely not implement if it passes a built-in ID to super()? For time zones, my specific question is with the id property and the getOffsetStringFor method-- can I rely on those always delegating to my custom implementation, not to an implementation using the internal ID slot? Not sure about what's the equivalent question for calendars.

A second issue: is the built-in calendar is observable to users of a subclass? If it is, then that seems like a problem because a caller might have code like this:

if (date.calendar.id==='hijri') doSomething();

And assuming that your subclass deviates from the built-in calendar's behavior (because if it didn't deviate, why bother subclassing?) then that code above might be wrong because the calendar isn't really acting like the built-in one in all cases.

Certainly in the NYSE custom calendar case, you'd not want the subclass to expose its underlying time zone ID because most of the time (because the market is open 22% of the hours in a week) the results won't match the builtin America/New_York zone.

ptomato added a commit that referenced this issue Nov 16, 2020
We had originally decided to go with a scheme where the value of the
[[Identifier]] internal slot determines the behaviour of the calendar
methods, and there are not individual parent classes for each built-in
calendar. This implements that scheme.

Merges most of the spec text of ISO8601Calendar into Calendar.

Closes: #847
See: #300
ptomato added a commit that referenced this issue Nov 16, 2020
This makes a small change to the constructor of Temporal.TimeZone so that
it doesn't accept any arbitrary ID, only a recognized built-in one, to
bring it in line with calendars.

See: #847
See: #300
ptomato added a commit that referenced this issue Nov 16, 2020
We had originally decided to go with a scheme where the value of the
[[Identifier]] internal slot determines the behaviour of the calendar
methods, and there are not individual parent classes for each built-in
calendar. This implements that scheme.

Merges most of the spec text of ISO8601Calendar into Calendar.

Closes: #847
See: #300
ptomato added a commit that referenced this issue Nov 16, 2020
This makes a small change to the constructor of Temporal.TimeZone so that
it doesn't accept any arbitrary ID, only a recognized built-in one, to
bring it in line with calendars.

See: #847
See: #300
ptomato added a commit that referenced this issue Nov 16, 2020
We had originally decided to go with a scheme where the value of the
[[Identifier]] internal slot determines the behaviour of the calendar
methods, and there are not individual parent classes for each built-in
calendar. This implements that scheme.

Merges most of the spec text of ISO8601Calendar into Calendar.

Closes: #847
See: #300
ptomato added a commit that referenced this issue Nov 16, 2020
This makes a small change to the constructor of Temporal.TimeZone so that
it doesn't accept any arbitrary ID, only a recognized built-in one, to
bring it in line with calendars.

See: #847
See: #300
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Additions to documentation non-prod-polyfill THIS POLYFILL IS NOT FOR PRODUCTION USE! question spec-text Specification text involved
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants