Skip to content

Commit 4da642b

Browse files
Align hour buckets to the site time zone (#7)
Fixes #6
1 parent 748f616 commit 4da642b

2 files changed

Lines changed: 31 additions & 2 deletions

File tree

‎packages/core/src/time.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,17 @@ export function startOfDay(ts: number, timeZone = 'UTC'): number {
6666
export function bucketStart(ts: number, granularity: Granularity, timeZone = 'UTC'): number {
6767
switch (granularity) {
6868
case 'minute':
69+
// Every zone offset is a whole number of minutes, so UTC and local
70+
// minute boundaries always coincide.
6971
return Math.floor(ts / MINUTE) * MINUTE;
70-
case 'hour':
71-
return Math.floor(ts / HOUR) * HOUR;
72+
case 'hour': {
73+
// Align to the local hour, like startOfDay does for the day: half-offset
74+
// zones (e.g. Asia/Kolkata) start their hours at :30 past the UTC hour.
75+
const offset = tzOffsetMinutes(ts, timeZone) * MINUTE;
76+
const localHour = Math.floor((ts + offset) / HOUR) * HOUR;
77+
const offsetAtHour = tzOffsetMinutes(localHour - offset, timeZone) * MINUTE;
78+
return localHour - offsetAtHour;
79+
}
7280
case 'day':
7381
return startOfDay(ts, timeZone);
7482
}

‎packages/core/test/time.test.ts‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,27 @@ describe('time buckets', () => {
2020
expect(bucketStart(ts, 'hour')).toBe(at('2026-05-10T13:00:00Z'));
2121
});
2222

23+
it('aligns hour buckets to a half-offset zone', () => {
24+
// 13:47 UTC is 19:17 in Kolkata (+5:30); the local hour starts at 19:00 IST = 13:30 UTC.
25+
const ts = at('2026-05-10T13:47:31.500Z');
26+
expect(bucketStart(ts, 'hour', 'Asia/Kolkata')).toBe(at('2026-05-10T13:30:00Z'));
27+
// Minute buckets are unaffected: every zone offset is a whole number of minutes.
28+
expect(bucketStart(ts, 'minute', 'Asia/Kolkata')).toBe(at('2026-05-10T13:47:00Z'));
29+
expect(bucketsBetween(ts, ts + 2 * HOUR, 'hour', 'Asia/Kolkata')).toEqual([
30+
at('2026-05-10T13:30:00Z'),
31+
at('2026-05-10T14:30:00Z'),
32+
at('2026-05-10T15:30:00Z'),
33+
]);
34+
});
35+
36+
it('keeps hour buckets an hour apart across a whole-hour DST change', () => {
37+
const tz = 'America/New_York';
38+
// The fall-back hour (EDT -> EST) still steps by one real hour per local hour.
39+
const b = bucketStart(at('2026-11-01T05:30:00Z'), 'hour', tz);
40+
expect(b).toBe(at('2026-11-01T05:00:00Z'));
41+
expect(nextBucket(b, 'hour', tz) - b).toBe(HOUR);
42+
});
43+
2344
it('computes local midnight for fixed-offset zones', () => {
2445
expect(tzOffsetMinutes(at('2026-05-10T12:00:00Z'), 'Europe/Moscow')).toBe(180);
2546
expect(startOfDay(at('2026-05-10T22:30:00Z'), 'Europe/Moscow')).toBe(

0 commit comments

Comments
 (0)