Skip to content

Commit

Permalink
Import the Temporal polyfill, add a utility to format dates and times
Browse files Browse the repository at this point in the history
  • Loading branch information
beverloo committed Jan 26, 2024
1 parent bb3de1c commit 028bcfb
Show file tree
Hide file tree
Showing 4 changed files with 424 additions and 49 deletions.
236 changes: 236 additions & 0 deletions app/lib/Temporal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
// Copyright 2024 Peter Beverloo & AnimeCon. All rights reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.

import { Temporal, format } from './Temporal';

describe('Temporal', () => {
it('is able to parse formats correctly', () => {
const instant = Temporal.Instant.from('2024-01-09T19:00:06.007Z');

expect(format(instant, 'YYYY-MM-DD')).toEqual('2024-01-09');
expect(format(instant, 'HH:mm:ss')).toEqual('19:00:06');
expect(format(instant, '[YYYY-MM-DD]')).toEqual('YYYY-MM-DD');
expect(format(instant, 'YYYY-[MM]-DD')).toEqual('2024-MM-09');
});

it('is able to format Instant', () => {
const instant = Temporal.Instant.from('2024-01-09T19:00:06.007Z');

expect(format(instant, 'YY')).toEqual('24');
expect(format(instant, 'YYYY')).toEqual('2024');

expect(format(instant, 'M')).toEqual('1');
expect(format(instant, 'MM')).toEqual('01');
expect(format(instant, 'MMM')).toEqual('Jan');
expect(format(instant, 'MMM', 'nl')).toEqual('jan');
expect(format(instant, 'MMMM')).toEqual('January');
expect(format(instant, 'MMMM', 'nl')).toEqual('januari');

expect(format(instant, 'D')).toEqual('9');
expect(format(instant, 'DD')).toEqual('09');
expect(format(instant, 'd')).toEqual('2');
expect(format(instant, 'dd')).toEqual('T');
expect(format(instant, 'dd', 'nl')).toEqual('D');
expect(format(instant, 'ddd')).toEqual('Tue');
expect(format(instant, 'ddd', 'nl')).toEqual('di');
expect(format(instant, 'dddd')).toEqual('Tuesday');
expect(format(instant, 'dddd', 'nl')).toEqual('dinsdag');

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, 'm')).toEqual('0');
expect(format(instant, 'mm')).toEqual('00');

expect(format(instant, 's')).toEqual('6');
expect(format(instant, 'ss')).toEqual('06');

expect(format(instant, 'SSS')).toEqual('007');
expect(format(instant, 'SSSS')).toEqual('000');

expect(format(instant, 'Z')).toEqual('+00:00');
expect(format(instant, 'ZZ')).toEqual('+0000');

expect(format(instant, 'A')).toEqual('PM');
expect(format(instant, 'a')).toEqual('pm');
});

it('is able to format PlainDate', () => {
const plainDate = Temporal.PlainDate.from('2024-02-24');

expect(format(plainDate, 'YY')).toEqual('24');
expect(format(plainDate, 'YYYY')).toEqual('2024');

expect(format(plainDate, 'M')).toEqual('2');
expect(format(plainDate, 'MM')).toEqual('02');
expect(format(plainDate, 'MMM')).toEqual('Feb');
expect(format(plainDate, 'MMM', 'nl')).toEqual('feb');
expect(format(plainDate, 'MMMM')).toEqual('February');
expect(format(plainDate, 'MMMM', 'nl')).toEqual('februari');

expect(format(plainDate, 'D')).toEqual('24');
expect(format(plainDate, 'DD')).toEqual('24');
expect(format(plainDate, 'd')).toEqual('6');
expect(format(plainDate, 'dd')).toEqual('S');
expect(format(plainDate, 'dd', 'nl')).toEqual('Z');
expect(format(plainDate, 'ddd')).toEqual('Sat');
expect(format(plainDate, 'ddd', 'nl')).toEqual('za');
expect(format(plainDate, 'dddd')).toEqual('Saturday');
expect(format(plainDate, 'dddd', 'nl')).toEqual('zaterdag');

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, 'm')).toEqual('0');
expect(format(plainDate, 'mm')).toEqual('00');

expect(format(plainDate, 's')).toEqual('0');
expect(format(plainDate, 'ss')).toEqual('00');

expect(format(plainDate, 'SSS')).toEqual('000');
expect(format(plainDate, 'SSSS')).toEqual('000');

expect(format(plainDate, 'Z')).toEqual('+00:00');
expect(format(plainDate, 'ZZ')).toEqual('+0000');

expect(format(plainDate, 'A')).toEqual('AM');
expect(format(plainDate, 'a')).toEqual('am');
});

