22import * as Temporal from '../../polyfill/lib/temporal.mjs' ;
33import ICAL from 'ical.js' ;
44
5- // The time zone can either be a named IANA time zone (in which case everything
6- // works just like Temporal.ZonedDateTime) or an iCalendar rule-based time zone
5+ // Example of a wrapper class for Temporal.ZonedDateTime that implements custom
6+ // time zones.
7+ // The use case is based on Thunderbird's use of the ical.js library to parse
8+ // iCalendar data. iCalendar uses VTIMEZONE components which define UTC offset
9+ // transitions inside the data format. VTIMEZONE can include a TZID field, which
10+ // may or may not be an IANA time zone ID. If it's an IANA time zone ID,
11+ // Thunderbird uses the environment's TZDB definition and ignores the rest of
12+ // the VTIMEZONE (in which case everything works just like
13+ // Temporal.ZonedDateTime, as we delegate to the this.#impl object). However,
14+ // Microsoft Exchange often generates TZID strings that aren't IANA IDs, and
15+ // then Thunderbird falls back to the iCalendar VTIMEZONE definition (in which
16+ // case we use ical.js to perform the time zone calculations.)
17+
718class ZonedDateTime {
19+ // #impl: The internal Temporal.ZonedDateTime object. If the VTIMEZONE is an
20+ // IANA time zone, its timeZoneId is the VTIMEZONE's TZID, and we delegate all
21+ // the operations to it. If not, its timeZoneId is UTC.
822 #impl;
9- #timeZone;
10- #isIANA;
23+ #timeZone; // The ICAL.Timezone instance.
24+ #isIANA; // Convenience flag indicating whether we can delegate to #impl.
1125
1226 // These properties allow the object to be used as a PlainDateTime property
13- // bag if the time zone isn't IANA
27+ // bag if the time zone isn't IANA. For example, as a relativeTo parameter in
28+ // Duration methods.
1429 era ;
1530 eraYear ;
1631 year ;
@@ -84,7 +99,7 @@ class ZonedDateTime {
8499 } ,
85100 timeZone
86101 ) ;
87- const epochSeconds = icalTime . toUnixTime ( ) ; // apply disambiguation parameter?
102+ const epochSeconds = icalTime . toUnixTime ( ) ; // TODO: apply disambiguation parameter?
88103 const epochNanoseconds =
89104 BigInt ( epochSeconds ) * 1000000000n + BigInt ( pdt . millisecond * 1e6 + pdt . microsecond * 1e3 + pdt . nanosecond ) ;
90105 return new ZonedDateTime ( epochNanoseconds , timeZone , pdt . calendarId ) ;
@@ -98,6 +113,8 @@ class ZonedDateTime {
98113 if ( this . #isIANA) {
99114 return this . #impl. toPlainDateTime ( ) ;
100115 }
116+ // this.#impl with a non-IANA time zone uses UTC internally, so we can just
117+ // calculate the plain date-time in UTC and add the UTC offset.
101118 return this . #impl. toPlainDateTime ( ) . add ( { nanoseconds : this . offsetNanoseconds } ) ;
102119 }
103120
@@ -167,6 +184,8 @@ class ZonedDateTime {
167184 this . #isIANA ||
168185 ( duration . years === 0 && duration . months === 0 && duration . weeks === 0 && duration . days === 0 )
169186 ) {
187+ // Adding non-calendar units is independent of time zone, so in that case
188+ // we can delegate to this.#impl even in the case of a non-IANA time zone
170189 const temporalZDT = this . #impl. add ( duration , options ) ;
171190 return new ZonedDateTime ( temporalZDT . epochNanoseconds , this . #timeZone, this . #impl. calendarId ) ;
172191 }
@@ -202,6 +221,8 @@ class ZonedDateTime {
202221 if ( largestUnit === 'year' || largestUnit === 'month' || largestUnit === 'week' || largestUnit === 'day' ) {
203222 throw new Error ( 'not implemented' ) ;
204223 }
224+ // Non-calendar largestUnit is independent of time zone, so we can delegate
225+ // to this.#impl even in the case of a non-IANA time zone
205226 return this . #impl. until ( other . #impl, options ) ;
206227 }
207228
0 commit comments