Last active
October 7, 2024 03:11
-
-
Save JeremyRH/3fc818dc78caa8df7c7c45f43b19a9cf to your computer and use it in GitHub Desktop.
date-utils.ts
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
const datePartMapping = [ | |
["year", "UTCFullYear"], | |
["month", "UTCMonth"], | |
["day", "UTCDate"], | |
["hour", "UTCHours"], | |
["minute", "UTCMinutes"], | |
["second", "UTCSeconds"], | |
] as const; | |
const dateValuesKeys = datePartMapping.map((m) => m[0]); | |
type DateValuesKeys = typeof dateValuesKeys extends Array<infer K> ? K : never; | |
const defaultDateValues = Object.fromEntries( | |
datePartMapping.map((m) => [m[0], 0]) | |
) as Record<DateValuesKeys, number>; | |
type DateValues = typeof defaultDateValues; | |
function getInt(n: string) { | |
return parseInt(n, 10); | |
} | |
function getUTCDate(values: DateValues) { | |
const date = new Date( | |
Date.UTC( | |
values.year, | |
// Date.UTC accepts 0 as the first month. | |
values.month - 1, | |
values.day, | |
values.hour, | |
values.minute, | |
values.second | |
) | |
); | |
// Years 0-99 are Date.UTC shorthand for 19xx. date.setUTCFullYear does not do this. | |
date.setUTCFullYear(values.year); | |
return date; | |
} | |
let cachedTimeZone: string; | |
let cachedFormatter: Intl.DateTimeFormat; | |
/** | |
* Gets date parts at the given time zone. | |
* | |
* @param date | |
* @param timeZone IANA time zone ID. | |
* @return \{ year: 2024, month: 12, ...etc. }. | |
*/ | |
export function getDateValuesAtZone(date: Date, timeZone: string) { | |
const result = { ...defaultDateValues }; | |
// en-GB is good enough for getting numeric values for date parts. | |
const formatter = | |
timeZone === cachedTimeZone | |
? cachedFormatter | |
: new Intl.DateTimeFormat("en-GB", { | |
day: "2-digit", | |
era: "short", | |
hour: "2-digit", | |
hour12: false, | |
minute: "2-digit", | |
month: "2-digit", | |
second: "2-digit", | |
timeZone, | |
year: "numeric", | |
}); | |
let era = ""; | |
cachedTimeZone = timeZone; | |
cachedFormatter = formatter; | |
for (const part of formatter.formatToParts(date)) { | |
if (part.type in result) { | |
(result as any)[part.type] = parseInt(part.value); | |
} else if (part.type === "era") { | |
era = part.value; | |
} | |
} | |
result.year = era === "BC" ? -(result.year - 1) : result.year; | |
return result; | |
} | |
function getDiffDateValues(target: DateValues, current: DateValues) { | |
const result = { ...defaultDateValues, isDiff: false }; | |
for (const key of dateValuesKeys) { | |
const diff = current[key] - target[key]; | |
if (diff !== 0) { | |
result.isDiff = true; | |
} | |
result[key] = diff; | |
} | |
return result; | |
} | |
function adjustDate( | |
date: Date, | |
diffValues: ReturnType<typeof getDiffDateValues> | |
) { | |
for (const [type, utcMethodName] of datePartMapping) { | |
const setMethod = `set${utcMethodName}` as const; | |
const getMethod = `get${utcMethodName}` as const; | |
date[setMethod](date[getMethod]() - diffValues[type]); | |
} | |
return date; | |
} | |
/** | |
* Gets a Date set to the observed date & time at the location of the time zone. | |
* | |
* @param timeZone IANA time zone ID. | |
* @param date 'YYYY-MM-DD' | |
* @param time 'hh:mm:ss' | |
*/ | |
export function getDateAtZone(timeZone: string, date: string, time = "0") { | |
const isNegativeYear = date.startsWith("-"); | |
const [inputYear, inputMonth, inputDay] = ( | |
isNegativeYear ? date.slice(1) : date | |
) | |
.split("-") | |
.map(getInt); | |
const [inputHour, inputMinute = 0, inputSecond = 0] = time | |
.split(":") | |
.map(getInt); | |
const inputValues = { | |
year: isNegativeYear ? -inputYear : inputYear, | |
month: inputMonth, | |
day: inputDay, | |
hour: inputHour, | |
minute: inputMinute, | |
second: inputSecond, | |
}; | |
let resultDate = getUTCDate(inputValues); | |
const diffFromUTCOffset = getDiffDateValues( | |
inputValues, | |
getDateValuesAtZone(resultDate, timeZone) | |
); | |
if (!diffFromUTCOffset.isDiff) { | |
return resultDate; | |
} | |
resultDate = adjustDate(resultDate, diffFromUTCOffset); | |
const diffFromTimeChange = getDiffDateValues( | |
inputValues, | |
getDateValuesAtZone(resultDate, timeZone) | |
); | |
if (!diffFromTimeChange.isDiff) { | |
return resultDate; | |
} | |
return adjustDate(resultDate, diffFromTimeChange); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment