Skip to content

Instantly share code, notes, and snippets.

@justingrant
Created September 1, 2021 06:08
Show Gist options
  • Save justingrant/f86a84c44982db36794f8bb484d135f2 to your computer and use it in GitHub Desktop.
Save justingrant/f86a84c44982db36794f8bb484d135f2 to your computer and use it in GitHub Desktop.
Temporal multi-type string parse proof-of-concept
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/*
type Temporal = typeof Temporal;
const types = [
'ZonedDateTime',
'Instant',
'PlainDateTime',
'PlainTime',
'PlainDate',
'PlainYearMonth',
'PlainMonthDay',
];
const units = [
'year',
'eraYear',
'era',
'month',
'monthCode',
'week',
'day',
'hour',
'minute',
'second',
'millisecond',
'microsecond',
'nanosecond',
];
*/
/**
* Parse an ISO string into Temporal instances and their components: date units,
* time units, offset, calendar, and time zone
*
* Issues found:
* * #1765 - PlainTime will parse anything, e.g. 01-01 or 2020-01-01 or 2020-01
* * #1766 - Can't distinguish default ISO calendar from user appending [u-ca=iso8601]
* * #1767 - Can't distinguish Z from +00:00 when parsing strings with TZ annotations
* * #1769 - No way to know if minutes and smaller units were present in a time string
*
* @param s - ISO string to parse
* @returns An object with:
* * a property for each Temporal type parsed successfully, e.g. `zonedDateTime`,
* `plainDate`, `plainTime`
* * the following properties from those successfully-parsed instances:
* 'year`, `eraYear`, `era`, `month`, `monthCode`, `week`, `day`, `hour`,
* `minute`, `second`, `millisecond`, `microsecond`, `nanosecond'
* * a `calendar` property if a bracketed calendar annotation was in the string
* * a `timeZone` property if a bracketed time zone annotation was in the string
* * an `offset` property ("Z" or numeric offset string) if an offset was in the string
*/
function parse(s) {
const types = [
'ZonedDateTime',
'Instant',
'PlainDateTime',
'PlainTime',
'PlainDate',
'PlainYearMonth',
'PlainMonthDay',
];
const units = [
'year',
'eraYear',
'era',
'month',
'monthCode',
'week',
'day',
'hour',
'minute',
'second',
'millisecond',
'microsecond',
'nanosecond',
];
const instances = {};
for (const t of types) {
try {
const lowerName = t[0].toLowerCase() + t.slice(1);
instances[lowerName] = Temporal[t].from(s);
} catch (e) {}
}
// #1765 - strings like 2020-01-01 only have date components, but PlainTime will parse them.
if (instances.plainTime && Object.keys(instances).length > 1 && !/\d[ Tt]\d\d/.test(s)) {
delete instances.plainTime;
}
const timeZone = instances.zonedDateTime?.timeZone;
// #1767 - Can't differentiate Z from +00:00 when parsing string with TZ annotations.
let offsetTimeZone;
if (instances.zonedDateTime) {
try {
// May throw if no offset is in the string, e.g. 2020-01-01T20:00[America/Los_Angeles]
offsetTimeZone = Temporal.TimeZone.from(s.split('[')[0]);
} catch {}
} else if (instances.instant) {
offsetTimeZone = Temporal.TimeZone.from(s);
if (offsetTimeZone.id === 'UTC') {
// Reading the individual units like month or year from an Z timestamp is
// usually a programmer bug so they aren't included here. If you really do
// want to do this, project the instant into a ZDT using the UTC timezone.
// e.g. mostComponentsMatched.toZonedDateTimeISO(offsetTimeZone);
// See #1751 for discussion.
delete instances.plainDateTime;
delete instances.plainDate;
delete instances.plainMonthDay;
delete instances.plainYearMonth;
delete instances.plainTime;
}
}
let mostComponentsMatched = Object.entries(instances)?.[0]?.[1] || {};
// 2020-01-01 can be parsed by PlainDateTime and ZonedDateTime but has no time components
if ((instances.plainDateTime || instances.zonedDateTime) && !instances.plainTime) {
mostComponentsMatched = instances.plainDate;
}
// If instant is the top match, but it contained a local time (not a Z string)
// then use PlainDateTime for its units.
if (instances.instant && instances.plainDateTime) mostComponentsMatched = instances.plainDateTime;
let calendar = mostComponentsMatched.calendar;
// #1766 - can't distinguish the default ISO calendar from the user appending [u-ca=iso8601]
if (calendar?.id === 'iso8601' && !s.toLowerCase().endsWith('[u-ca=iso8601]')) calendar = undefined;
// #1769 - No way to know if minutes and smaller units were present in a time string.
// TODO: work around this with regexes. Look at regex.mjs in the polyfill for a start.
const components = {};
for (const unit of units) {
if (mostComponentsMatched[unit] !== undefined) components[unit] = mostComponentsMatched[unit];
}
const result = { ...instances, ...components };
if (timeZone) result.timeZone = timeZone;
if (offsetTimeZone) result.offset = offsetTimeZone.id === 'UTC' ? 'Z' : offsetTimeZone.id;
if (calendar) result.calendar = calendar;
return result;
}
[
'2020-01-01T12:00-08:00[America/Los_Angeles][u-ca=gregory]',
// Waiting for #1749 to be merged '2020-01-01T20:00Z[America/Los_Angeles]',
'2020-01-01T20:00[America/Los_Angeles]',
'2020-01-01[America/Los_Angeles]',
'2020-01-01T12:00-08:00',
'20200101T1200-0800',
'2020-01-01T20:00Z',
'2020-01-01T12:00',
'2020-01-01T12:00:00',
'2020-01-01T12:00:00.123',
'12:00',
'1200',
'12:00:00',
'120000',
'12:00:00.123',
'2020-01-01',
'2020-01',
'01-01',
'bogus',
].reduce((o, s) => {
o[s] = parse(s);
return o;
}, {});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment