Skip to content
74 linesCodeBlameRaw
1/**
2 * Time zones on a profile: the IANA names a person picks from in their
3 * settings, and the local time the card over their name shows. Pure, so it
4 * is tested on its own.
5 */
6
7/** Whether the runtime knows `zone` as a time zone. */
8export function knownTimeZone(zone: string | null | undefined): zone is string {
9 if (!zone) return false;
10 try {
11 new Intl.DateTimeFormat("en-US", { timeZone: zone });
12 return true;
13 } catch {
14 return false;
15 }
16}
17
18/**
19 * Every IANA zone the runtime knows, sorted, with `current` kept in the
20 * list even when the runtime does not know it, so a saved choice is never
21 * dropped. UTC is always there.
22 */
23export function timeZoneNames(current?: string | null): string[] {
24 let names: string[] = [];
25 try {
26 names = Intl.supportedValuesOf("timeZone");
27 } catch {
28 names = [];
29 }
30 const all = new Set(names);
31 all.add("UTC");
32 if (current) all.add(current);
33 return [...all].sort((a, b) => a.localeCompare(b));
34}
35
36/** A zone's name as a person reads it: `America/Port_of_Spain` as `America/Port of Spain`. */
37export function timeZoneLabel(zone: string): string {
38 return zone.replaceAll("_", " ");
39}
40
41/** The zone's offset from UTC at `now`, such as `UTC−06:00`; null when unknown. */
42export function utcOffset(zone: string, now: number): string | null {
43 try {
44 const part = new Intl.DateTimeFormat("en-US", { timeZone: zone, timeZoneName: "longOffset" })
45 .formatToParts(now)
46 .find((one) => one.type === "timeZoneName")?.value;
47 if (!part) return null;
48 // "GMT-06:00", or plain "GMT" at UTC itself.
49 const offset = part.replace(/^GMT/, "") || "+00:00";
50 return `UTC${offset.replace("-", "−")}`;
51 } catch {
52 return null;
53 }
54}
55
56/**
57 * The time of day at `now` in `zone`, such as `3:42 PM`, in `locale` (the
58 * viewer's own when left out); null when there is no zone or the runtime
59 * does not know it.
60 */
61export function localTime(zone: string | null | undefined, now: number, locale?: string): string | null {
62 if (!knownTimeZone(zone)) return null;
63 return new Intl.DateTimeFormat(locale, { hour: "numeric", minute: "2-digit", timeZone: zone }).format(now);
64}
65
66/** The browser's own zone, or null where it cannot say (on the server, say). */
67export function browserTimeZone(): string | null {
68 try {
69 const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
70 return knownTimeZone(zone) ? zone : null;
71 } catch {
72 return null;
73 }
74}