Skip to content

Commit 757a936

Browse files
feat: withDateOperation HOF and startOf util
1 parent 6a75f09 commit 757a936

5 files changed

Lines changed: 439 additions & 1 deletion

File tree

packages/time/src/date/parse/tests/parse.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'
2-
import { Temporal } from '@js-temporal/polyfill'
32
import { parse } from '../parse'
43

54
const dateTimeFormat = new Intl.DateTimeFormat('en-US', {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { withDateOperation } from '../withDateOperation'
2+
import type {
3+
DateInput,
4+
DateOperationOptions,
5+
DateOperationResult,
6+
} from '../withDateOperation'
7+
8+
export type StartOfUnit =
9+
| 'year'
10+
| 'month'
11+
| 'week'
12+
| 'day'
13+
| 'hour'
14+
| 'minute'
15+
| 'second'
16+
| 'millisecond'
17+
18+
export interface StartOfArgs extends Record<string, unknown> {
19+
unit: StartOfUnit
20+
}
21+
22+
/**
23+
* startOf
24+
* Returns the start of a given unit for a date/time instance
25+
*/
26+
export function startOf(
27+
input: DateInput,
28+
args: StartOfArgs,
29+
options?: DateOperationOptions,
30+
): DateOperationResult {
31+
return withDateOperation<StartOfArgs>((zdt, { unit }) => {
32+
switch (unit) {
33+
case 'year':
34+
return zdt.with({ month: 1, day: 1, hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 })
35+
case 'month':
36+
return zdt.with({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 })
37+
case 'week': {
38+
const dayOfWeek = zdt.dayOfWeek
39+
const firstDayOfWeek = 1
40+
const daysToSubtract = (dayOfWeek - firstDayOfWeek + 7) % 7
41+
return zdt.subtract({ days: daysToSubtract })
42+
}
43+
case 'day':
44+
return zdt.with({ hour: 0, minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 })
45+
case 'hour':
46+
return zdt.with({ minute: 0, second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 })
47+
case 'minute':
48+
return zdt.with({ second: 0, millisecond: 0, microsecond: 0, nanosecond: 0 })
49+
case 'second':
50+
return zdt.with({ millisecond: 0, microsecond: 0, nanosecond: 0 })
51+
case 'millisecond':
52+
return zdt.with({ microsecond: 0, nanosecond: 0 })
53+
default:
54+
return zdt
55+
}
56+
})(input, args, options)
57+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { startOf } from '../startOf'
3+
4+
describe('startOf', () => {
5+
describe('with string input', () => {
6+
test('should return start of year', () => {
7+
const result = startOf('2024-03-15T14:42:12.789Z', { unit: 'year' }, {
8+
timeZone: 'UTC',
9+
})
10+
expect(result.value).toBe('2024-01-01T00:00:00Z')
11+
expect(result.options.timeZone).toBe('UTC')
12+
expect(result.options.calendar).toBeDefined()
13+
})
14+
15+
test('should return start of month', () => {
16+
const result = startOf('2024-03-15T14:42:12.789Z', { unit: 'month' }, {
17+
timeZone: 'UTC',
18+
})
19+
expect(result.value).toBe('2024-03-01T00:00:00Z')
20+
})
21+
22+
test('should return start of day', () => {
23+
const result = startOf('2024-03-15T14:42:12.789Z', { unit: 'day' }, {
24+
timeZone: 'UTC',
25+
})
26+
expect(result.value).toBe('2024-03-15T00:00:00Z')
27+
})
28+
29+
test('should return start of hour', () => {
30+
const result = startOf('2024-03-15T14:42:12.789Z', { unit: 'hour' })
31+
expect(result.value).toContain('2024-03-15T14:00:00')
32+
})
33+
34+
test('should return start of minute', () => {
35+
const result = startOf('2024-03-15T14:42:12.789Z', { unit: 'minute' })
36+
expect(result.value).toContain('2024-03-15T14:42:00')
37+
})
38+
39+
test('should return start of second', () => {
40+
const result = startOf('2024-03-15T14:42:12.789Z', { unit: 'second' })
41+
expect(result.value).toContain('2024-03-15T14:42:12')
42+
})
43+
44+
test('should return start of millisecond', () => {
45+
const result = startOf('2024-03-15T14:42:12.789Z', { unit: 'millisecond' })
46+
expect(result.value).toContain('2024-03-15T14:42:12.789')
47+
})
48+
})
49+
50+
51+
describe('week calculation', () => {
52+
test('should return start of week (Monday)', () => {
53+
// 2024-03-15 is a Friday (day 5), so start of week should be Monday (2024-03-11)
54+
const result = startOf('2024-03-15T14:42:12.789Z', { unit: 'week' })
55+
const resultDate = new Date(result.value as unknown as string)
56+
expect(resultDate.getDay()).toBe(1) // Monday
57+
})
58+
59+
test('should return start of week for Sunday', () => {
60+
// 2024-03-17 is a Sunday (day 7), so start of week should be Monday (2024-03-11)
61+
const result = startOf('2024-03-17T14:42:12.789Z', { unit: 'week' })
62+
const resultDate = new Date(result.value as unknown as string)
63+
expect(resultDate.getDay()).toBe(1) // Monday
64+
})
65+
66+
test('should return start of week for Monday', () => {
67+
// 2024-03-11 is a Monday (day 1), so start of week should be itself
68+
const result = startOf('2024-03-11T14:42:12.789Z', { unit: 'week' })
69+
const resultDate = new Date(result.value as unknown as string)
70+
expect(resultDate.getDay()).toBe(1) // Monday
71+
expect(resultDate.toISOString()).toContain('2024-03-11')
72+
})
73+
})
74+
75+
76+
describe('edge cases', () => {
77+
test('should handle start of year for January 1st', () => {
78+
const result = startOf('2024-01-01T00:00:00Z', { unit: 'year' }, {
79+
timeZone: 'UTC',
80+
})
81+
expect(result.value).toBe('2024-01-01T00:00:00Z')
82+
})
83+
84+
test('should handle start of month for first day', () => {
85+
const result = startOf('2024-03-01T00:00:00Z', { unit: 'month' }, {
86+
timeZone: 'UTC',
87+
})
88+
expect(result.value).toBe('2024-03-01T00:00:00Z')
89+
})
90+
91+
test('should handle start of day at midnight', () => {
92+
const result = startOf('2024-03-15T00:00:00Z', { unit: 'day' }, {
93+
timeZone: 'UTC',
94+
})
95+
expect(result.value).toBe('2024-03-15T00:00:00Z')
96+
})
97+
98+
test('should handle start of hour at 00:00', () => {
99+
const result = startOf('2024-03-15T14:00:00Z', { unit: 'hour' })
100+
expect(result.value).toContain('2024-03-15T14:00:00')
101+
})
102+
})
103+
})
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { Temporal } from '@js-temporal/polyfill'
3+
import { withDateOperation } from './withDateOperation'
4+
5+
describe('withDateOperation', () => {
6+
const mockOperation = withDateOperation((zdt) => zdt)
7+
8+
describe('timezone handling', () => {
9+
test('should use default timezone when not provided', () => {
10+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' })
11+
expect(result.options.timeZone).toBeDefined()
12+
expect(typeof result.options.timeZone).toBe('string')
13+
})
14+
15+
test('should use custom timezone', () => {
16+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
17+
timeZone: 'Asia/Tokyo',
18+
})
19+
expect(result.options.timeZone).toBe('Asia/Tokyo')
20+
})
21+
22+
test('should return timezone in options', () => {
23+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
24+
timeZone: 'Europe/London',
25+
})
26+
expect(result.options.timeZone).toBe('Europe/London')
27+
})
28+
})
29+
30+
describe('calendar handling', () => {
31+
test('should use default calendar when not provided', () => {
32+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' })
33+
expect(result.options.calendar).toBeDefined()
34+
expect(typeof result.options.calendar).toBe('string')
35+
})
36+
37+
test('should use custom calendar', () => {
38+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
39+
calendar: 'japanese',
40+
})
41+
expect(result.options.calendar).toBe('japanese')
42+
})
43+
44+
test('should return calendar in options', () => {
45+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
46+
calendar: 'islamic',
47+
})
48+
expect(result.options.calendar).toBe('islamic')
49+
})
50+
})
51+
52+
describe('conversion methods', () => {
53+
test('should have asDate method', () => {
54+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
55+
timeZone: 'UTC',
56+
})
57+
const date = result.asDate()
58+
expect(date).toBeInstanceOf(Date)
59+
expect(date.toISOString()).toContain('2024-03-15')
60+
})
61+
62+
test('should have asEpoch method', () => {
63+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
64+
timeZone: 'UTC',
65+
})
66+
const epoch = result.asEpoch()
67+
expect(typeof epoch).toBe('number')
68+
expect(new Date(epoch).toISOString()).toContain('2024-03-15')
69+
})
70+
71+
test('should have asString method', () => {
72+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
73+
timeZone: 'UTC',
74+
})
75+
const str = result.asString()
76+
expect(typeof str).toBe('string')
77+
expect(str).toContain('2024-03-15')
78+
})
79+
80+
test('should have asLong method', () => {
81+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
82+
timeZone: 'America/New_York',
83+
calendar: 'gregory',
84+
})
85+
const long = result.asLong()
86+
expect(typeof long).toBe('string')
87+
expect(long).toContain('2024-03-15')
88+
expect(long).toContain('[America/New_York]')
89+
expect(long).toContain('[u-ca=gregory]')
90+
})
91+
92+
test('should have asZonedDateTime method', () => {
93+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
94+
timeZone: 'UTC',
95+
})
96+
const zdt = result.asZonedDateTime()
97+
expect(zdt).toBeInstanceOf(Temporal.ZonedDateTime)
98+
expect(zdt.toInstant().toString()).toContain('2024-03-15')
99+
})
100+
})
101+
102+
describe('result properties', () => {
103+
test('should have value property as string', () => {
104+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
105+
timeZone: 'UTC',
106+
})
107+
expect(typeof result.value).toBe('string')
108+
expect(result.value).toContain('2024-03-15')
109+
})
110+
111+
test('should have options property', () => {
112+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' })
113+
expect(result.options).toHaveProperty('timeZone')
114+
expect(result.options).toHaveProperty('calendar')
115+
expect(typeof result.options.timeZone).toBe('string')
116+
expect(typeof result.options.calendar).toBe('string')
117+
})
118+
119+
test('should have timeZone property', () => {
120+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
121+
timeZone: 'Asia/Tokyo',
122+
})
123+
expect(result.timeZone).toBe('Asia/Tokyo')
124+
})
125+
126+
test('should have calendar property', () => {
127+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
128+
calendar: 'japanese',
129+
})
130+
expect(result.calendar).toBe('japanese')
131+
})
132+
133+
test('should support destructuring', () => {
134+
const result = mockOperation('2024-03-15T14:42:12.789Z', { unit: 'test' }, {
135+
timeZone: 'UTC',
136+
calendar: 'gregory',
137+
})
138+
const { value, timeZone, calendar } = result
139+
expect(typeof value).toBe('string')
140+
expect(timeZone).toBe('UTC')
141+
expect(calendar).toBe('gregory')
142+
})
143+
})
144+
145+
describe('operation execution', () => {
146+
test('should execute the provided operation function', () => {
147+
const addDayOperation = withDateOperation<{ days: number }>((zdt, { days }) => {
148+
return zdt.add({ days })
149+
})
150+
const result = addDayOperation('2024-03-15T00:00:00Z', { days: 1 }, {
151+
timeZone: 'UTC',
152+
})
153+
expect(result.value).toBe('2024-03-16T00:00:00Z')
154+
})
155+
156+
test('should pass args to the operation function', () => {
157+
const customOperation = withDateOperation<{ multiplier: number }>((zdt, { multiplier }) => {
158+
const days = zdt.day * multiplier
159+
return zdt.with({ day: days })
160+
})
161+
const result = customOperation('2024-03-15T00:00:00Z', { multiplier: 2 }, {
162+
timeZone: 'UTC',
163+
})
164+
expect(result.value).toContain('2024-03-30')
165+
})
166+
})
167+
})

0 commit comments

Comments
 (0)