foldkit 0.60.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.
Files changed (38) hide show
  1. package/dist/calendar/arithmetic.d.ts +140 -0
  2. package/dist/calendar/arithmetic.d.ts.map +1 -0
  3. package/dist/calendar/arithmetic.js +169 -0
  4. package/dist/calendar/calendarDate.d.ts +162 -0
  5. package/dist/calendar/calendarDate.d.ts.map +1 -0
  6. package/dist/calendar/calendarDate.js +196 -0
  7. package/dist/calendar/comparison.d.ts +163 -0
  8. package/dist/calendar/comparison.d.ts.map +1 -0
  9. package/dist/calendar/comparison.js +134 -0
  10. package/dist/calendar/index.d.ts +7 -0
  11. package/dist/calendar/index.d.ts.map +1 -0
  12. package/dist/calendar/index.js +6 -0
  13. package/dist/calendar/info.d.ts +76 -0
  14. package/dist/calendar/info.d.ts.map +1 -0
  15. package/dist/calendar/info.js +125 -0
  16. package/dist/calendar/locale.d.ts +71 -0
  17. package/dist/calendar/locale.d.ts.map +1 -0
  18. package/dist/calendar/locale.js +171 -0
  19. package/dist/calendar/public.d.ts +2 -0
  20. package/dist/calendar/public.d.ts.map +1 -0
  21. package/dist/calendar/public.js +1 -0
  22. package/dist/calendar/today.d.ts +41 -0
  23. package/dist/calendar/today.d.ts.map +1 -0
  24. package/dist/calendar/today.js +33 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/dist/ui/calendar/index.d.ts +242 -0
  29. package/dist/ui/calendar/index.d.ts.map +1 -0
  30. package/dist/ui/calendar/index.js +515 -0
  31. package/dist/ui/calendar/public.d.ts +3 -0
  32. package/dist/ui/calendar/public.d.ts.map +1 -0
  33. package/dist/ui/calendar/public.js +1 -0
  34. package/dist/ui/dragAndDrop/index.d.ts +1 -1
  35. package/dist/ui/index.d.ts +1 -0
  36. package/dist/ui/index.d.ts.map +1 -1
  37. package/dist/ui/index.js +1 -0
  38. 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
+ });