public
imalexnord
read
Slot
Calendar-first scheduling powered by Cloudflare Workers and Google Calendar.
Languages
Repository composition by tracked source files.
TypeScript
79%
CSS
19%
SQL
2%
HTML
0%
Create file
Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/slot.git
ssh://[email protected]:2222/user/imalexnord/slot.git
Trace
worker/time.ts
Trace helps you understand code history line by line. See who changed each line, when it changed, and which commit introduced it.
Author
Date
Commit
Line
Code
1
import type { BusyRange } from './types';
3
const formatterCache = new Map<string, Intl.DateTimeFormat>();
5
function formatter(timeZone: string): Intl.DateTimeFormat {
6
const cached = formatterCache.get(timeZone);
7
if (cached) return cached;
8
const next = new Intl.DateTimeFormat('en-CA', {
9
timeZone,
10
year: 'numeric',
11
month: '2-digit',
12
day: '2-digit',
13
hour: '2-digit',
14
minute: '2-digit',
15
second: '2-digit',
16
hourCycle: 'h23',
17
weekday: 'short',
18
});
19
formatterCache.set(timeZone, next);
20
return next;
21
}
23
export function partsInZone(date: Date, timeZone: string): Record<string, string> {
24
return Object.fromEntries(
25
formatter(timeZone)
26
.formatToParts(date)
27
.filter((part) => part.type !== 'literal')
28
.map((part) => [part.type, part.value]),
29
);
30
}
32
export function dateKeyInZone(date: Date, timeZone: string): string {
33
const parts = partsInZone(date, timeZone);
34
return `${parts.year}-${parts.month}-${parts.day}`;
35
}
38
export function addDaysToDateKey(dateKey: string, days: number): string {
39
const [year, month, day] = dateKey.split('-').map(Number);
40
return new Date(Date.UTC(year, month - 1, day + days, 12, 0, 0)).toISOString().slice(0, 10);
41
}
43
export function weekdayForDate(dateKey: string, timeZone: string): number {
44
const noon = zonedDateTimeToUtc(dateKey, '12:00', timeZone);
45
const label = partsInZone(noon, timeZone).weekday;
46
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(label);
47
}
49
export function zonedDateTimeToUtc(dateKey: string, time: string, timeZone: string): Date {
50
const [year, month, day] = dateKey.split('-').map(Number);
51
const [hour, minute] = time.split(':').map(Number);
52
let guess = Date.UTC(year, month - 1, day, hour, minute, 0);
54
// Iterate to account for the zone offset, including DST transitions.
55
for (let i = 0; i < 3; i += 1) {
56
const parts = partsInZone(new Date(guess), timeZone);
57
const representedAsUtc = Date.UTC(
58
Number(parts.year),
59
Number(parts.month) - 1,
60
Number(parts.day),
61
Number(parts.hour),
62
Number(parts.minute),
63
Number(parts.second),
64
);
65
const targetAsUtc = Date.UTC(year, month - 1, day, hour, minute, 0);
66
const delta = targetAsUtc - representedAsUtc;
67
if (delta === 0) break;
68
guess += delta;
69
}
71
return new Date(guess);
72
}
74
export function minutesToTime(total: number): string {
75
const hour = Math.floor(total / 60) % 24;
76
const minute = total % 60;
77
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
78
}
80
export function timeToMinutes(value: string): number {
81
const [hour, minute] = value.split(':').map(Number);
82
return hour * 60 + minute;
83
}
85
export function overlaps(start: number, end: number, busy: BusyRange[]): boolean {
86
return busy.some((range) => start < range.end && end > range.start);
87
}
89
export function addMinutes(timestamp: number, minutes: number): number {
90
return timestamp + minutes * 60_000;
91
}
93
export function slotKeys(start: number, end: number, incrementMinutes = 15): string[] {
94
const increment = incrementMinutes * 60_000;
95
const first = Math.floor(start / increment) * increment;
96
const last = Math.ceil(end / increment) * increment;
97
const keys: string[] = [];
98
for (let cursor = first; cursor < last; cursor += increment) {
99
keys.push(new Date(cursor).toISOString());
100
}
101
return keys;
102
}