Skip to content

Commit 741cc80

Browse files
committed
WIP: Cookbook example showing how to handle icalendar time zones
TODO: Add test data & make sure it works TODO: Comment code better & add page in cookbook
1 parent 26e4ceb commit 741cc80

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
import * as Temporal from '../../polyfill/lib/temporal.mjs';
2+
import ICAL from 'ical.js';
3+
4+
// The time zone can either be a named IANA time zone (in which case everything
5+
// works just like Temporal.ZonedDateTime) or an iCalendar rule-based time zone
6+
class ZonedDateTime {
7+
#impl;
8+
#timeZone;
9+
#isIANA;
10+
11+
// These properties allow the object to be used as a PlainDateTime property
12+
// bag if the time zone isn't IANA
13+
era;
14+
eraYear;
15+
year;
16+
month;
17+
monthCode;
18+
day;
19+
hour;
20+
minute;
21+
second;
22+
millisecond;
23+
microsecond;
24+
nanosecond;
25+
calendar;
26+
27+
// This property additionally allows the object to be used as a ZonedDateTime
28+
// property bag if the time zone is IANA
29+
timeZone;
30+
31+
constructor(epochNs, timeZone, calendar = 'iso8601') {
32+
this.#timeZone = timeZone;
33+
this.#isIANA = Intl.supportedValuesOf('timeZone').includes(timeZone.tzid);
34+
this.#impl = new Temporal.ZonedDateTime(epochNs, this.#isIANA ? this.#timeZone.tzid : 'UTC', calendar);
35+
36+
// Define public property-bag properties
37+
if (this.#isIANA) {
38+
this.timeZone = timeZone.tzid;
39+
}
40+
this.calendar = calendar;
41+
42+
const pdt = this.toPlainDateTime();
43+
this.era = pdt.era;
44+
this.eraYear = pdt.eraYear;
45+
this.year = pdt.year;
46+
this.month = pdt.month;
47+
this.monthCode = pdt.monthCode;
48+
this.day = pdt.day;
49+
this.hour = pdt.hour;
50+
this.minute = pdt.minute;
51+
this.second = pdt.second;
52+
this.millisecond = pdt.millisecond;
53+
this.microsecond = pdt.microsecond;
54+
this.nanosecond = pdt.nanosecond;
55+
}
56+
57+
// For now, from() only clones; semantics of deserialization from string are
58+
// yet to be defined
59+
static from(item) {
60+
return new ZonedDateTime(item.#impl.epochNanoseconds, item.#timeZone, item.#impl.calendarId);
61+
}
62+
63+
// Use this method instead of Instant.prototype.toZonedDateTimeISO()
64+
static fromInstant(instant, timeZone, calendar = 'iso8601') {
65+
return new ZonedDateTime(instant.epochNanoseconds, timeZone, calendar);
66+
}
67+
68+
// Use this method instead of PlainDateTime.prototype.toZonedDateTime() and
69+
// PlainDate.prototype.toZonedDateTime()
70+
static fromPlainDateTime(pdt, timeZone, options) {
71+
if (timeZone.tzid) {
72+
const temporalZDT = pdt.toZonedDateTime(timeZone.tzid, options);
73+
return new ZonedDateTime(temporalZDT.epochNanoseconds, timeZone, pdt.calendarId);
74+
}
75+
const icalTime = new ICAL.Time(pdt, timeZone);
76+
const epochSeconds = icalTime.toUnixTime(); // apply disambiguation parameter?
77+
const epochNanoseconds =
78+
BigInt(epochSeconds) * 1000000000n + BigInt(pdt.millisecond * 1e6 + pdt.microsecond * 1e3 + pdt.nanosecond);
79+
return new ZonedDateTime(epochNanoseconds, timeZone, pdt.calendarId);
80+
}
81+
82+
static compare(a, b) {
83+
return Temporal.ZonedDateTime.compare(a.#impl, b.#impl);
84+
}
85+
86+
toPlainDateTime() {
87+
if (this.#isIANA) {
88+
return this.#impl.toPlainDateTime();
89+
}
90+
return this.#impl.toPlainDateTime().add({ nanoseconds: this.offsetNanoseconds });
91+
}
92+
93+
get offsetNanoseconds() {
94+
if (this.#isIANA) {
95+
return this.#impl.offsetNanoseconds;
96+
}
97+
const epochSeconds = Math.floor(this.#impl.epochMilliseconds / 1000);
98+
const utcTime = new ICAL.Time();
99+
utcTime.fromUnixTime(epochSeconds);
100+
const time = utcTime.convertToZone(this.#timeZone);
101+
const offsetSeconds = this.#timeZone.utcOffset(time);
102+
return offsetSeconds * 1e9;
103+
}
104+
105+
// similar to the other xOfY properties, only showing one for the example
106+
get dayOfWeek() {
107+
return this.toPlainDateTime().dayOfWeek;
108+
}
109+
// ...get dayOfYear(), etc. omitted because they are very similar to the above
110+
111+
#isoDateTimePartString(n) {
112+
return String(n).padStart(2, '0');
113+
}
114+
115+
get offset() {
116+
const offsetNs = this.offsetNanoseconds;
117+
const sign = offsetNs < 0 ? '-' : '+';
118+
const absoluteNs = Math.abs(offsetNs);
119+
const hour = Math.floor(absoluteNs / 3600e9);
120+
const minute = Math.floor(absoluteNs / 60e9) % 60;
121+
const second = Math.floor(absoluteNs / 1e9) % 60;
122+
let result = `${sign}${this.#isoDateTimePartString(hour)}:${this.#isoDateTimePartString(minute)}`;
123+
if (second === 0) {
124+
return result;
125+
}
126+
result += `:${this.#isoDateTimePartString(second)}`;
127+
return result;
128+
}
129+
130+
get epochMilliseconds() {
131+
return this.#impl.epochMilliseconds;
132+
}
133+
134+
get epochNanoseconds() {
135+
return this.#impl.epochNanoseconds;
136+
}
137+
138+
// PlainTime property bag and string arguments omitted for brevity
139+
withPlainTime(time) {
140+
const pdt = this.toPlainDateTime();
141+
return ZonedDateTime.fromPlainDateTime(pdt.withPlainTime(time), this.#timeZone);
142+
}
143+
144+
withCalendar(calendar) {
145+
return new ZonedDateTime(this.#impl.epochNanoseconds, this.#timeZone, calendar);
146+
}
147+
148+
withTimeZone(timeZone) {
149+
return new ZonedDateTime(this.#impl.epochNanoseconds, timeZone, this.#impl.calendarId);
150+
}
151+
152+
// Not currently implemented, for brevity: duration property bag and duration
153+
// string inputs
154+
add(duration, options) {
155+
if (
156+
this.#isIANA ||
157+
(duration.years === 0 && duration.months === 0 && duration.weeks === 0 && duration.days === 0)
158+
) {
159+
const temporalZDT = this.#impl.add(duration, options);
160+
return new ZonedDateTime(temporalZDT.epochNanoseconds, this.#timeZone, this.#impl.calendarId);
161+
}
162+
const pdt = this.toPlainDateTime().add(
163+
{
164+
years: duration.years,
165+
months: duration.months,
166+
weeks: duration.weeks,
167+
days: duration.days
168+
},
169+
options
170+
);
171+
const intermediate = ZonedDateTime.fromPlainDateTime(pdt, this.#timeZone, { disambiguation: 'compatible' });
172+
return intermediate.add(
173+
Temporal.Duration.from({
174+
hours: duration.hours,
175+
minutes: duration.minutes,
176+
seconds: duration.seconds,
177+
milliseconds: duration.milliseconds,
178+
microseconds: duration.microseconds,
179+
nanoseconds: duration.nanoseconds
180+
})
181+
);
182+
}
183+
184+
// Not currently implemented, for brevity: property bag and string inputs;
185+
// plural forms of largestUnit
186+
// largestUnit > "hours" is also not currently implemented because that would
187+
// require semantics for equality of two ICAL.Timezone instances (see the note
188+
// about equals() below)
189+
until(other, options) {
190+
const { largestUnit = 'hour' } = options ?? {};
191+
if (largestUnit === 'year' || largestUnit === 'month' || largestUnit === 'week' || largestUnit === 'day') {
192+
throw new Error('not implemented');
193+
}
194+
return this.#impl.until(other.#impl, options);
195+
}
196+
197+
startOfDay() {
198+
const pdt = this.toPlainDateTime();
199+
const midnight = Temporal.PlainTime.from('00:00');
200+
return ZonedDateTime.fromPlainDateTime(pdt.withPlainTime(midnight), this.#timeZone, {
201+
disambiguation: 'compatible'
202+
});
203+
}
204+
205+
toInstant() {
206+
return this.#impl.toInstant();
207+
}
208+
209+
toPlainDate() {
210+
return this.toPlainDateTime().toPlainDate();
211+
}
212+
213+
toPlainTime() {
214+
return this.toPlainDateTime().toPlainTime();
215+
}
216+
217+
valueOf() {
218+
throw new TypeError();
219+
}
220+
221+
// Methods that are not implemented, and why:
222+
// Semantics for equality of ICAL.Timezone not defined, so omitting this
223+
// method for now, as its semantics would need to be better defined
224+
equals(other) {
225+
if (this.#isIANA && other.#isIANA) {
226+
return this.#impl.equals(other.#impl);
227+
}
228+
throw new Error('not implemented');
229+
}
230+
231+
// Not currently implemented, for brevity
232+
with(zonedDateTimeLike, options) {
233+
if (this.#isIANA) {
234+
const temporalZDT = this.#impl.with(zonedDateTimeLike, options);
235+
return new ZonedDateTime(temporalZDT.epochNanoseconds, this.#timeZone, this.#impl.calendarId);
236+
}
237+
throw new Error('not implemented');
238+
}
239+
240+
// Not currently implemented, for brevity
241+
round(options) {
242+
if (this.#isIANA) {
243+
return this.#impl.round(options);
244+
}
245+
throw new Error('not implemented');
246+
}
247+
248+
// ICAL.Timezone doesn't yet have a method for fetching prev/next transition,
249+
// so omitting this method for now
250+
getTimeZoneTransition(direction) {
251+
if (this.#isIANA) {
252+
const temporalZDTorNull = this.#impl.getTimeZoneTransition(direction);
253+
if (temporalZDTorNull === null) {
254+
return null;
255+
}
256+
return new ZonedDateTime(temporalZDTorNull.epochNanoseconds, this.#timeZone, this.#impl.calendarId);
257+
}
258+
throw new Error('not implemented');
259+
}
260+
261+
// Omitting these three convert-to-string methods for now, semantics of
262+
// (de)serialization are yet to be defined. Would also need to figure out how
263+
// to get localized output for toLocaleString() in particular.
264+
toLocaleString(locales, options) {
265+
if (this.#isIANA) {
266+
return this.#impl.toLocaleString(locales, options);
267+
}
268+
throw new Error('not implemented');
269+
}
270+
271+
toString(options) {
272+
if (this.#isIANA) {
273+
return this.#impl.toString(options);
274+
}
275+
throw new Error('not implemented');
276+
}
277+
278+
toJSON() {
279+
return this.toString();
280+
}
281+
}

0 commit comments

Comments
 (0)