foldkit 0.59.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/calendar/arithmetic.d.ts +140 -0
- package/dist/calendar/arithmetic.d.ts.map +1 -0
- package/dist/calendar/arithmetic.js +169 -0
- package/dist/calendar/calendarDate.d.ts +162 -0
- package/dist/calendar/calendarDate.d.ts.map +1 -0
- package/dist/calendar/calendarDate.js +196 -0
- package/dist/calendar/comparison.d.ts +163 -0
- package/dist/calendar/comparison.d.ts.map +1 -0
- package/dist/calendar/comparison.js +134 -0
- package/dist/calendar/index.d.ts +7 -0
- package/dist/calendar/index.d.ts.map +1 -0
- package/dist/calendar/index.js +6 -0
- package/dist/calendar/info.d.ts +76 -0
- package/dist/calendar/info.d.ts.map +1 -0
- package/dist/calendar/info.js +125 -0
- package/dist/calendar/locale.d.ts +71 -0
- package/dist/calendar/locale.d.ts.map +1 -0
- package/dist/calendar/locale.js +171 -0
- package/dist/calendar/public.d.ts +2 -0
- package/dist/calendar/public.d.ts.map +1 -0
- package/dist/calendar/public.js +1 -0
- package/dist/calendar/today.d.ts +41 -0
- package/dist/calendar/today.d.ts.map +1 -0
- package/dist/calendar/today.js +33 -0
- package/dist/html/index.d.ts +3 -3
- package/dist/html/index.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/test/apps/disabledButton.d.ts +2 -2
- package/dist/ui/calendar/index.d.ts +242 -0
- package/dist/ui/calendar/index.d.ts.map +1 -0
- package/dist/ui/calendar/index.js +515 -0
- package/dist/ui/calendar/public.d.ts +3 -0
- package/dist/ui/calendar/public.d.ts.map +1 -0
- package/dist/ui/calendar/public.js +1 -0
- package/dist/ui/combobox/multi.d.ts +7 -4
- package/dist/ui/combobox/multi.d.ts.map +1 -1
- package/dist/ui/combobox/multi.js +3 -1
- package/dist/ui/combobox/multiPublic.d.ts +1 -1
- package/dist/ui/combobox/multiPublic.d.ts.map +1 -1
- package/dist/ui/combobox/multiPublic.js +1 -1
- package/dist/ui/combobox/public.d.ts +1 -1
- package/dist/ui/combobox/public.d.ts.map +1 -1
- package/dist/ui/combobox/public.js +1 -1
- package/dist/ui/combobox/shared.d.ts +3 -3
- package/dist/ui/combobox/shared.js +3 -3
- package/dist/ui/combobox/single.d.ts +8 -4
- package/dist/ui/combobox/single.d.ts.map +1 -1
- package/dist/ui/combobox/single.js +4 -1
- package/dist/ui/dialog/index.d.ts +1 -1
- package/dist/ui/dialog/index.js +2 -2
- package/dist/ui/dragAndDrop/index.d.ts +1 -1
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/listbox/multi.d.ts +7 -4
- package/dist/ui/listbox/multi.d.ts.map +1 -1
- package/dist/ui/listbox/multi.js +3 -1
- package/dist/ui/listbox/multiPublic.d.ts +1 -1
- package/dist/ui/listbox/multiPublic.d.ts.map +1 -1
- package/dist/ui/listbox/multiPublic.js +1 -1
- package/dist/ui/listbox/shared.d.ts +3 -3
- package/dist/ui/listbox/shared.js +2 -2
- package/dist/ui/listbox/single.d.ts +1 -1
- package/dist/ui/menu/index.d.ts +2 -2
- package/dist/ui/menu/index.js +2 -2
- package/dist/ui/popover/index.d.ts +2 -2
- package/dist/ui/popover/index.js +2 -2
- package/dist/ui/transition/index.d.ts +2 -2
- package/dist/ui/transition/index.d.ts.map +1 -1
- package/dist/ui/transition/index.js +2 -2
- package/dist/ui/transition/public.d.ts +1 -1
- package/dist/ui/transition/public.d.ts.map +1 -1
- package/dist/ui/transition/public.js +1 -1
- package/dist/ui/transition/schema.d.ts +3 -3
- package/dist/ui/transition/schema.d.ts.map +1 -1
- package/dist/ui/transition/schema.js +2 -2
- package/dist/ui/transition/update.js +1 -1
- package/package.json +9 -1
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { type CalendarDate } from './calendarDate';
|
|
2
|
+
/**
|
|
3
|
+
* Adds `n` days to a calendar date. Negative `n` subtracts days.
|
|
4
|
+
* Handles month and year rollovers correctly in both directions.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { Calendar } from 'foldkit'
|
|
9
|
+
* import { pipe } from 'effect'
|
|
10
|
+
*
|
|
11
|
+
* Calendar.addDays(Calendar.make(2026, 4, 13), 5)
|
|
12
|
+
* // { year: 2026, month: 4, day: 18 }
|
|
13
|
+
*
|
|
14
|
+
* pipe(Calendar.make(2026, 4, 30), Calendar.addDays(1))
|
|
15
|
+
* // { year: 2026, month: 5, day: 1 }
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare const addDays: {
|
|
19
|
+
(n: number): (self: CalendarDate) => CalendarDate;
|
|
20
|
+
(self: CalendarDate, n: number): CalendarDate;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Subtracts `n` days from a calendar date. Equivalent to `addDays(self, -n)`.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { Calendar } from 'foldkit'
|
|
28
|
+
* import { pipe } from 'effect'
|
|
29
|
+
*
|
|
30
|
+
* Calendar.subtractDays(Calendar.make(2026, 5, 1), 1)
|
|
31
|
+
* // { year: 2026, month: 4, day: 30 }
|
|
32
|
+
*
|
|
33
|
+
* pipe(Calendar.make(2026, 1, 1), Calendar.subtractDays(1))
|
|
34
|
+
* // { year: 2025, month: 12, day: 31 }
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export declare const subtractDays: {
|
|
38
|
+
(n: number): (self: CalendarDate) => CalendarDate;
|
|
39
|
+
(self: CalendarDate, n: number): CalendarDate;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Adds `n` months to a calendar date. Negative `n` subtracts months.
|
|
43
|
+
*
|
|
44
|
+
* Clamps the day to the last valid day of the resulting month when the
|
|
45
|
+
* original day would exceed it. So `addMonths(make(2026, 1, 31), 1)` returns
|
|
46
|
+
* February 28, 2026 (not March 3).
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```ts
|
|
50
|
+
* import { Calendar } from 'foldkit'
|
|
51
|
+
* import { pipe } from 'effect'
|
|
52
|
+
*
|
|
53
|
+
* Calendar.addMonths(Calendar.make(2026, 4, 13), 3)
|
|
54
|
+
* // { year: 2026, month: 7, day: 13 }
|
|
55
|
+
*
|
|
56
|
+
* pipe(Calendar.make(2026, 1, 31), Calendar.addMonths(1))
|
|
57
|
+
* // { year: 2026, month: 2, day: 28 } — clamped from 31
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare const addMonths: {
|
|
61
|
+
(n: number): (self: CalendarDate) => CalendarDate;
|
|
62
|
+
(self: CalendarDate, n: number): CalendarDate;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Subtracts `n` months from a calendar date. Equivalent to `addMonths(self, -n)`.
|
|
66
|
+
*/
|
|
67
|
+
export declare const subtractMonths: {
|
|
68
|
+
(n: number): (self: CalendarDate) => CalendarDate;
|
|
69
|
+
(self: CalendarDate, n: number): CalendarDate;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Adds `n` years to a calendar date. Handles leap-year edge cases by clamping
|
|
73
|
+
* day-of-month when the target year's month is shorter (February 29 in a
|
|
74
|
+
* leap year + 1 year = February 28).
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* import { Calendar } from 'foldkit'
|
|
79
|
+
* import { pipe } from 'effect'
|
|
80
|
+
*
|
|
81
|
+
* Calendar.addYears(Calendar.make(2024, 2, 29), 1)
|
|
82
|
+
* // { year: 2025, month: 2, day: 28 } — clamped
|
|
83
|
+
*
|
|
84
|
+
* pipe(Calendar.make(2026, 4, 13), Calendar.addYears(5))
|
|
85
|
+
* // { year: 2031, month: 4, day: 13 }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare const addYears: {
|
|
89
|
+
(n: number): (self: CalendarDate) => CalendarDate;
|
|
90
|
+
(self: CalendarDate, n: number): CalendarDate;
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Subtracts `n` years from a calendar date. Equivalent to `addYears(self, -n)`.
|
|
94
|
+
*/
|
|
95
|
+
export declare const subtractYears: {
|
|
96
|
+
(n: number): (self: CalendarDate) => CalendarDate;
|
|
97
|
+
(self: CalendarDate, n: number): CalendarDate;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Returns the number of days from `self` until `end`, positive when `end` is
|
|
101
|
+
* after `self`, negative when before, zero when equal. Matches `Temporal.PlainDate.until`.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* import { Calendar } from 'foldkit'
|
|
106
|
+
* import { pipe } from 'effect'
|
|
107
|
+
*
|
|
108
|
+
* const today = Calendar.make(2026, 4, 13)
|
|
109
|
+
* const birthday = Calendar.make(2026, 7, 15)
|
|
110
|
+
*
|
|
111
|
+
* Calendar.daysUntil(today, birthday) // 93
|
|
112
|
+
* pipe(today, Calendar.daysUntil(birthday)) // 93
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export declare const daysUntil: {
|
|
116
|
+
(end: CalendarDate): (self: CalendarDate) => number;
|
|
117
|
+
(self: CalendarDate, end: CalendarDate): number;
|
|
118
|
+
};
|
|
119
|
+
/**
|
|
120
|
+
* Returns the number of days from `start` until `self`, positive when `self`
|
|
121
|
+
* is after `start`, negative when before, zero when equal. Matches
|
|
122
|
+
* `Temporal.PlainDate.since`.
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* import { Calendar } from 'foldkit'
|
|
127
|
+
* import { pipe } from 'effect'
|
|
128
|
+
*
|
|
129
|
+
* const today = Calendar.make(2026, 4, 13)
|
|
130
|
+
* const startOfYear = Calendar.make(2026, 1, 1)
|
|
131
|
+
*
|
|
132
|
+
* Calendar.daysSince(today, startOfYear) // 102
|
|
133
|
+
* pipe(today, Calendar.daysSince(startOfYear)) // 102
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export declare const daysSince: {
|
|
137
|
+
(start: CalendarDate): (self: CalendarDate) => number;
|
|
138
|
+
(self: CalendarDate, start: CalendarDate): number;
|
|
139
|
+
};
|
|
140
|
+
//# sourceMappingURL=arithmetic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"arithmetic.d.ts","sourceRoot":"","sources":["../../src/calendar/arithmetic.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,YAAY,EAA2B,MAAM,gBAAgB,CAAA;AAyD3E;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,OAAO,EAAE;IACpB,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAA;IACjD,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CAK9C,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,EAAE;IACzB,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAA;IACjD,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CAI9C,CAAA;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,SAAS,EAAE;IACtB,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAA;IACjD,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CAU7C,CAAA;AAEF;;GAEG;AACH,eAAO,MAAM,cAAc,EAAE;IAC3B,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAA;IACjD,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CAI9C,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,QAAQ,EAAE;IACrB,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAA;IACjD,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CAK9C,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,aAAa,EAAE;IAC1B,CAAC,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,YAAY,CAAA;IACjD,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,MAAM,GAAG,YAAY,CAAA;CAI9C,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,SAAS,EAAE;IACtB,CAAC,GAAG,EAAE,YAAY,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,CAAA;IACnD,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,YAAY,GAAG,MAAM,CAAA;CAKhD,CAAA;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,SAAS,EAAE;IACtB,CAAC,KAAK,EAAE,YAAY,GAAG,CAAC,IAAI,EAAE,YAAY,KAAK,MAAM,CAAA;IACrD,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,GAAG,MAAM,CAAA;CAKlD,CAAA"}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Function } from 'effect';
|
|
2
|
+
import { daysInMonth, unsafeMake } from './calendarDate';
|
|
3
|
+
// NOTE: Rata Die conversion uses Howard Hinnant's algorithm, which correctly
|
|
4
|
+
// handles all Gregorian dates including century and quadricentennial leap-year
|
|
5
|
+
// boundaries. Treat `toRataDie`/`fromRataDie` as mathematical primitives —
|
|
6
|
+
// their correctness is verified by the test suite, not by reading the formulas.
|
|
7
|
+
// See http://howardhinnant.github.io/date_algorithms.html
|
|
8
|
+
const MONTHS_PER_YEAR = 12;
|
|
9
|
+
const DAYS_PER_YEAR = 365;
|
|
10
|
+
// NOTE: Gregorian era constants for Howard Hinnant's Rata Die algorithm.
|
|
11
|
+
// `YEARS_PER_ERA` is the length of the full Gregorian leap-year cycle.
|
|
12
|
+
// `DAYS_PER_ERA` is the exact day count over that cycle (400 × 365.2425).
|
|
13
|
+
// `EPOCH_DAY_OFFSET` aligns the Rata Die ordinal with 1970-01-01 = 0.
|
|
14
|
+
const YEARS_PER_ERA = 400;
|
|
15
|
+
const DAYS_PER_ERA = 146097;
|
|
16
|
+
const EPOCH_DAY_OFFSET = 719468;
|
|
17
|
+
const toRataDie = (date) => {
|
|
18
|
+
const { year, month, day } = date;
|
|
19
|
+
const adjustedYear = month <= 2 ? year - 1 : year;
|
|
20
|
+
const era = Math.floor(adjustedYear / YEARS_PER_ERA);
|
|
21
|
+
const yearOfEra = adjustedYear - era * YEARS_PER_ERA;
|
|
22
|
+
const dayOfYear = Math.floor((153 * (month > 2 ? month - 3 : month + 9) + 2) / 5) + day - 1;
|
|
23
|
+
const dayOfEra = yearOfEra * DAYS_PER_YEAR +
|
|
24
|
+
Math.floor(yearOfEra / 4) -
|
|
25
|
+
Math.floor(yearOfEra / 100) +
|
|
26
|
+
dayOfYear;
|
|
27
|
+
return era * DAYS_PER_ERA + dayOfEra - EPOCH_DAY_OFFSET;
|
|
28
|
+
};
|
|
29
|
+
const fromRataDie = (rataDie) => {
|
|
30
|
+
const shifted = rataDie + EPOCH_DAY_OFFSET;
|
|
31
|
+
const era = Math.floor(shifted / DAYS_PER_ERA);
|
|
32
|
+
const dayOfEra = shifted - era * DAYS_PER_ERA;
|
|
33
|
+
const yearOfEra = Math.floor((dayOfEra -
|
|
34
|
+
Math.floor(dayOfEra / 1460) +
|
|
35
|
+
Math.floor(dayOfEra / 36524) -
|
|
36
|
+
Math.floor(dayOfEra / 146096)) /
|
|
37
|
+
DAYS_PER_YEAR);
|
|
38
|
+
const year = yearOfEra + era * YEARS_PER_ERA;
|
|
39
|
+
const dayOfYear = dayOfEra -
|
|
40
|
+
(DAYS_PER_YEAR * yearOfEra +
|
|
41
|
+
Math.floor(yearOfEra / 4) -
|
|
42
|
+
Math.floor(yearOfEra / 100));
|
|
43
|
+
const monthAdjusted = Math.floor((5 * dayOfYear + 2) / 153);
|
|
44
|
+
const month = monthAdjusted < 10 ? monthAdjusted + 3 : monthAdjusted - 9;
|
|
45
|
+
const day = dayOfYear - Math.floor((153 * monthAdjusted + 2) / 5) + 1;
|
|
46
|
+
return unsafeMake(month <= 2 ? year + 1 : year, month, day);
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Adds `n` days to a calendar date. Negative `n` subtracts days.
|
|
50
|
+
* Handles month and year rollovers correctly in both directions.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```ts
|
|
54
|
+
* import { Calendar } from 'foldkit'
|
|
55
|
+
* import { pipe } from 'effect'
|
|
56
|
+
*
|
|
57
|
+
* Calendar.addDays(Calendar.make(2026, 4, 13), 5)
|
|
58
|
+
* // { year: 2026, month: 4, day: 18 }
|
|
59
|
+
*
|
|
60
|
+
* pipe(Calendar.make(2026, 4, 30), Calendar.addDays(1))
|
|
61
|
+
* // { year: 2026, month: 5, day: 1 }
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export const addDays = Function.dual(2, (self, n) => n === 0 ? self : fromRataDie(toRataDie(self) + n));
|
|
65
|
+
/**
|
|
66
|
+
* Subtracts `n` days from a calendar date. Equivalent to `addDays(self, -n)`.
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { Calendar } from 'foldkit'
|
|
71
|
+
* import { pipe } from 'effect'
|
|
72
|
+
*
|
|
73
|
+
* Calendar.subtractDays(Calendar.make(2026, 5, 1), 1)
|
|
74
|
+
* // { year: 2026, month: 4, day: 30 }
|
|
75
|
+
*
|
|
76
|
+
* pipe(Calendar.make(2026, 1, 1), Calendar.subtractDays(1))
|
|
77
|
+
* // { year: 2025, month: 12, day: 31 }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const subtractDays = Function.dual(2, (self, n) => addDays(self, -n));
|
|
81
|
+
/**
|
|
82
|
+
* Adds `n` months to a calendar date. Negative `n` subtracts months.
|
|
83
|
+
*
|
|
84
|
+
* Clamps the day to the last valid day of the resulting month when the
|
|
85
|
+
* original day would exceed it. So `addMonths(make(2026, 1, 31), 1)` returns
|
|
86
|
+
* February 28, 2026 (not March 3).
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { Calendar } from 'foldkit'
|
|
91
|
+
* import { pipe } from 'effect'
|
|
92
|
+
*
|
|
93
|
+
* Calendar.addMonths(Calendar.make(2026, 4, 13), 3)
|
|
94
|
+
* // { year: 2026, month: 7, day: 13 }
|
|
95
|
+
*
|
|
96
|
+
* pipe(Calendar.make(2026, 1, 31), Calendar.addMonths(1))
|
|
97
|
+
* // { year: 2026, month: 2, day: 28 } — clamped from 31
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export const addMonths = Function.dual(2, (self, n) => {
|
|
101
|
+
const totalMonthsFromZero = self.year * MONTHS_PER_YEAR + (self.month - 1) + n;
|
|
102
|
+
const newYear = Math.floor(totalMonthsFromZero / MONTHS_PER_YEAR);
|
|
103
|
+
const newMonth = (((totalMonthsFromZero % MONTHS_PER_YEAR) + MONTHS_PER_YEAR) %
|
|
104
|
+
MONTHS_PER_YEAR) +
|
|
105
|
+
1;
|
|
106
|
+
const newDay = Math.min(self.day, daysInMonth(newYear, newMonth));
|
|
107
|
+
return unsafeMake(newYear, newMonth, newDay);
|
|
108
|
+
});
|
|
109
|
+
/**
|
|
110
|
+
* Subtracts `n` months from a calendar date. Equivalent to `addMonths(self, -n)`.
|
|
111
|
+
*/
|
|
112
|
+
export const subtractMonths = Function.dual(2, (self, n) => addMonths(self, -n));
|
|
113
|
+
/**
|
|
114
|
+
* Adds `n` years to a calendar date. Handles leap-year edge cases by clamping
|
|
115
|
+
* day-of-month when the target year's month is shorter (February 29 in a
|
|
116
|
+
* leap year + 1 year = February 28).
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* import { Calendar } from 'foldkit'
|
|
121
|
+
* import { pipe } from 'effect'
|
|
122
|
+
*
|
|
123
|
+
* Calendar.addYears(Calendar.make(2024, 2, 29), 1)
|
|
124
|
+
* // { year: 2025, month: 2, day: 28 } — clamped
|
|
125
|
+
*
|
|
126
|
+
* pipe(Calendar.make(2026, 4, 13), Calendar.addYears(5))
|
|
127
|
+
* // { year: 2031, month: 4, day: 13 }
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
export const addYears = Function.dual(2, (self, n) => addMonths(self, n * MONTHS_PER_YEAR));
|
|
131
|
+
/**
|
|
132
|
+
* Subtracts `n` years from a calendar date. Equivalent to `addYears(self, -n)`.
|
|
133
|
+
*/
|
|
134
|
+
export const subtractYears = Function.dual(2, (self, n) => addYears(self, -n));
|
|
135
|
+
/**
|
|
136
|
+
* Returns the number of days from `self` until `end`, positive when `end` is
|
|
137
|
+
* after `self`, negative when before, zero when equal. Matches `Temporal.PlainDate.until`.
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```ts
|
|
141
|
+
* import { Calendar } from 'foldkit'
|
|
142
|
+
* import { pipe } from 'effect'
|
|
143
|
+
*
|
|
144
|
+
* const today = Calendar.make(2026, 4, 13)
|
|
145
|
+
* const birthday = Calendar.make(2026, 7, 15)
|
|
146
|
+
*
|
|
147
|
+
* Calendar.daysUntil(today, birthday) // 93
|
|
148
|
+
* pipe(today, Calendar.daysUntil(birthday)) // 93
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export const daysUntil = Function.dual(2, (self, end) => toRataDie(end) - toRataDie(self));
|
|
152
|
+
/**
|
|
153
|
+
* Returns the number of days from `start` until `self`, positive when `self`
|
|
154
|
+
* is after `start`, negative when before, zero when equal. Matches
|
|
155
|
+
* `Temporal.PlainDate.since`.
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* ```ts
|
|
159
|
+
* import { Calendar } from 'foldkit'
|
|
160
|
+
* import { pipe } from 'effect'
|
|
161
|
+
*
|
|
162
|
+
* const today = Calendar.make(2026, 4, 13)
|
|
163
|
+
* const startOfYear = Calendar.make(2026, 1, 1)
|
|
164
|
+
*
|
|
165
|
+
* Calendar.daysSince(today, startOfYear) // 102
|
|
166
|
+
* pipe(today, Calendar.daysSince(startOfYear)) // 102
|
|
167
|
+
* ```
|
|
168
|
+
*/
|
|
169
|
+
export const daysSince = Function.dual(2, (self, start) => toRataDie(self) - toRataDie(start));
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Schema as S } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Determines if a year is a leap year in the Gregorian calendar.
|
|
4
|
+
*
|
|
5
|
+
* A year is a leap year if it is divisible by 4, except for century years
|
|
6
|
+
* (divisible by 100) which must also be divisible by 400. So 2000 is a leap
|
|
7
|
+
* year but 1900 is not.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { Calendar } from 'foldkit'
|
|
12
|
+
*
|
|
13
|
+
* Calendar.isLeapYear(2024) // true
|
|
14
|
+
* Calendar.isLeapYear(2026) // false
|
|
15
|
+
* Calendar.isLeapYear(2000) // true (divisible by 400)
|
|
16
|
+
* Calendar.isLeapYear(1900) // false (century, not divisible by 400)
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare const isLeapYear: (year: number) => boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Returns the number of days in a given month of a given year.
|
|
22
|
+
* Leap-year-aware: February returns 29 in leap years, 28 otherwise.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { Calendar } from 'foldkit'
|
|
27
|
+
*
|
|
28
|
+
* Calendar.daysInMonth(2026, 1) // 31 (January)
|
|
29
|
+
* Calendar.daysInMonth(2026, 2) // 28 (February, non-leap)
|
|
30
|
+
* Calendar.daysInMonth(2024, 2) // 29 (February, leap)
|
|
31
|
+
* Calendar.daysInMonth(2026, 4) // 30 (April)
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export declare const daysInMonth: (year: number, month: number) => number;
|
|
35
|
+
/**
|
|
36
|
+
* A calendar date — year, month, day. No time, no timezone.
|
|
37
|
+
*
|
|
38
|
+
* Models the same concept as Java's `LocalDate` or TC39's `Temporal.PlainDate`.
|
|
39
|
+
* Useful when you need to represent a date without a clock attached —
|
|
40
|
+
* birthdays, deadlines, form date inputs, event calendars.
|
|
41
|
+
*
|
|
42
|
+
* Validation ensures the date is a real calendar date: months are 1-12 and
|
|
43
|
+
* days are within the month's actual length. Leap-year-aware, so February 30
|
|
44
|
+
* is rejected and February 29 is only accepted in leap years.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* import { Calendar } from 'foldkit'
|
|
49
|
+
* import { Schema as S } from 'effect'
|
|
50
|
+
*
|
|
51
|
+
* const date = Calendar.make(2026, 4, 13)
|
|
52
|
+
* S.decodeUnknownSync(Calendar.CalendarDate)({ year: 2026, month: 4, day: 13 })
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export declare const CalendarDate: S.filter<S.Struct<{
|
|
56
|
+
year: typeof S.Int;
|
|
57
|
+
month: S.filter<typeof S.Int>;
|
|
58
|
+
day: S.filter<typeof S.Int>;
|
|
59
|
+
}>>;
|
|
60
|
+
export type CalendarDate = typeof CalendarDate.Type;
|
|
61
|
+
/**
|
|
62
|
+
* Type guard for `CalendarDate`. Returns true when `value` is a valid
|
|
63
|
+
* calendar date struct.
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* ```ts
|
|
67
|
+
* import { Calendar } from 'foldkit'
|
|
68
|
+
*
|
|
69
|
+
* Calendar.isCalendarDate({ year: 2026, month: 4, day: 13 }) // true
|
|
70
|
+
* Calendar.isCalendarDate({ year: 2026, month: 2, day: 30 }) // false
|
|
71
|
+
* Calendar.isCalendarDate('2026-04-13') // false
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
export declare const isCalendarDate: (value: unknown) => value is CalendarDate;
|
|
75
|
+
/**
|
|
76
|
+
* Constructs a `CalendarDate`, validating via Schema.
|
|
77
|
+
* Throws a `ParseError` if the combination is not a real calendar date
|
|
78
|
+
* (e.g. February 30, month 13, day 0).
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* import { Calendar } from 'foldkit'
|
|
83
|
+
*
|
|
84
|
+
* const birthday = Calendar.make(1990, 7, 15)
|
|
85
|
+
* Calendar.make(2024, 2, 29) // OK, 2024 is a leap year
|
|
86
|
+
* Calendar.make(2026, 2, 29) // throws — not a leap year
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
export declare const make: (year: number, month: number, day: number) => CalendarDate;
|
|
90
|
+
/**
|
|
91
|
+
* Constructs a `CalendarDate` without Schema validation. Only for inputs the
|
|
92
|
+
* caller knows are valid — typically arithmetic results inside the calendar
|
|
93
|
+
* module. Consumers should prefer `make`.
|
|
94
|
+
*/
|
|
95
|
+
export declare const unsafeMake: (year: number, month: number, day: number) => CalendarDate;
|
|
96
|
+
/**
|
|
97
|
+
* Constructs a `CalendarDate` from a JavaScript `Date` object, reading the
|
|
98
|
+
* year/month/day in the browser's local timezone.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* import { Calendar } from 'foldkit'
|
|
103
|
+
*
|
|
104
|
+
* const date = Calendar.fromDateLocal(new Date())
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
107
|
+
export declare const fromDateLocal: (date: Date) => CalendarDate;
|
|
108
|
+
/**
|
|
109
|
+
* Constructs a `CalendarDate` from a JavaScript `Date` object, reading the
|
|
110
|
+
* year/month/day in a specific IANA timezone (e.g. `"America/New_York"`).
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* import { Calendar } from 'foldkit'
|
|
115
|
+
*
|
|
116
|
+
* const bookingDate = Calendar.fromDateInZone(new Date(), 'America/New_York')
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export declare const fromDateInZone: (date: Date, timeZone: string) => CalendarDate;
|
|
120
|
+
/**
|
|
121
|
+
* Converts a `CalendarDate` to a JavaScript `Date` object representing
|
|
122
|
+
* midnight at the start of that day in the browser's local timezone.
|
|
123
|
+
*
|
|
124
|
+
* Note: `Date` objects always carry a time and timezone component. This
|
|
125
|
+
* function intentionally pins the time to local midnight. For cross-timezone
|
|
126
|
+
* use, pass the result through an `Intl.DateTimeFormat` with an explicit
|
|
127
|
+
* timezone, or keep working with `CalendarDate`.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* import { Calendar } from 'foldkit'
|
|
132
|
+
*
|
|
133
|
+
* const jsDate = Calendar.toDateLocal(Calendar.make(2026, 4, 13))
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
export declare const toDateLocal: (calendarDate: CalendarDate) => Date;
|
|
137
|
+
/**
|
|
138
|
+
* Schema transform between an ISO 8601 date string (`YYYY-MM-DD`) and a
|
|
139
|
+
* `CalendarDate`. Useful for form inputs, JSON serialization, URL query
|
|
140
|
+
* parameters, and hidden form input values.
|
|
141
|
+
*
|
|
142
|
+
* Decoding accepts only zero-padded ISO dates. Invalid calendar dates like
|
|
143
|
+
* `2026-02-30` decode the string shape but fail the `CalendarDate` filter.
|
|
144
|
+
*
|
|
145
|
+
* @example
|
|
146
|
+
* ```ts
|
|
147
|
+
* import { Calendar } from 'foldkit'
|
|
148
|
+
* import { Schema as S } from 'effect'
|
|
149
|
+
*
|
|
150
|
+
* const decode = S.decodeUnknownSync(Calendar.CalendarDateFromIsoString)
|
|
151
|
+
* const encode = S.encodeSync(Calendar.CalendarDateFromIsoString)
|
|
152
|
+
*
|
|
153
|
+
* decode('2026-04-13') // { year: 2026, month: 4, day: 13 }
|
|
154
|
+
* encode(Calendar.make(2026, 4, 13)) // "2026-04-13"
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
export declare const CalendarDateFromIsoString: S.transformOrFail<typeof S.String, S.filter<S.Struct<{
|
|
158
|
+
year: typeof S.Int;
|
|
159
|
+
month: S.filter<typeof S.Int>;
|
|
160
|
+
day: S.filter<typeof S.Int>;
|
|
161
|
+
}>>, never>;
|
|
162
|
+
//# sourceMappingURL=calendarDate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"calendarDate.d.ts","sourceRoot":"","sources":["../../src/calendar/calendarDate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,MAAM,IAAI,CAAC,EAAE,MAAM,QAAQ,CAAA;AAEjD;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,UAAU,GAAI,MAAM,MAAM,KAAG,OACgB,CAAA;AAE1D;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,KAAG,MAQzD,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY;;;;GAUxB,CAAA;AAED,MAAM,MAAM,YAAY,GAAG,OAAO,YAAY,CAAC,IAAI,CAAA;AAEnD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,cAAc,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,KAAK,IAAI,YACtC,CAAA;AAEpB;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,IAAI,GAAI,MAAM,MAAM,EAAE,OAAO,MAAM,EAAE,KAAK,MAAM,KAAG,YACP,CAAA;AAEzD;;;;GAIG;AACH,eAAO,MAAM,UAAU,GACrB,MAAM,MAAM,EACZ,OAAO,MAAM,EACb,KAAK,MAAM,KACV,YACmE,CAAA;AAEtE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,GAAI,MAAM,IAAI,KAAG,YAC0B,CAAA;AAErE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,GAAI,MAAM,IAAI,EAAE,UAAU,MAAM,KAAG,YAa7D,CAAA;AAED;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,WAAW,GAAI,cAAc,YAAY,KAAG,IACc,CAAA;AAIvE;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,yBAAyB;;;;WA4BrC,CAAA"}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { ParseResult, Schema as S } from 'effect';
|
|
2
|
+
/**
|
|
3
|
+
* Determines if a year is a leap year in the Gregorian calendar.
|
|
4
|
+
*
|
|
5
|
+
* A year is a leap year if it is divisible by 4, except for century years
|
|
6
|
+
* (divisible by 100) which must also be divisible by 400. So 2000 is a leap
|
|
7
|
+
* year but 1900 is not.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { Calendar } from 'foldkit'
|
|
12
|
+
*
|
|
13
|
+
* Calendar.isLeapYear(2024) // true
|
|
14
|
+
* Calendar.isLeapYear(2026) // false
|
|
15
|
+
* Calendar.isLeapYear(2000) // true (divisible by 400)
|
|
16
|
+
* Calendar.isLeapYear(1900) // false (century, not divisible by 400)
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const isLeapYear = (year) => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
20
|
+
/**
|
|
21
|
+
* Returns the number of days in a given month of a given year.
|
|
22
|
+
* Leap-year-aware: February returns 29 in leap years, 28 otherwise.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { Calendar } from 'foldkit'
|
|
27
|
+
*
|
|
28
|
+
* Calendar.daysInMonth(2026, 1) // 31 (January)
|
|
29
|
+
* Calendar.daysInMonth(2026, 2) // 28 (February, non-leap)
|
|
30
|
+
* Calendar.daysInMonth(2024, 2) // 29 (February, leap)
|
|
31
|
+
* Calendar.daysInMonth(2026, 4) // 30 (April)
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export const daysInMonth = (year, month) => {
|
|
35
|
+
if (month === 2) {
|
|
36
|
+
return isLeapYear(year) ? 29 : 28;
|
|
37
|
+
}
|
|
38
|
+
if (month === 4 || month === 6 || month === 9 || month === 11) {
|
|
39
|
+
return 30;
|
|
40
|
+
}
|
|
41
|
+
return 31;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* A calendar date — year, month, day. No time, no timezone.
|
|
45
|
+
*
|
|
46
|
+
* Models the same concept as Java's `LocalDate` or TC39's `Temporal.PlainDate`.
|
|
47
|
+
* Useful when you need to represent a date without a clock attached —
|
|
48
|
+
* birthdays, deadlines, form date inputs, event calendars.
|
|
49
|
+
*
|
|
50
|
+
* Validation ensures the date is a real calendar date: months are 1-12 and
|
|
51
|
+
* days are within the month's actual length. Leap-year-aware, so February 30
|
|
52
|
+
* is rejected and February 29 is only accepted in leap years.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import { Calendar } from 'foldkit'
|
|
57
|
+
* import { Schema as S } from 'effect'
|
|
58
|
+
*
|
|
59
|
+
* const date = Calendar.make(2026, 4, 13)
|
|
60
|
+
* S.decodeUnknownSync(Calendar.CalendarDate)({ year: 2026, month: 4, day: 13 })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
export const CalendarDate = S.Struct({
|
|
64
|
+
year: S.Int,
|
|
65
|
+
month: S.Int.pipe(S.between(1, 12)),
|
|
66
|
+
day: S.Int.pipe(S.between(1, 31)),
|
|
67
|
+
}).pipe(S.filter(({ year, month, day }) => day <= daysInMonth(year, month), {
|
|
68
|
+
identifier: 'CalendarDate',
|
|
69
|
+
description: 'a valid calendar date (year, month 1-12, day within month length)',
|
|
70
|
+
}));
|
|
71
|
+
/**
|
|
72
|
+
* Type guard for `CalendarDate`. Returns true when `value` is a valid
|
|
73
|
+
* calendar date struct.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* import { Calendar } from 'foldkit'
|
|
78
|
+
*
|
|
79
|
+
* Calendar.isCalendarDate({ year: 2026, month: 4, day: 13 }) // true
|
|
80
|
+
* Calendar.isCalendarDate({ year: 2026, month: 2, day: 30 }) // false
|
|
81
|
+
* Calendar.isCalendarDate('2026-04-13') // false
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export const isCalendarDate = S.is(CalendarDate);
|
|
85
|
+
/**
|
|
86
|
+
* Constructs a `CalendarDate`, validating via Schema.
|
|
87
|
+
* Throws a `ParseError` if the combination is not a real calendar date
|
|
88
|
+
* (e.g. February 30, month 13, day 0).
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* ```ts
|
|
92
|
+
* import { Calendar } from 'foldkit'
|
|
93
|
+
*
|
|
94
|
+
* const birthday = Calendar.make(1990, 7, 15)
|
|
95
|
+
* Calendar.make(2024, 2, 29) // OK, 2024 is a leap year
|
|
96
|
+
* Calendar.make(2026, 2, 29) // throws — not a leap year
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
export const make = (year, month, day) => S.decodeUnknownSync(CalendarDate)({ year, month, day });
|
|
100
|
+
/**
|
|
101
|
+
* Constructs a `CalendarDate` without Schema validation. Only for inputs the
|
|
102
|
+
* caller knows are valid — typically arithmetic results inside the calendar
|
|
103
|
+
* module. Consumers should prefer `make`.
|
|
104
|
+
*/
|
|
105
|
+
export const unsafeMake = (year, month, day) => CalendarDate.make({ year, month, day }, { disableValidation: true });
|
|
106
|
+
/**
|
|
107
|
+
* Constructs a `CalendarDate` from a JavaScript `Date` object, reading the
|
|
108
|
+
* year/month/day in the browser's local timezone.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* import { Calendar } from 'foldkit'
|
|
113
|
+
*
|
|
114
|
+
* const date = Calendar.fromDateLocal(new Date())
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
export const fromDateLocal = (date) => unsafeMake(date.getFullYear(), date.getMonth() + 1, date.getDate());
|
|
118
|
+
/**
|
|
119
|
+
* Constructs a `CalendarDate` from a JavaScript `Date` object, reading the
|
|
120
|
+
* year/month/day in a specific IANA timezone (e.g. `"America/New_York"`).
|
|
121
|
+
*
|
|
122
|
+
* @example
|
|
123
|
+
* ```ts
|
|
124
|
+
* import { Calendar } from 'foldkit'
|
|
125
|
+
*
|
|
126
|
+
* const bookingDate = Calendar.fromDateInZone(new Date(), 'America/New_York')
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export const fromDateInZone = (date, timeZone) => {
|
|
130
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
131
|
+
timeZone,
|
|
132
|
+
year: 'numeric',
|
|
133
|
+
month: '2-digit',
|
|
134
|
+
day: '2-digit',
|
|
135
|
+
});
|
|
136
|
+
const parts = formatter.formatToParts(date);
|
|
137
|
+
const getPart = (type) => {
|
|
138
|
+
const part = parts.find(candidate => candidate.type === type);
|
|
139
|
+
return part ? Number(part.value) : 0;
|
|
140
|
+
};
|
|
141
|
+
return unsafeMake(getPart('year'), getPart('month'), getPart('day'));
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Converts a `CalendarDate` to a JavaScript `Date` object representing
|
|
145
|
+
* midnight at the start of that day in the browser's local timezone.
|
|
146
|
+
*
|
|
147
|
+
* Note: `Date` objects always carry a time and timezone component. This
|
|
148
|
+
* function intentionally pins the time to local midnight. For cross-timezone
|
|
149
|
+
* use, pass the result through an `Intl.DateTimeFormat` with an explicit
|
|
150
|
+
* timezone, or keep working with `CalendarDate`.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* ```ts
|
|
154
|
+
* import { Calendar } from 'foldkit'
|
|
155
|
+
*
|
|
156
|
+
* const jsDate = Calendar.toDateLocal(Calendar.make(2026, 4, 13))
|
|
157
|
+
* ```
|
|
158
|
+
*/
|
|
159
|
+
export const toDateLocal = (calendarDate) => new Date(calendarDate.year, calendarDate.month - 1, calendarDate.day);
|
|
160
|
+
const isoPattern = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
161
|
+
/**
|
|
162
|
+
* Schema transform between an ISO 8601 date string (`YYYY-MM-DD`) and a
|
|
163
|
+
* `CalendarDate`. Useful for form inputs, JSON serialization, URL query
|
|
164
|
+
* parameters, and hidden form input values.
|
|
165
|
+
*
|
|
166
|
+
* Decoding accepts only zero-padded ISO dates. Invalid calendar dates like
|
|
167
|
+
* `2026-02-30` decode the string shape but fail the `CalendarDate` filter.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* import { Calendar } from 'foldkit'
|
|
172
|
+
* import { Schema as S } from 'effect'
|
|
173
|
+
*
|
|
174
|
+
* const decode = S.decodeUnknownSync(Calendar.CalendarDateFromIsoString)
|
|
175
|
+
* const encode = S.encodeSync(Calendar.CalendarDateFromIsoString)
|
|
176
|
+
*
|
|
177
|
+
* decode('2026-04-13') // { year: 2026, month: 4, day: 13 }
|
|
178
|
+
* encode(Calendar.make(2026, 4, 13)) // "2026-04-13"
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export const CalendarDateFromIsoString = S.transformOrFail(S.String, CalendarDate, {
|
|
182
|
+
strict: true,
|
|
183
|
+
decode: (input, _options, ast) => {
|
|
184
|
+
const match = input.match(isoPattern);
|
|
185
|
+
if (match === null) {
|
|
186
|
+
return ParseResult.fail(new ParseResult.Type(ast, input, `Expected ISO date (YYYY-MM-DD), got ${JSON.stringify(input)}`));
|
|
187
|
+
}
|
|
188
|
+
const [, yearString, monthString, dayString] = match;
|
|
189
|
+
return ParseResult.succeed({
|
|
190
|
+
year: Number(yearString),
|
|
191
|
+
month: Number(monthString),
|
|
192
|
+
day: Number(dayString),
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
encode: ({ year, month, day }) => ParseResult.succeed(`${String(year).padStart(4, '0')}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`),
|
|
196
|
+
});
|