summarum/src/engine/datetime.ts
Horizontal42 d4c34bf4af extract baseUnitId in datetime.ts
addToDate and isCalendarUnit both inlined the same prefix-stripping
expression.
2026-08-12 20:07:31 +03:00

144 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Date helpers and a city → IANA timezone map for "time in Tokyo".
import { DateOrder } from "./types";
/** Does the OS locale write the day or the month first? (31.12 vs 12/31) */
export function detectDateOrder(locale?: string): DateOrder {
const parts = new Intl.DateTimeFormat(locale, { day: "numeric", month: "numeric", year: "numeric" })
.formatToParts(new Date(2000, 11, 31));
for (const p of parts) {
if (p.type === "day") return "dmy";
if (p.type === "month") return "mdy";
}
return "dmy";
}
const Z: Record<string, string> = {
// ---- common abbreviations
utc: "UTC", gmt: "UTC", est: "America/New_York", edt: "America/New_York",
cst: "America/Chicago", cdt: "America/Chicago", mst: "America/Denver",
pst: "America/Los_Angeles", pdt: "America/Los_Angeles", msk: "Europe/Moscow",
cet: "Europe/Paris", eet: "Europe/Kyiv", jst: "Asia/Tokyo", ist: "Asia/Kolkata",
// ---- cities (en)
"new york": "America/New_York", "los angeles": "America/Los_Angeles",
"san francisco": "America/Los_Angeles", chicago: "America/Chicago",
seattle: "America/Los_Angeles", boston: "America/New_York", miami: "America/New_York",
toronto: "America/Toronto", vancouver: "America/Vancouver",
"mexico city": "America/Mexico_City", "sao paulo": "America/Sao_Paulo",
"buenos aires": "America/Argentina/Buenos_Aires", lima: "America/Lima",
london: "Europe/London", dublin: "Europe/Dublin", lisbon: "Europe/Lisbon",
madrid: "Europe/Madrid", barcelona: "Europe/Madrid", paris: "Europe/Paris",
berlin: "Europe/Berlin", munich: "Europe/Berlin", amsterdam: "Europe/Amsterdam",
brussels: "Europe/Brussels", zurich: "Europe/Zurich", geneva: "Europe/Zurich",
rome: "Europe/Rome", milan: "Europe/Rome", vienna: "Europe/Vienna",
prague: "Europe/Prague", warsaw: "Europe/Warsaw", stockholm: "Europe/Stockholm",
oslo: "Europe/Oslo", copenhagen: "Europe/Copenhagen", helsinki: "Europe/Helsinki",
athens: "Europe/Athens", istanbul: "Europe/Istanbul", kyiv: "Europe/Kyiv",
kiev: "Europe/Kyiv", minsk: "Europe/Minsk", moscow: "Europe/Moscow",
"saint petersburg": "Europe/Moscow", riga: "Europe/Riga", vilnius: "Europe/Vilnius",
tallinn: "Europe/Tallinn", tbilisi: "Asia/Tbilisi", yerevan: "Asia/Yerevan",
baku: "Asia/Baku", dubai: "Asia/Dubai", "abu dhabi": "Asia/Dubai",
"tel aviv": "Asia/Jerusalem", jerusalem: "Asia/Jerusalem", riyadh: "Asia/Riyadh",
doha: "Asia/Qatar", tehran: "Asia/Tehran", karachi: "Asia/Karachi",
delhi: "Asia/Kolkata", mumbai: "Asia/Kolkata", bangalore: "Asia/Kolkata",
kolkata: "Asia/Kolkata", dhaka: "Asia/Dhaka", bangkok: "Asia/Bangkok",
hanoi: "Asia/Ho_Chi_Minh", "ho chi minh": "Asia/Ho_Chi_Minh",
jakarta: "Asia/Jakarta", singapore: "Asia/Singapore", "kuala lumpur": "Asia/Kuala_Lumpur",
manila: "Asia/Manila", "hong kong": "Asia/Hong_Kong", taipei: "Asia/Taipei",
beijing: "Asia/Shanghai", shanghai: "Asia/Shanghai", shenzhen: "Asia/Shanghai",
seoul: "Asia/Seoul", tokyo: "Asia/Tokyo", osaka: "Asia/Tokyo",
sydney: "Australia/Sydney", melbourne: "Australia/Melbourne",
brisbane: "Australia/Brisbane", perth: "Australia/Perth", auckland: "Pacific/Auckland",
cairo: "Africa/Cairo", lagos: "Africa/Lagos", nairobi: "Africa/Nairobi",
johannesburg: "Africa/Johannesburg", "cape town": "Africa/Johannesburg",
casablanca: "Africa/Casablanca", almaty: "Asia/Almaty", astana: "Asia/Almaty",
tashkent: "Asia/Tashkent", bishkek: "Asia/Bishkek", novosibirsk: "Asia/Novosibirsk",
yekaterinburg: "Asia/Yekaterinburg", vladivostok: "Asia/Vladivostok",
// ---- cities (ru)
"нью йорк": "America/New_York", "лос анджелес": "America/Los_Angeles",
"сан франциско": "America/Los_Angeles", чикаго: "America/Chicago",
торонто: "America/Toronto", лондон: "Europe/London", париж: "Europe/Paris",
берлин: "Europe/Berlin", мадрид: "Europe/Madrid", рим: "Europe/Rome",
амстердам: "Europe/Amsterdam", вена: "Europe/Vienna", прага: "Europe/Prague",
варшава: "Europe/Warsaw", стокгольм: "Europe/Stockholm", хельсинки: "Europe/Helsinki",
афины: "Europe/Athens", стамбул: "Europe/Istanbul", киев: "Europe/Kyiv",
минск: "Europe/Minsk", москва: "Europe/Moscow", москве: "Europe/Moscow",
питер: "Europe/Moscow", "санкт петербург": "Europe/Moscow", петербург: "Europe/Moscow",
рига: "Europe/Riga", вильнюс: "Europe/Vilnius", таллин: "Europe/Tallinn",
тбилиси: "Asia/Tbilisi", ереван: "Asia/Yerevan", баку: "Asia/Baku",
дубай: "Asia/Dubai", дубае: "Asia/Dubai", "тель авив": "Asia/Jerusalem",
дели: "Asia/Kolkata", мумбаи: "Asia/Kolkata", бангкок: "Asia/Bangkok",
бангкоке: "Asia/Bangkok", сингапур: "Asia/Singapore", сингапуре: "Asia/Singapore",
джакарта: "Asia/Jakarta", гонконг: "Asia/Hong_Kong", пекин: "Asia/Shanghai",
шанхай: "Asia/Shanghai", сеул: "Asia/Seoul", токио: "Asia/Tokyo",
сидней: "Australia/Sydney", мельбурн: "Australia/Melbourne", окленд: "Pacific/Auckland",
каир: "Africa/Cairo", алматы: "Asia/Almaty", астана: "Asia/Almaty",
ташкент: "Asia/Tashkent", бишкек: "Asia/Bishkek", новосибирск: "Asia/Novosibirsk",
екатеринбург: "Asia/Yekaterinburg", владивосток: "Asia/Vladivostok",
};
/** Resolve a sequence of words to an IANA timezone. Tries longest run first. */
export function resolveZone(words: string[]): string | null {
const lower = words.map((w) => w.toLowerCase().replace(/[-.]/g, " ").trim());
if (lower[0] === "local") return Intl.DateTimeFormat().resolvedOptions().timeZone;
for (let len = Math.min(lower.length, 3); len >= 1; len--) {
const key = lower.slice(0, len).join(" ");
if (Z[key]) return Z[key];
}
// direct IANA id like Europe/Berlin typed by hand
const joined = words.join("/");
try {
new Intl.DateTimeFormat("en", { timeZone: joined });
return joined;
} catch {
return null;
}
}
export function startOfToday(): number {
const d = new Date();
d.setHours(0, 0, 0, 0);
return d.getTime();
}
/**
* Calendar-aware add. Days/weeks go through setDate so the wall-clock time
* survives DST transitions; months/years clamp to the end of the target
* month (Jan 31 + 1 month = Feb 28, not Mar 3).
*/
/** Strips a prefixed unit id ("milli:second") down to its base ("second"). */
function baseUnitId(unitId: string): string {
return unitId.includes(":") ? unitId.split(":")[1] : unitId;
}
export function addToDate(ms: number, amount: number, unitId: string): number {
const baseId = baseUnitId(unitId);
const d = new Date(ms);
if ((baseId === "day" || baseId === "week") && Number.isInteger(amount)) {
d.setDate(d.getDate() + amount * (baseId === "week" ? 7 : 1));
return d.getTime();
}
if (baseId === "month" && Number.isInteger(amount)) {
const day = d.getDate();
d.setDate(1);
d.setMonth(d.getMonth() + amount);
d.setDate(Math.min(day, lastDayOfMonth(d)));
return d.getTime();
}
if (baseId === "year" && Number.isInteger(amount)) {
const day = d.getDate();
d.setDate(1);
d.setFullYear(d.getFullYear() + amount);
d.setDate(Math.min(day, lastDayOfMonth(d)));
return d.getTime();
}
return ms; // caller falls back to ms math for everything else
}
function lastDayOfMonth(d: Date): number {
return new Date(d.getFullYear(), d.getMonth() + 1, 0).getDate();
}
export function isCalendarUnit(unitId: string, amount: number): boolean {
const baseId = baseUnitId(unitId);
return (baseId === "day" || baseId === "week" || baseId === "month" || baseId === "year") && Number.isInteger(amount);
}