This repository has no description
Something went wrong. Try again.
3.9 kB · 128 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129// SPDX-License-Identifier: AGPL-3.0-or-later
import {getTimeZones, timeZonesNames} from '@vvo/tzdb';
const UTC_TIME_ZONE_ID = 'UTC';
interface TimeZoneRecord { readonly name: string; readonly group: ReadonlyArray<string>; readonly alternativeName?: string; readonly countryName?: string; readonly countryCode?: string; readonly mainCities: ReadonlyArray<string>; readonly currentTimeOffsetInMinutes: number;}
interface TimeZoneDisplayOption { readonly value: string; readonly label: string; readonly searchText: string; readonly offsetMinutes: number;}
const supportedTimeZoneIds = new Set<string>([...timeZonesNames, UTC_TIME_ZONE_ID]);
function cleanTimeZoneId(timeZone: string | null | undefined): string | null { const value = timeZone?.trim(); return value ? value : null;}
function getDisplayTimeZones(): Array<TimeZoneRecord> { return getTimeZones({includeUtc: true});}
function findTimeZone(timeZone: string | null | undefined): TimeZoneRecord | null { const value = cleanTimeZoneId(timeZone); if (!value) { return null; } for (const record of getDisplayTimeZones()) { if (record.name === value || record.group.includes(value)) { return record; } } return null;}
function getRuntimeOffsetMinutes(timeZone: string): number | null { const now = new Date(); try { const parts = new Intl.DateTimeFormat('en-US', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hourCycle: 'h23', }).formatToParts(now); const values = new Map(parts.map((part) => [part.type, part.value])); const year = Number(values.get('year')); const month = Number(values.get('month')); const day = Number(values.get('day')); const hour = Number(values.get('hour')); const minute = Number(values.get('minute')); const second = Number(values.get('second')); if ([year, month, day, hour, minute, second].some((value) => Number.isNaN(value))) { return null; } const zonedAsUtc = Date.UTC(year, month - 1, day, hour, minute, second); return Math.round((zonedAsUtc - now.getTime()) / 60_000); } catch { return null; }}
export function isSupportedTimeZoneId(timeZone: string | null | undefined): boolean { const value = cleanTimeZoneId(timeZone); return value != null && supportedTimeZoneIds.has(value);}
export function getCurrentTimeZoneOffsetMinutes(timeZone: string | null | undefined): number | null { const record = findTimeZone(timeZone); if (!record) { return null; } return getRuntimeOffsetMinutes(record.name) ?? record.currentTimeOffsetInMinutes;}
function formatUtcOffset(offsetMinutes: number): string { const sign = offsetMinutes >= 0 ? '+' : '-'; const absolute = Math.abs(offsetMinutes); const hours = Math.floor(absolute / 60) .toString() .padStart(2, '0'); const minutes = (absolute % 60).toString().padStart(2, '0'); return `UTC${sign}${hours}:${minutes}`;}
function formatTimeZonePlace(record: TimeZoneRecord): string { if (record.name === UTC_TIME_ZONE_ID) { return UTC_TIME_ZONE_ID; } const name = record.alternativeName || record.name.replace(/_/g, ' '); const cities = record.mainCities.slice(0, 2).join(', '); return cities ? `${name} - ${cities}` : name;}
export function getTimeZoneDisplayOptions(): Array<TimeZoneDisplayOption> { return getDisplayTimeZones() .map((record) => ({ value: record.name, label: `${formatUtcOffset(record.currentTimeOffsetInMinutes)} ${formatTimeZonePlace(record)}`, searchText: [ record.name, ...record.group, record.alternativeName, record.countryName, record.countryCode, ...record.mainCities, ] .filter(Boolean) .join(' '), offsetMinutes: record.currentTimeOffsetInMinutes, })) .sort((left, right) => left.offsetMinutes - right.offsetMinutes || left.label.localeCompare(right.label));}