|
| 1 | +function bigIntAbs(n) { |
| 2 | + if (n < 0n) return -n; |
| 3 | + return n; |
| 4 | +} |
| 5 | + |
| 6 | +// The years are unlimited, but for output purposes we assume 10 digits, |
| 7 | +// because ISO 8601 requires the expanded year format to pick a consistent |
| 8 | +// number of digits. |
| 9 | +function formatExpandedYear(year) { |
| 10 | + let yearString; |
| 11 | + if (year < 1000 || year > 9999) { |
| 12 | + let sign = year < 0 ? '-' : '+'; |
| 13 | + let yearNumber = bigIntAbs(year); |
| 14 | + yearString = sign + `${yearNumber}`.padStart(10, '0'); |
| 15 | + } else { |
| 16 | + yearString = `${year}`; |
| 17 | + } |
| 18 | + return yearString; |
| 19 | +} |
| 20 | + |
| 21 | +function isLeapYear(year) { |
| 22 | + const isDiv4 = year % 4n === 0n; |
| 23 | + const isDiv100 = year % 100n === 0n; |
| 24 | + const isDiv400 = year % 400n === 0n; |
| 25 | + return isDiv4 && (!isDiv100 || isDiv400); |
| 26 | +} |
| 27 | + |
| 28 | +// This checks to see if the ISO string matches our 10-digit expanded year |
| 29 | +// format, and if so, returns both the expanded year as a BigInt, and a new |
| 30 | +// ISO string with an in-range year that can be passed to the original |
| 31 | +// Temporal string parsing functions. |
| 32 | +// The in-range year is 1972 if the expanded year is a leap year, and |
| 33 | +// otherwise 1970, so that the rules for February 29 remain correct. |
| 34 | +// See the note about the number of digits in formatExpandedYear(). |
| 35 | +function parseExpandedYear(isoString) { |
| 36 | + const matchExpandedYear = /^[-+\u2212]\d{10}/; |
| 37 | + const result = matchExpandedYear.exec(isoString); |
| 38 | + if (!result) return { isoString }; |
| 39 | + const expandedYear = BigInt(result[0]); |
| 40 | + const isoYear = isLeapYear(expandedYear) ? 1972 : 1970; |
| 41 | + return { |
| 42 | + expandedYear, |
| 43 | + isoString: isoString.replace(matchExpandedYear, isoYear.toString()) |
| 44 | + }; |
| 45 | +} |
| 46 | + |
| 47 | +// This is a map of Temporal objects to their expanded year (as BigInt). |
| 48 | +// The data model consists of the Temporal object (with the ISO year set |
| 49 | +// internally to 1970 or 1972) and the expanded year. This map is used to |
| 50 | +// associate Temporal objects with their expanded years, instead of defining |
| 51 | +// extra properties on the Temporal object. |
| 52 | +const expandedYears = new WeakMap(); |
| 53 | + |
| 54 | +class ExpandedPlainDate extends Temporal.PlainDate { |
| 55 | + // The expanded-year versions of the Temporal types are limited to using the |
| 56 | + // ISO calendar. |
| 57 | + constructor(year, isoMonth, isoDay) { |
| 58 | + year = BigInt(year); |
| 59 | + const isoYear = isLeapYear(year) ? 1972 : 1970; |
| 60 | + super(isoYear, isoMonth, isoDay, 'iso8601'); |
| 61 | + expandedYears.set(this, year); |
| 62 | + } |
| 63 | + |
| 64 | + static _convert(plainDate, expandedYear) { |
| 65 | + if (plainDate instanceof ExpandedPlainDate) return plainDate; |
| 66 | + const f = plainDate.getISOFields(); |
| 67 | + return new this(expandedYear, f.isoMonth, f.isoDay); |
| 68 | + } |
| 69 | + |
| 70 | + static from(item) { |
| 71 | + if (typeof item === 'string') { |
| 72 | + const { expandedYear, isoString } = parseExpandedYear(item); |
| 73 | + item = Temporal.PlainDate.from(isoString); |
| 74 | + if (expandedYear) return this._convert(item, expandedYear); |
| 75 | + } |
| 76 | + if (item instanceof Temporal.PlainDate) { |
| 77 | + return this._convert(item, BigInt(item.year)); |
| 78 | + } |
| 79 | + const calendar = Temporal.Calendar.from('iso8601'); |
| 80 | + return calendar.dateFromFields(item, undefined, this); |
| 81 | + } |
| 82 | + |
| 83 | + // This overrides the .year property to return the expanded year instead. If |
| 84 | + // you were doing this with a calendar, you would instead need to make a |
| 85 | + // separate field. (But Instant doesn't have a calendar, so that solution |
| 86 | + // wouldn't be able to completely expand Temporal.) |
| 87 | + get year() { |
| 88 | + return expandedYears.get(this); |
| 89 | + } |
| 90 | + |
| 91 | + toString() { |
| 92 | + const year = formatExpandedYear(this.year); |
| 93 | + const { isoMonth, isoDay } = this.getISOFields(); |
| 94 | + const month = `${isoMonth}`.padStart(2, '0'); |
| 95 | + const day = `${isoDay}`.padStart(2, '0'); |
| 96 | + return `${year}-${month}-${day}`; |
| 97 | + } |
| 98 | +} |
| 99 | + |
| 100 | +class ExpandedPlainDateTime extends Temporal.PlainDateTime { |
| 101 | + constructor(year, isoMonth, isoDay, hour, minute, second, millisecond, microsecond, nanosecond) { |
| 102 | + year = BigInt(year); |
| 103 | + const isoYear = isLeapYear(year) ? 1972 : 1970; |
| 104 | + super(isoYear, isoMonth, isoDay, hour, minute, second, millisecond, microsecond, nanosecond, 'iso8601'); |
| 105 | + expandedYears.set(this, year); |
| 106 | + } |
| 107 | + |
| 108 | + static _convert(plainDateTime, expandedYear) { |
| 109 | + if (plainDateTime instanceof ExpandedPlainDateTime) return plainDateTime; |
| 110 | + const f = plainDateTime.getISOFields(); |
| 111 | + return new this( |
| 112 | + expandedYear, |
| 113 | + f.isoMonth, |
| 114 | + f.isoDay, |
| 115 | + f.isoHour, |
| 116 | + f.isoMinute, |
| 117 | + f.isoSecond, |
| 118 | + f.isoMillisecond, |
| 119 | + f.isoMicrosecond, |
| 120 | + f.isoNanosecond |
| 121 | + ); |
| 122 | + } |
| 123 | + |
| 124 | + static from(item) { |
| 125 | + if (typeof item === 'string') { |
| 126 | + const { expandedYear, isoString } = parseExpandedYear(item); |
| 127 | + item = Temporal.PlainDateTime.from(isoString); |
| 128 | + if (expandedYear) return this._convert(item, expandedYear); |
| 129 | + } |
| 130 | + if (item instanceof Temporal.PlainDateTime) { |
| 131 | + return this._convert(item, BigInt(item.year)); |
| 132 | + } |
| 133 | + const calendar = Temporal.Calendar.from('iso8601'); |
| 134 | + return calendar.dateFromFields(item, undefined, this); |
| 135 | + } |
| 136 | + |
| 137 | + get year() { |
| 138 | + return expandedYears.get(this); |
| 139 | + } |
| 140 | + |
| 141 | + toString(options = {}) { |
| 142 | + const dateString = this.toPlainDate().toString({ |
| 143 | + ...options, |
| 144 | + showCalendar: 'never' |
| 145 | + }); |
| 146 | + const timeString = this.toPlainTime().toString(options); |
| 147 | + return `${dateString}T${timeString}`; |
| 148 | + } |
| 149 | + |
| 150 | + toPlainDate() { |
| 151 | + return ExpandedPlainDate._convert(super.toPlainDate(), this.year); |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +class ExpandedPlainTime extends Temporal.PlainTime { |
| 156 | + toPlainDateTime(date) { |
| 157 | + return ExpandedPlainDateTime._convert(super.toPlainDateTime(date), date.year); |
| 158 | + } |
| 159 | +} |
| 160 | + |
| 161 | +function makeExpandedTemporal() { |
| 162 | + return { |
| 163 | + ...Temporal, |
| 164 | + PlainDate: ExpandedPlainDate, |
| 165 | + PlainDateTime: ExpandedPlainDateTime, |
| 166 | + PlainTime: ExpandedPlainTime |
| 167 | + }; |
| 168 | +} |
| 169 | + |
| 170 | +const ExpandedTemporal = makeExpandedTemporal(); |
| 171 | + |
| 172 | +const date = ExpandedTemporal.PlainDate.from({ year: 635427810, month: 2, day: 2 }); |
| 173 | +assert.equal(date.toString(), '+0635427810-02-02'); |
| 174 | +const dateTime = ExpandedTemporal.PlainTime.from('10:23').toPlainDateTime(date); |
| 175 | +assert.equal(dateTime.toString(), '+0635427810-02-02T10:23:00'); |
| 176 | +const dateFromString = ExpandedTemporal.PlainDateTime.from('-0075529144-02-29T12:53:27.55'); |
| 177 | +assert.equal(dateFromString.year, -75529144n); |
0 commit comments