diff --git a/app/lib/Temporal.test.ts b/app/lib/Temporal.test.ts index 2050418e..36af4f26 100644 --- a/app/lib/Temporal.test.ts +++ b/app/lib/Temporal.test.ts @@ -28,6 +28,7 @@ describe('Temporal', () => { expect(format(instant, 'D')).toEqual('9'); expect(format(instant, 'DD')).toEqual('09'); + expect(format(instant, 'Do')).toEqual('9th'); expect(format(instant, 'd')).toEqual('2'); expect(format(instant, 'dd')).toEqual('T'); expect(format(instant, 'dd', 'nl')).toEqual('D'); @@ -36,10 +37,18 @@ describe('Temporal', () => { expect(format(instant, 'dddd')).toEqual('Tuesday'); expect(format(instant, 'dddd', 'nl')).toEqual('dinsdag'); + expect(format(instant, 'W')).toEqual('2'); + expect(format(instant, 'w')).toEqual('2'); + expect(format(instant, 'Wo')).toEqual('2nd'); + expect(format(instant, 'WW')).toEqual('02'); + expect(format(instant, 'ww')).toEqual('02'); + expect(format(instant, 'H')).toEqual('19'); expect(format(instant, 'HH')).toEqual('19'); expect(format(instant, 'h')).toEqual('7'); expect(format(instant, 'hh')).toEqual('07'); + expect(format(instant, 'k')).toEqual('20'); + expect(format(instant, 'kk')).toEqual('20'); expect(format(instant, 'm')).toEqual('0'); expect(format(instant, 'mm')).toEqual('00'); @@ -47,14 +56,21 @@ describe('Temporal', () => { expect(format(instant, 's')).toEqual('6'); expect(format(instant, 'ss')).toEqual('06'); + expect(format(instant, 'X')).toEqual('1704826806'); + expect(format(instant, 'x')).toEqual('1704826806007'); + expect(format(instant, 'SSS')).toEqual('007'); expect(format(instant, 'SSSS')).toEqual('000'); expect(format(instant, 'Z')).toEqual('+00:00'); + expect(format(instant, 'z')).toEqual('UTC'); expect(format(instant, 'ZZ')).toEqual('+0000'); + expect(format(instant, 'zzz')).toEqual('UTC'); expect(format(instant, 'A')).toEqual('PM'); expect(format(instant, 'a')).toEqual('pm'); + + expect(format(instant, 'Q')).toEqual('1'); }); it('is able to format PlainDate', () => { @@ -72,6 +88,7 @@ describe('Temporal', () => { expect(format(plainDate, 'D')).toEqual('24'); expect(format(plainDate, 'DD')).toEqual('24'); + expect(format(plainDate, 'Do')).toEqual('24th'); expect(format(plainDate, 'd')).toEqual('6'); expect(format(plainDate, 'dd')).toEqual('S'); expect(format(plainDate, 'dd', 'nl')).toEqual('Z'); @@ -80,10 +97,18 @@ describe('Temporal', () => { expect(format(plainDate, 'dddd')).toEqual('Saturday'); expect(format(plainDate, 'dddd', 'nl')).toEqual('zaterdag'); + expect(format(plainDate, 'W')).toEqual('8'); + expect(format(plainDate, 'w')).toEqual('8'); + expect(format(plainDate, 'Wo')).toEqual('8th'); + expect(format(plainDate, 'WW')).toEqual('08'); + expect(format(plainDate, 'ww')).toEqual('08'); + expect(format(plainDate, 'H')).toEqual('0'); expect(format(plainDate, 'HH')).toEqual('00'); expect(format(plainDate, 'h')).toEqual('0'); expect(format(plainDate, 'hh')).toEqual('00'); + expect(format(plainDate, 'k')).toEqual('1'); + expect(format(plainDate, 'kk')).toEqual('01'); expect(format(plainDate, 'm')).toEqual('0'); expect(format(plainDate, 'mm')).toEqual('00'); @@ -91,14 +116,21 @@ describe('Temporal', () => { expect(format(plainDate, 's')).toEqual('0'); expect(format(plainDate, 'ss')).toEqual('00'); + expect(format(plainDate, 'X')).toEqual('1708732800'); + expect(format(plainDate, 'x')).toEqual('1708732800000'); + expect(format(plainDate, 'SSS')).toEqual('000'); expect(format(plainDate, 'SSSS')).toEqual('000'); expect(format(plainDate, 'Z')).toEqual('+00:00'); + expect(format(plainDate, 'z')).toEqual('UTC'); expect(format(plainDate, 'ZZ')).toEqual('+0000'); + expect(format(plainDate, 'zzz')).toEqual('UTC'); expect(format(plainDate, 'A')).toEqual('AM'); expect(format(plainDate, 'a')).toEqual('am'); + + expect(format(plainDate, 'Q')).toEqual('1'); }); it('is able to format PlainDateTime', () => { @@ -116,6 +148,7 @@ describe('Temporal', () => { expect(format(plainDateTime, 'D')).toEqual('9'); expect(format(plainDateTime, 'DD')).toEqual('09'); + expect(format(plainDateTime, 'Do')).toEqual('9th'); expect(format(plainDateTime, 'd')).toEqual('6'); expect(format(plainDateTime, 'dd')).toEqual('S'); expect(format(plainDateTime, 'dd', 'nl')).toEqual('Z'); @@ -124,10 +157,18 @@ describe('Temporal', () => { expect(format(plainDateTime, 'dddd')).toEqual('Saturday'); expect(format(plainDateTime, 'dddd', 'nl')).toEqual('zaterdag'); + expect(format(plainDateTime, 'W')).toEqual('10'); + expect(format(plainDateTime, 'w')).toEqual('10'); + expect(format(plainDateTime, 'Wo')).toEqual('10th'); + expect(format(plainDateTime, 'WW')).toEqual('10'); + expect(format(plainDateTime, 'ww')).toEqual('10'); + expect(format(plainDateTime, 'H')).toEqual('20'); expect(format(plainDateTime, 'HH')).toEqual('20'); expect(format(plainDateTime, 'h')).toEqual('8'); expect(format(plainDateTime, 'hh')).toEqual('08'); + expect(format(plainDateTime, 'k')).toEqual('21'); + expect(format(plainDateTime, 'kk')).toEqual('21'); expect(format(plainDateTime, 'm')).toEqual('1'); expect(format(plainDateTime, 'mm')).toEqual('01'); @@ -135,14 +176,21 @@ describe('Temporal', () => { expect(format(plainDateTime, 's')).toEqual('45'); expect(format(plainDateTime, 'ss')).toEqual('45'); + expect(format(plainDateTime, 'X')).toEqual('1710014505'); + expect(format(plainDateTime, 'x')).toEqual('1710014505000'); + expect(format(plainDateTime, 'SSS')).toEqual('000'); expect(format(plainDateTime, 'SSSS')).toEqual('000'); expect(format(plainDateTime, 'Z')).toEqual('+00:00'); + expect(format(plainDateTime, 'z')).toEqual('UTC'); expect(format(plainDateTime, 'ZZ')).toEqual('+0000'); + expect(format(plainDateTime, 'zzz')).toEqual('UTC'); expect(format(plainDateTime, 'A')).toEqual('PM'); expect(format(plainDateTime, 'a')).toEqual('pm'); + + expect(format(plainDateTime, 'Q')).toEqual('1'); }); it('is able to format PlainTime', () => { @@ -160,6 +208,7 @@ describe('Temporal', () => { expect(format(plainTime, 'D')).toEqual('1'); expect(format(plainTime, 'DD')).toEqual('01'); + expect(format(plainTime, 'Do')).toEqual('1st'); expect(format(plainTime, 'd')).toEqual('4'); expect(format(plainTime, 'dd')).toEqual('T'); expect(format(plainTime, 'dd', 'nl')).toEqual('D'); @@ -168,10 +217,18 @@ describe('Temporal', () => { expect(format(plainTime, 'dddd')).toEqual('Thursday'); expect(format(plainTime, 'dddd', 'nl')).toEqual('donderdag'); + expect(format(plainTime, 'W')).toEqual('53'); + expect(format(plainTime, 'w')).toEqual('53'); + expect(format(plainTime, 'Wo')).toEqual('53rd'); + expect(format(plainTime, 'WW')).toEqual('53'); + expect(format(plainTime, 'ww')).toEqual('53'); + expect(format(plainTime, 'H')).toEqual('13'); expect(format(plainTime, 'HH')).toEqual('13'); expect(format(plainTime, 'h')).toEqual('1'); expect(format(plainTime, 'hh')).toEqual('01'); + expect(format(plainTime, 'k')).toEqual('14'); + expect(format(plainTime, 'kk')).toEqual('14'); expect(format(plainTime, 'm')).toEqual('37'); expect(format(plainTime, 'mm')).toEqual('37'); @@ -179,14 +236,21 @@ describe('Temporal', () => { expect(format(plainTime, 's')).toEqual('0'); expect(format(plainTime, 'ss')).toEqual('00'); + expect(format(plainTime, 'X')).toEqual('49020'); + expect(format(plainTime, 'x')).toEqual('49020000'); + expect(format(plainTime, 'SSS')).toEqual('000'); expect(format(plainTime, 'SSSS')).toEqual('000'); expect(format(plainTime, 'Z')).toEqual('+00:00'); + expect(format(plainTime, 'z')).toEqual('UTC'); expect(format(plainTime, 'ZZ')).toEqual('+0000'); + expect(format(plainTime, 'zzz')).toEqual('UTC'); expect(format(plainTime, 'A')).toEqual('PM'); expect(format(plainTime, 'a')).toEqual('pm'); + + expect(format(plainTime, 'Q')).toEqual('1'); }); it('is able to format ZonedDateTime', () => { @@ -205,6 +269,7 @@ describe('Temporal', () => { expect(format(zonedDateTime, 'D')).toEqual('5'); expect(format(zonedDateTime, 'DD')).toEqual('05'); + expect(format(zonedDateTime, 'Do')).toEqual('5th'); expect(format(zonedDateTime, 'd')).toEqual('3'); expect(format(zonedDateTime, 'dd')).toEqual('W'); expect(format(zonedDateTime, 'dd', 'nl')).toEqual('W'); @@ -213,10 +278,18 @@ describe('Temporal', () => { expect(format(zonedDateTime, 'dddd')).toEqual('Wednesday'); expect(format(zonedDateTime, 'dddd', 'nl')).toEqual('woensdag'); + expect(format(zonedDateTime, 'W')).toEqual('23'); + expect(format(zonedDateTime, 'w')).toEqual('23'); + expect(format(zonedDateTime, 'Wo')).toEqual('23rd'); + expect(format(zonedDateTime, 'WW')).toEqual('23'); + expect(format(zonedDateTime, 'ww')).toEqual('23'); + expect(format(zonedDateTime, 'H')).toEqual('14'); expect(format(zonedDateTime, 'HH')).toEqual('14'); expect(format(zonedDateTime, 'h')).toEqual('2'); expect(format(zonedDateTime, 'hh')).toEqual('02'); + expect(format(zonedDateTime, 'k')).toEqual('15'); + expect(format(zonedDateTime, 'kk')).toEqual('15'); expect(format(zonedDateTime, 'm')).toEqual('30'); expect(format(zonedDateTime, 'mm')).toEqual('30'); @@ -224,13 +297,20 @@ describe('Temporal', () => { expect(format(zonedDateTime, 's')).toEqual('0'); expect(format(zonedDateTime, 'ss')).toEqual('00'); + expect(format(zonedDateTime, 'X')).toEqual('1717590600'); + expect(format(zonedDateTime, 'x')).toEqual('1717590600000'); + expect(format(zonedDateTime, 'SSS')).toEqual('000'); expect(format(zonedDateTime, 'SSSS')).toEqual('000'); expect(format(zonedDateTime, 'Z')).toEqual('+02:00'); + expect(format(zonedDateTime, 'z')).toEqual('Europe/Amsterdam'); expect(format(zonedDateTime, 'ZZ')).toEqual('+0200'); + expect(format(zonedDateTime, 'zzz')).toEqual('Europe/Amsterdam'); expect(format(zonedDateTime, 'A')).toEqual('PM'); expect(format(zonedDateTime, 'a')).toEqual('pm'); + + expect(format(zonedDateTime, 'Q')).toEqual('2'); }); }); diff --git a/app/lib/Temporal.ts b/app/lib/Temporal.ts index d95ff3c9..2822ede5 100644 --- a/app/lib/Temporal.ts +++ b/app/lib/Temporal.ts @@ -5,13 +5,25 @@ import { Temporal } from 'temporal-polyfill'; export { Temporal }; /** - * Regular expression using which we parse the format passed to the `format` function. This is - * copied directly from the DayJS repository, which is MIT licensed much like ourselves. + * Regular expression using which we parse the format passed to the `format` function. It combines + * the original expression with the one used in the _advancedFormat_ plugin, as we support both. * * @see https://github.com/iamkun/dayjs/blob/dev/src/constant.js + * @see https://github.com/iamkun/dayjs/blob/dev/src/plugin/advancedFormat/index.js */ const kFormatRegexp = - /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSSS|SSS/g; + /\[([^\]]+)]|Y{1,4}|M{1,4}|Do|D{1,2}|d{1,4}|Wo|W{1,2}|w{1,2}|H{1,2}|h{1,2}|k{1,2}|a|A|m{1,2}|s{1,2}|x|X|Z{1,2}|zzz|z|SSSS|SSS|Q/g; + +/** + * Returns the given `value` as a number witn an ordinal. I.e. "1" -> "1st", "4" -> "4th", etc. + * @see https://stackoverflow.com/a/31615643 + */ +function withOrdinal(number: number): string { + const suffices = [ 'th', 'st', 'nd', 'rd' ]; + const value = number % 100; + + return value + (suffices[ (value - 20) % 10 ] || suffices[value] || suffices[0]); +} /** * Formats the given `dateTime` to a string according to the indicated `format`. The implementation @@ -20,7 +32,50 @@ const kFormatRegexp = * `Instant`, `PlainDate`, `PlainDateTime` and `PlainTime` will be interpret as if they were in UTC, * whereas `ZonedDateTime` will maintain its associated timezone. * + * Format | Output | Description + * -------|-----------------------|-------------------------------------- + * YY | 18 | Two-digit year + * YYYY | 2018 | Four-digit year + * M | 1 - 12 | The month, beginning at 1 + * MM | 01 - 12 | The month, 2-digits + * MMM | Jan - Dec | The abbreviated month name + * MMMM | January - December | The full month name + * D | 1 - 31 | The day of the month + * DD | 01 - 31 | The day of the month, 2-digits + * Do | 1st 2nd ... 31st | Day of Month with ordinal + * d | 0 - 6 | The day of the week, with Sunday as 0 + * dd | Su - Sa | The min name of the day of the week + * ddd | Sun - Sat | The short name of the day of the week + * dddd | Sunday - Saturday | The name of the day of the week + * w | 1 2 ... 52 53 | Week of year + * ww | 01 02 ... 52 53 | Week of year, 2-digits + * Wo | 1st 2nd ... 52nd 53rd | Week of year with ordinal + * W | 1 2 ... 52 53 | ISO Week of year + * WW | 01 02 ... 52 53 | ISO Week of year, 2-digits + * H | 0 - 23 | The hour + * HH | 00 - 23 | The hour, 2-digits + * h | 1 - 12 | The hour, 12-hour clock + * hh | 01 - 12 | The hour, 12-hour clock, 2-digits + * k | 1 - 24 | The hour, beginning at 1 + * kk | 01 - 24 | The hour, 2-digits, beginning at 1 + * m | 0 - 59 | The minute + * mm | 00 - 59 | The minute, 2-digits + * s | 0 - 59 | The second + * ss | 00 - 59 | The second, 2-digits + * X | 1360013296 | Unix Timestamp in second + * x | 1360013296123 | Unix Timestamp in millisecond + * SSS | 000 - 999 | The millisecond, 3-digits + * SSSS | 000 - 999 | The microsecond, 3-digits + * Z | +05:00 | The offset from UTC, ±HH:mm + * ZZ | +0500 | The offset from UTC, ±HHmm + * z | EST | Abbreviated named offset + * zzz | Eastern Standard Time | Unabbreviated named offset + * A | AM PM | Meridiem, in capitals + * a | am pm | Meridiem, in lowercase + * Q | 1 - 4 | Quarter + * * @see https://day.js.org/docs/en/display/format + * @see https://day.js.org/docs/en/plugin/advanced-format */ export function format(dateTime: Temporal.Instant, format: string, locale?: string): string; export function format( @@ -33,7 +88,7 @@ export function format(dateTime: any, format: string, locale?: string): string { if (dateTime instanceof Temporal.Instant) { zonedDateTime = dateTime.toZonedDateTimeISO('UTC'); } else if (dateTime instanceof Temporal.PlainDate) { - zonedDateTime = dateTime.toZonedDateTime({ timeZone: 'UTC' }); + zonedDateTime = dateTime.toZonedDateTime('UTC'); } else if (dateTime instanceof Temporal.PlainDateTime) { zonedDateTime = dateTime.toZonedDateTime('UTC', { disambiguation: 'earlier' }); } else if (dateTime instanceof Temporal.PlainTime) { @@ -72,6 +127,8 @@ export function format(dateTime: any, format: string, locale?: string): string { return `0${fields.isoDay}`.substr(-2); case 'd': return `${zonedDateTime.dayOfWeek - 1}`; + case 'Do': // advancedFormat plugin + return withOrdinal(fields.isoDay); case 'dd': return zonedDateTime.toLocaleString(effectiveLocale, { weekday: 'narrow' }); case 'ddd': @@ -79,6 +136,15 @@ export function format(dateTime: any, format: string, locale?: string): string { case 'dddd': return zonedDateTime.toLocaleString(effectiveLocale, { weekday: 'long' }); + case 'W': // advancedFormat plugin + case 'w': // advancedFormat plugin + return `${zonedDateTime.weekOfYear}`; + case 'Wo': // advancedFormat plugin + return withOrdinal(zonedDateTime.weekOfYear); + case 'WW': // advancedFormat plugin + case 'ww': // advancedFormat plugin + return `0${zonedDateTime.weekOfYear}`.substr(-2); + case 'H': return `${fields.isoHour}`; case 'HH': @@ -87,6 +153,10 @@ export function format(dateTime: any, format: string, locale?: string): string { return `${fields.isoHour % 12}`; case 'hh': return `0${fields.isoHour % 12}`.substr(-2); + case 'k': // advancedFormat plugin + return `${fields.isoHour + 1}`; + case 'kk': // advancedFormat plugin + return `0${fields.isoHour + 1}`.substr(-2); case 'm': return `${fields.isoMinute}`; @@ -98,6 +168,11 @@ export function format(dateTime: any, format: string, locale?: string): string { case 'ss': return `0${fields.isoSecond}`.substr(-2); + case 'X': // advancedFormat plugin + return `${zonedDateTime.epochSeconds}`; + case 'x': // advancedFormat plugin + return `${zonedDateTime.epochMilliseconds}`; + case 'SSS': return `00${fields.isoMillisecond}`.substr(-3); case 'SSSS': @@ -105,6 +180,9 @@ export function format(dateTime: any, format: string, locale?: string): string { case 'Z': return fields.offset; + case 'z': // advancedFormat plugin + case 'zzz': // advancedFormat plugin + return zonedDateTime.timeZoneId; case 'ZZ': return fields.offset.replace(':', ''); @@ -113,6 +191,9 @@ export function format(dateTime: any, format: string, locale?: string): string { case 'a': return fields.isoHour < 12 ? 'am' : 'pm'; + case 'Q': // advancedFormat plugin + return `${Math.ceil(fields.isoMonth / 3)}`; + default: throw new Error(`Invalid formatting parameter received (f=${format}, v=${match})`); }