Skip to content

Instantly share code, notes, and snippets.

@JeremyRH
Last active October 7, 2024 03:11
Show Gist options
  • Save JeremyRH/3fc818dc78caa8df7c7c45f43b19a9cf to your computer and use it in GitHub Desktop.
Save JeremyRH/3fc818dc78caa8df7c7c45f43b19a9cf to your computer and use it in GitHub Desktop.
date-utils.ts
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