|
| 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