it('is able to format PlainDateTime', () => {
const plainDateTime = Temporal.PlainDateTime.from('2024-03-09T20:01:45');

expect(format(plainDateTime, 'YY')).toEqual('24');
expect(format(plainDateTime, 'YYYY')).toEqual('2024');

expect(format(plainDateTime, 'M')).toEqual('3');
expect(format(plainDateTime, 'MM')).toEqual('03');
expect(format(plainDateTime, 'MMM')).toEqual('Mar');
expect(format(plainDateTime, 'MMM', 'nl')).toEqual('mrt');
expect(format(plainDateTime, 'MMMM')).toEqual('March');
expect(format(plainDateTime, 'MMMM', 'nl')).toEqual('maart');

expect(format(plainDateTime, 'D')).toEqual('9');
expect(format(plainDateTime, 'DD')).toEqual('09');
expect(format(plainDateTime, 'd')).toEqual('6');
expect(format(plainDateTime, 'dd')).toEqual('S');
expect(format(plainDateTime, 'dd', 'nl')).toEqual('Z');
expect(format(plainDateTime, 'ddd')).toEqual('Sat');
expect(format(plainDateTime, 'ddd', 'nl')).toEqual('za');
expect(format(plainDateTime, 'dddd')).toEqual('Saturday');
expect(format(plainDateTime, 'dddd', 'nl')).toEqual('zaterdag');

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, 'm')).toEqual('1');
expect(format(plainDateTime, 'mm')).toEqual('01');

expect(format(plainDateTime, 's')).toEqual('45');
expect(format(plainDateTime, 'ss')).toEqual('45');

expect(format(plainDateTime, 'SSS')).toEqual('000');
expect(format(plainDateTime, 'SSSS')).toEqual('000');

expect(format(plainDateTime, 'Z')).toEqual('+00:00');
expect(format(plainDateTime, 'ZZ')).toEqual('+0000');

expect(format(plainDateTime, 'A')).toEqual('PM');
expect(format(plainDateTime, 'a')).toEqual('pm');
});

it('is able to format PlainTime', () => {
const plainTime = Temporal.PlainTime.from('13:37');

expect(format(plainTime, 'YY')).toEqual('70');
expect(format(plainTime, 'YYYY')).toEqual('1970');

expect(format(plainTime, 'M')).toEqual('1');
expect(format(plainTime, 'MM')).toEqual('01');
expect(format(plainTime, 'MMM')).toEqual('Jan');
expect(format(plainTime, 'MMM', 'nl')).toEqual('jan');
expect(format(plainTime, 'MMMM')).toEqual('January');
expect(format(plainTime, 'MMMM', 'nl')).toEqual('januari');

expect(format(plainTime, 'D')).toEqual('1');
expect(format(plainTime, 'DD')).toEqual('01');
expect(format(plainTime, 'd')).toEqual('4');
expect(format(plainTime, 'dd')).toEqual('T');
expect(format(plainTime, 'dd', 'nl')).toEqual('D');
expect(format(plainTime, 'ddd')).toEqual('Thu');
expect(format(plainTime, 'ddd', 'nl')).toEqual('do');
expect(format(plainTime, 'dddd')).toEqual('Thursday');
expect(format(plainTime, 'dddd', 'nl')).toEqual('donderdag');

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, 'm')).toEqual('37');
expect(format(plainTime, 'mm')).toEqual('37');

expect(format(plainTime, 's')).toEqual('0');
expect(format(plainTime, 'ss')).toEqual('00');

expect(format(plainTime, 'SSS')).toEqual('000');
expect(format(plainTime, 'SSSS')).toEqual('000');

expect(format(plainTime, 'Z')).toEqual('+00:00');
expect(format(plainTime, 'ZZ')).toEqual('+0000');

expect(format(plainTime, 'A')).toEqual('PM');
expect(format(plainTime, 'a')).toEqual('pm');
});

it('is able to format ZonedDateTime', () => {
const zonedDateTime =
Temporal.Instant.from('2024-06-05T12:30:00Z').toZonedDateTimeISO('Europe/Amsterdam');

expect(format(zonedDateTime, 'YY')).toEqual('24');
expect(format(zonedDateTime, 'YYYY')).toEqual('2024');

expect(format(zonedDateTime, 'M')).toEqual('6');
expect(format(zonedDateTime, 'MM')).toEqual('06');
expect(format(zonedDateTime, 'MMM')).toEqual('Jun');
expect(format(zonedDateTime, 'MMM', 'nl')).toEqual('jun');
expect(format(zonedDateTime, 'MMMM')).toEqual('June');
expect(format(zonedDateTime, 'MMMM', 'nl')).toEqual('juni');

expect(format(zonedDateTime, 'D')).toEqual('5');
expect(format(zonedDateTime, 'DD')).toEqual('05');
expect(format(zonedDateTime, 'd')).toEqual('3');
expect(format(zonedDateTime, 'dd')).toEqual('W');
expect(format(zonedDateTime, 'dd', 'nl')).toEqual('W');
expect(format(zonedDateTime, 'ddd')).toEqual('Wed');
expect(format(zonedDateTime, 'ddd', 'nl')).toEqual('wo');
expect(format(zonedDateTime, 'dddd')).toEqual('Wednesday');
expect(format(zonedDateTime, 'dddd', 'nl')).toEqual('woensdag');

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, 'm')).toEqual('30');
expect(format(zonedDateTime, 'mm')).toEqual('30');

