-
Notifications
You must be signed in to change notification settings - Fork 958
Expand file tree
/
Copy pathtime.ts
More file actions
89 lines (79 loc) · 2.9 KB
/
Copy pathtime.ts
File metadata and controls
89 lines (79 loc) · 2.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
/**
* Centralized time helpers. The whole codebase reads `now()` rather than
* touching `Date.now()` directly, so tests can monkey-patch the clock in one
* place if needed.
*/
export type EpochMs = number;
/** Default wall-clock source. Swap with `setNow` for deterministic tests. */
let _now: () => EpochMs = () => Date.now();
export function now(): EpochMs {
return _now();
}
/**
* Override the clock source. Returns a restore function. Intended for tests:
*
* ```ts
* const restore = setNow(() => 1_700_000_000_000);
* try { ... } finally { restore(); }
* ```
*/
export function setNow(fn: () => EpochMs): () => void {
const previous = _now;
_now = fn;
return () => { _now = previous; };
}
/** Monotonic high-resolution clock (ms, fractional). Independent of `now()`. */
export function hrNowMs(): number {
return Number(process.hrtime.bigint()) / 1_000_000;
}
/** Format a millisecond duration into a short human string ("12ms" / "1.2s" / "3m4s"). */
export function formatDurationMs(ms: number): string {
if (!Number.isFinite(ms)) return "?";
const abs = Math.abs(ms);
if (abs < 1) return ms.toFixed(2) + "ms";
if (abs < 1000) return Math.round(ms) + "ms";
if (abs < 60_000) return (ms / 1000).toFixed(ms < 10_000 ? 2 : 1) + "s";
const minutes = Math.floor(ms / 60_000);
const seconds = Math.round((ms % 60_000) / 1000);
return seconds === 0 ? `${minutes}m` : `${minutes}m${seconds}s`;
}
export interface IsoFromEpochOptions {
/** Include numeric UTC offset for localized timestamps. UTC fast path stays unchanged. */
offset?: boolean;
}
/** Convenience: ISO 8601 string for a given epoch ms. Defaults to UTC. */
export function isoFromEpochMs(ms: EpochMs, tz?: string, opts: IsoFromEpochOptions = {}): string {
if (!tz || tz === "UTC") return new Date(ms).toISOString();
const d = new Date(ms);
const parts = dateTimeParts(d, tz);
const base = `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}.${parts.fractionalSecond ?? "000"}`;
return opts.offset ? base + offsetForTimeZone(d, tz) : base;
}
function dateTimeParts(d: Date, tz: string): Record<string, string> {
const fmt = new Intl.DateTimeFormat("en-CA", {
timeZone: tz,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3,
hour12: false,
});
const out: Record<string, string> = {};
for (const part of fmt.formatToParts(d)) {
if (part.type !== "literal") out[part.type] = part.value;
}
return out;
}
function offsetForTimeZone(d: Date, tz: string): string {
const fmt = new Intl.DateTimeFormat("en-US", {
timeZone: tz,
timeZoneName: "longOffset",
hour: "2-digit",
});
const zone = fmt.formatToParts(d).find((part) => part.type === "timeZoneName")?.value;
if (!zone || zone === "GMT" || zone === "UTC") return "+00:00";
return zone.replace(/^GMT/, "");
}