Created
September 1, 2021 06:08
-
-
Save justingrant/f86a84c44982db36794f8bb484d135f2 to your computer and use it in GitHub Desktop.
Temporal multi-type string parse proof-of-concept
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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