expect(format(zonedDateTime, 's')).toEqual('0');
expect(format(zonedDateTime, 'ss')).toEqual('00');

expect(format(zonedDateTime, 'SSS')).toEqual('000');
expect(format(zonedDateTime, 'SSSS')).toEqual('000');

expect(format(zonedDateTime, 'Z')).toEqual('+02:00');
expect(format(zonedDateTime, 'ZZ')).toEqual('+0200');

expect(format(zonedDateTime, 'A')).toEqual('PM');
expect(format(zonedDateTime, 'a')).toEqual('pm');
});
});
122 changes: 122 additions & 0 deletions app/lib/Temporal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2024 Peter Beverloo & AnimeCon. All rights reserved.
// Use of this source code is governed by a MIT license that can be found in the LICENSE file.

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.
*
* @see https://github.com/iamkun/dayjs/blob/dev/src/constant.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;

/**
* Formats the given `dateTime` to a string according to the indicated `format`. The implementation
* of this formatter follows the basic formatting rules of the DayJS library.
*
* `Instant`, `PlainDate`, `PlainDateTime` and `PlainTime` will be interpret as if they were in UTC,
* whereas `ZonedDateTime` will maintain its associated timezone.
*
* @see https://day.js.org/docs/en/display/format
*/
export function format(dateTime: Temporal.Instant, format: string, locale?: string): string;
export function format(
dateTime: Temporal.PlainDate | Temporal.PlainDateTime | Temporal.PlainTime,
format: string, locale?: string): string;
export function format(dateTime: Temporal.ZonedDateTime, format: string, locale?: string): string;
export function format(dateTime: any, format: string, locale?: string): string {
let zonedDateTime: Temporal.ZonedDateTime;

if (dateTime instanceof Temporal.Instant) {
zonedDateTime = dateTime.toZonedDateTimeISO('UTC');
} else if (dateTime instanceof Temporal.PlainDate) {
zonedDateTime = dateTime.toZonedDateTime({ timeZone: 'UTC' });
} else if (dateTime instanceof Temporal.PlainDateTime) {
zonedDateTime = dateTime.toZonedDateTime('UTC', { disambiguation: 'earlier' });
} else if (dateTime instanceof Temporal.PlainTime) {
zonedDateTime = dateTime.toZonedDateTime({
plainDate: Temporal.PlainDate.from('1970-01-01'),
timeZone: 'UTC',
});
} else if (dateTime instanceof Temporal.ZonedDateTime) {
zonedDateTime = dateTime;
} else {
throw new Error(`Invalid value passed for DateTime (t=${typeof dateTime}, v=${dateTime})`);
}

const effectiveLocale = locale ?? 'en-GB';
const fields = zonedDateTime.getISOFields();

const matches = (match: string) => {
switch (match) {
case 'YY':
return `${fields.isoYear}`.substr(2);
case 'YYYY':
return fields.isoYear;

case 'M':
return `${fields.isoMonth}`;
case 'MM':
return `0${fields.isoMonth}`.substr(-2);
case 'MMM':
return zonedDateTime.toLocaleString(effectiveLocale, { month: 'short' });
case 'MMMM':
return zonedDateTime.toLocaleString(effectiveLocale, { month: 'long' });

case 'D':
return `${fields.isoDay}`;
case 'DD':
return `0${fields.isoDay}`.substr(-2);
case 'd':
return `${zonedDateTime.dayOfWeek - 1}`;
case 'dd':
return zonedDateTime.toLocaleString(effectiveLocale, { weekday: 'narrow' });
case 'ddd':
return zonedDateTime.toLocaleString(effectiveLocale, { weekday: 'short' });
case 'dddd':
return zonedDateTime.toLocaleString(effectiveLocale, { weekday: 'long' });

case 'H':
return `${fields.isoHour}`;
case 'HH':
return `0${fields.isoHour}`.substr(-2);
case 'h':
return `${fields.isoHour % 12}`;
case 'hh':
return `0${fields.isoHour % 12}`.substr(-2);

case 'm':
return `${fields.isoMinute}`;
case 'mm':
return `0${fields.isoMinute}`.substr(-2);

case 's':
return `${fields.isoSecond}`;
case 'ss':
return `0${fields.isoSecond}`.substr(-2);

case 'SSS':
return `00${fields.isoMillisecond}`.substr(-3);
case 'SSSS':
return `00${fields.isoMicrosecond}`.substr(-3);

case 'Z':
return fields.offset;
case 'ZZ':
return fields.offset.replace(':', '');

case 'A':
return fields.isoHour < 12 ? 'AM' : 'PM';
case 'a':
return fields.isoHour < 12 ? 'am' : 'pm';

default:
throw new Error(`Invalid formatting parameter received (f=${format}, v=${match})`);
}
};

return format.replace(kFormatRegexp, (match, $1) => $1 || matches(match));
}
Loading

0 comments on commit 028bcfb

Please sign in to comment.