@intech-software/chronera 0.1.6 → 0.1.7

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.
@@ -0,0 +1,279 @@
1
+ import { addDays, addMonths, addYears, getAbsoluteDay, isAfter, } from "./convenience.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Day-of-week mapping
4
+ // ---------------------------------------------------------------------------
5
+ // absoluteDay % 7 → correct mapping per actual absoluteDayFromGregorianFields:
6
+ // Thursday=0, Friday=1, Saturday=2, Sunday=3, Monday=4, Tuesday=5, Wednesday=6
7
+ const DOW_INDEX = {
8
+ thursday: 0,
9
+ friday: 1,
10
+ saturday: 2,
11
+ sunday: 3,
12
+ monday: 4,
13
+ tuesday: 5,
14
+ wednesday: 6,
15
+ };
16
+ // ---------------------------------------------------------------------------
17
+ // Core computation
18
+ // ---------------------------------------------------------------------------
19
+ const MAX_OCCURRENCES = 500;
20
+ const MAX_YEARS_AHEAD = 50;
21
+ /**
22
+ * Computes all occurrences of a recurring schedule.
23
+ */
24
+ export function getOccurrences(startDate, rule, limit) {
25
+ const maxCount = Math.min(limit ?? MAX_OCCURRENCES, MAX_OCCURRENCES);
26
+ const results = [];
27
+ // Compute absolute "50 years from start" safety boundary
28
+ const farFuture = addYears(startDate, MAX_YEARS_AHEAD, "constrain");
29
+ const withinBounds = (date) => {
30
+ if (rule.until && isAfter(date, rule.until))
31
+ return false;
32
+ if (isAfter(date, farFuture))
33
+ return false;
34
+ return true;
35
+ };
36
+ const withinCount = () => {
37
+ if (rule.count !== undefined && results.length >= rule.count)
38
+ return false;
39
+ return true;
40
+ };
41
+ switch (rule.frequency) {
42
+ case "daily": {
43
+ let current = startDate;
44
+ while (withinBounds(current) &&
45
+ withinCount() &&
46
+ results.length < maxCount) {
47
+ results.push(current);
48
+ current = addDays(current, rule.interval);
49
+ }
50
+ break;
51
+ }
52
+ case "weekly": {
53
+ if (rule.daysOfWeek && rule.daysOfWeek.length > 0) {
54
+ // For weekly with daysOfWeek: iterate week by week (interval * 7 days)
55
+ // Within each week boundary, include occurrences on matching days of week
56
+ const startAbs = getAbsoluteDay(startDate);
57
+ const dowTargets = new Set(rule.daysOfWeek.map((d) => DOW_INDEX[d]));
58
+ // DOW offset: we need to know startDate's day of week
59
+ // Find the start-of-week (Sunday) for startDate
60
+ // We'll iterate day by day within each week window
61
+ let weekAnchor = startDate;
62
+ let done = false;
63
+ while (!done) {
64
+ // Check each day of the 7-day window starting at weekAnchor
65
+ for (let i = 0; i < 7 && !done; i++) {
66
+ const candidate = addDays(weekAnchor, i);
67
+ const candidateAbs = getAbsoluteDay(candidate);
68
+ // Must not be before startDate
69
+ if (candidateAbs < startAbs)
70
+ continue;
71
+ const dow = ((candidateAbs % 7) + 7) % 7;
72
+ if (dowTargets.has(dow)) {
73
+ if (!withinBounds(candidate) ||
74
+ !withinCount() ||
75
+ results.length >= maxCount) {
76
+ done = true;
77
+ break;
78
+ }
79
+ results.push(candidate);
80
+ }
81
+ }
82
+ if (!done) {
83
+ weekAnchor = addDays(weekAnchor, rule.interval * 7);
84
+ if (!withinBounds(weekAnchor) || results.length >= maxCount) {
85
+ done = true;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ else {
91
+ // Simple weekly: every interval * 7 days
92
+ let current = startDate;
93
+ while (withinBounds(current) &&
94
+ withinCount() &&
95
+ results.length < maxCount) {
96
+ results.push(current);
97
+ current = addDays(current, rule.interval * 7);
98
+ }
99
+ }
100
+ break;
101
+ }
102
+ case "monthly": {
103
+ let current = startDate;
104
+ while (withinBounds(current) &&
105
+ withinCount() &&
106
+ results.length < maxCount) {
107
+ results.push(current);
108
+ current = addMonths(current, rule.interval, "constrain");
109
+ }
110
+ break;
111
+ }
112
+ case "yearly": {
113
+ let current = startDate;
114
+ while (withinBounds(current) &&
115
+ withinCount() &&
116
+ results.length < maxCount) {
117
+ results.push(current);
118
+ current = addYears(current, rule.interval, "constrain");
119
+ }
120
+ break;
121
+ }
122
+ }
123
+ return results;
124
+ }
125
+ /**
126
+ * Returns true if the given date is an occurrence of the recurrence rule starting from startDate.
127
+ */
128
+ export function isOccurrence(date, startDate, rule) {
129
+ const dateAbs = getAbsoluteDay(date);
130
+ const startAbs = getAbsoluteDay(startDate);
131
+ if (dateAbs < startAbs)
132
+ return false;
133
+ if (rule.until && isAfter(date, rule.until))
134
+ return false;
135
+ const diff = dateAbs - startAbs;
136
+ switch (rule.frequency) {
137
+ case "daily":
138
+ return diff % rule.interval === 0;
139
+ case "weekly": {
140
+ if (rule.daysOfWeek && rule.daysOfWeek.length > 0) {
141
+ // Check if the week offset is a multiple of interval
142
+ const weekDiff = Math.floor(diff / 7);
143
+ if (weekDiff % rule.interval !== 0)
144
+ return false;
145
+ // Check if the day of week matches
146
+ const dow = ((dateAbs % 7) + 7) % 7;
147
+ return rule.daysOfWeek.some((d) => DOW_INDEX[d] === dow);
148
+ }
149
+ return diff % (rule.interval * 7) === 0;
150
+ }
151
+ case "monthly": {
152
+ // Compute how many months apart the two dates are
153
+ // and check that diff is an exact multiple of rule.interval months
154
+ // by stepping through
155
+ let cursor = startDate;
156
+ let monthCount = 0;
157
+ const targetAbs = dateAbs;
158
+ while (getAbsoluteDay(cursor) <= targetAbs) {
159
+ const cursorAbs = getAbsoluteDay(cursor);
160
+ if (cursorAbs === targetAbs && monthCount % rule.interval === 0) {
161
+ return true;
162
+ }
163
+ if (cursorAbs > targetAbs)
164
+ break;
165
+ cursor = addMonths(cursor, rule.interval, "constrain");
166
+ monthCount += rule.interval;
167
+ if (monthCount > 12 * MAX_YEARS_AHEAD)
168
+ break;
169
+ }
170
+ return false;
171
+ }
172
+ case "yearly": {
173
+ let cursor = startDate;
174
+ let yearCount = 0;
175
+ const targetAbs = dateAbs;
176
+ while (getAbsoluteDay(cursor) <= targetAbs) {
177
+ if (getAbsoluteDay(cursor) === targetAbs)
178
+ return true;
179
+ cursor = addYears(cursor, rule.interval, "constrain");
180
+ yearCount += rule.interval;
181
+ if (yearCount > MAX_YEARS_AHEAD)
182
+ break;
183
+ }
184
+ return false;
185
+ }
186
+ }
187
+ }
188
+ // ---------------------------------------------------------------------------
189
+ // Fluent Builder Implementation
190
+ // ---------------------------------------------------------------------------
191
+ class RecurrenceRuleBuilderImpl {
192
+ _startDate;
193
+ _frequency;
194
+ _interval;
195
+ _daysOfWeek;
196
+ _until;
197
+ _count;
198
+ constructor(startDate, frequency, interval) {
199
+ this._startDate = startDate;
200
+ this._frequency = frequency;
201
+ this._interval = interval;
202
+ }
203
+ on(days) {
204
+ this._daysOfWeek = days;
205
+ return this;
206
+ }
207
+ until(date) {
208
+ this._until = date;
209
+ return this;
210
+ }
211
+ count(n) {
212
+ this._count = n;
213
+ return this;
214
+ }
215
+ build() {
216
+ const rule = {
217
+ frequency: this._frequency,
218
+ interval: this._interval,
219
+ };
220
+ if (this._daysOfWeek)
221
+ rule.daysOfWeek = this._daysOfWeek;
222
+ if (this._until)
223
+ rule.until = this._until;
224
+ if (this._count !== undefined)
225
+ rule.count = this._count;
226
+ return rule;
227
+ }
228
+ next(n) {
229
+ return getOccurrences(this._startDate, this.build(), n);
230
+ }
231
+ occurrences() {
232
+ return getOccurrences(this._startDate, this.build());
233
+ }
234
+ }
235
+ class FrequencySelectorImpl {
236
+ _startDate;
237
+ _interval;
238
+ constructor(startDate, interval) {
239
+ this._startDate = startDate;
240
+ this._interval = interval;
241
+ }
242
+ days() {
243
+ return new RecurrenceRuleBuilderImpl(this._startDate, "daily", this._interval);
244
+ }
245
+ weeks() {
246
+ return new RecurrenceRuleBuilderImpl(this._startDate, "weekly", this._interval);
247
+ }
248
+ months() {
249
+ return new RecurrenceRuleBuilderImpl(this._startDate, "monthly", this._interval);
250
+ }
251
+ years() {
252
+ return new RecurrenceRuleBuilderImpl(this._startDate, "yearly", this._interval);
253
+ }
254
+ }
255
+ class RecurrenceBuilderImpl {
256
+ _startDate;
257
+ constructor(startDate) {
258
+ this._startDate = startDate;
259
+ }
260
+ every(n) {
261
+ return new FrequencySelectorImpl(this._startDate, n);
262
+ }
263
+ }
264
+ /**
265
+ * Creates a fluent recurrence builder anchored to the given start date.
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * const dates = recur(localDate(2026, 1, 1))
270
+ * .every(1).weeks()
271
+ * .on(['monday', 'wednesday', 'friday'])
272
+ * .until(localDate(2026, 3, 31))
273
+ * .occurrences();
274
+ * ```
275
+ */
276
+ export function recur(startDate) {
277
+ return new RecurrenceBuilderImpl(startDate);
278
+ }
279
+ //# sourceMappingURL=recurrence.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"recurrence.js","sourceRoot":"","sources":["../../src/operations/recurrence.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n addDays,\n addMonths,\n addYears,\n getAbsoluteDay,\n isAfter,\n} from \"./convenience.js\";\n\nimport type { DateOrCalendarDate } from \"../public-types.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type RecurrenceFrequency = \"daily\" | \"weekly\" | \"monthly\" | \"yearly\";\n\nexport type DayOfWeek =\n | \"monday\"\n | \"tuesday\"\n | \"wednesday\"\n | \"thursday\"\n | \"friday\"\n | \"saturday\"\n | \"sunday\";\n\nexport interface RecurrenceRule {\n readonly frequency: RecurrenceFrequency;\n readonly interval: number;\n readonly daysOfWeek?: DayOfWeek[];\n readonly until?: DateOrCalendarDate;\n readonly count?: number;\n}\n\n// ---------------------------------------------------------------------------\n// Builder Interfaces\n// ---------------------------------------------------------------------------\n\nexport interface RecurrenceBuilder {\n every(n: number): FrequencySelector;\n}\n\nexport interface FrequencySelector {\n days(): RecurrenceRuleBuilder;\n weeks(): RecurrenceRuleBuilder;\n months(): RecurrenceRuleBuilder;\n years(): RecurrenceRuleBuilder;\n}\n\nexport interface RecurrenceRuleBuilder {\n on(days: DayOfWeek[]): RecurrenceRuleBuilder;\n until(date: DateOrCalendarDate): RecurrenceRuleBuilder;\n count(n: number): RecurrenceRuleBuilder;\n next(n: number): DateOrCalendarDate[];\n occurrences(): DateOrCalendarDate[];\n build(): RecurrenceRule;\n}\n\n// ---------------------------------------------------------------------------\n// Day-of-week mapping\n// ---------------------------------------------------------------------------\n\n// absoluteDay % 7 → correct mapping per actual absoluteDayFromGregorianFields:\n// Thursday=0, Friday=1, Saturday=2, Sunday=3, Monday=4, Tuesday=5, Wednesday=6\nconst DOW_INDEX: Record<DayOfWeek, number> = {\n thursday: 0,\n friday: 1,\n saturday: 2,\n sunday: 3,\n monday: 4,\n tuesday: 5,\n wednesday: 6,\n};\n\n// ---------------------------------------------------------------------------\n// Core computation\n// ---------------------------------------------------------------------------\n\nconst MAX_OCCURRENCES = 500;\nconst MAX_YEARS_AHEAD = 50;\n\n/**\n * Computes all occurrences of a recurring schedule.\n */\nexport function getOccurrences<T extends DateOrCalendarDate>(\n startDate: T,\n rule: RecurrenceRule,\n limit?: number,\n): T[] {\n const maxCount = Math.min(limit ?? MAX_OCCURRENCES, MAX_OCCURRENCES);\n const results: T[] = [];\n\n // Compute absolute \"50 years from start\" safety boundary\n const farFuture = addYears(startDate, MAX_YEARS_AHEAD, \"constrain\");\n\n const withinBounds = (date: T): boolean => {\n if (rule.until && isAfter(date, rule.until as T)) return false;\n if (isAfter(date, farFuture)) return false;\n return true;\n };\n\n const withinCount = (): boolean => {\n if (rule.count !== undefined && results.length >= rule.count) return false;\n return true;\n };\n\n switch (rule.frequency) {\n case \"daily\": {\n let current = startDate;\n while (\n withinBounds(current) &&\n withinCount() &&\n results.length < maxCount\n ) {\n results.push(current);\n current = addDays(current, rule.interval);\n }\n break;\n }\n\n case \"weekly\": {\n if (rule.daysOfWeek && rule.daysOfWeek.length > 0) {\n // For weekly with daysOfWeek: iterate week by week (interval * 7 days)\n // Within each week boundary, include occurrences on matching days of week\n const startAbs = getAbsoluteDay(startDate);\n const dowTargets = new Set(rule.daysOfWeek.map((d) => DOW_INDEX[d]));\n\n // DOW offset: we need to know startDate's day of week\n // Find the start-of-week (Sunday) for startDate\n // We'll iterate day by day within each week window\n\n let weekAnchor = startDate;\n let done = false;\n\n while (!done) {\n // Check each day of the 7-day window starting at weekAnchor\n for (let i = 0; i < 7 && !done; i++) {\n const candidate = addDays(weekAnchor, i);\n const candidateAbs = getAbsoluteDay(candidate);\n\n // Must not be before startDate\n if (candidateAbs < startAbs) continue;\n\n const dow = ((candidateAbs % 7) + 7) % 7;\n if (dowTargets.has(dow)) {\n if (\n !withinBounds(candidate) ||\n !withinCount() ||\n results.length >= maxCount\n ) {\n done = true;\n break;\n }\n results.push(candidate);\n }\n }\n\n if (!done) {\n weekAnchor = addDays(weekAnchor, rule.interval * 7);\n if (!withinBounds(weekAnchor) || results.length >= maxCount) {\n done = true;\n }\n }\n }\n } else {\n // Simple weekly: every interval * 7 days\n let current = startDate;\n while (\n withinBounds(current) &&\n withinCount() &&\n results.length < maxCount\n ) {\n results.push(current);\n current = addDays(current, rule.interval * 7);\n }\n }\n break;\n }\n\n case \"monthly\": {\n let current = startDate;\n while (\n withinBounds(current) &&\n withinCount() &&\n results.length < maxCount\n ) {\n results.push(current);\n current = addMonths(current, rule.interval, \"constrain\");\n }\n break;\n }\n\n case \"yearly\": {\n let current = startDate;\n while (\n withinBounds(current) &&\n withinCount() &&\n results.length < maxCount\n ) {\n results.push(current);\n current = addYears(current, rule.interval, \"constrain\");\n }\n break;\n }\n }\n\n return results;\n}\n\n/**\n * Returns true if the given date is an occurrence of the recurrence rule starting from startDate.\n */\nexport function isOccurrence<T extends DateOrCalendarDate>(\n date: T,\n startDate: T,\n rule: RecurrenceRule,\n): boolean {\n const dateAbs = getAbsoluteDay(date);\n const startAbs = getAbsoluteDay(startDate);\n\n if (dateAbs < startAbs) return false;\n if (rule.until && isAfter(date, rule.until as T)) return false;\n\n const diff = dateAbs - startAbs;\n\n switch (rule.frequency) {\n case \"daily\":\n return diff % rule.interval === 0;\n\n case \"weekly\": {\n if (rule.daysOfWeek && rule.daysOfWeek.length > 0) {\n // Check if the week offset is a multiple of interval\n const weekDiff = Math.floor(diff / 7);\n if (weekDiff % rule.interval !== 0) return false;\n // Check if the day of week matches\n const dow = ((dateAbs % 7) + 7) % 7;\n return rule.daysOfWeek.some((d) => DOW_INDEX[d] === dow);\n }\n return diff % (rule.interval * 7) === 0;\n }\n\n case \"monthly\": {\n // Compute how many months apart the two dates are\n // and check that diff is an exact multiple of rule.interval months\n // by stepping through\n let cursor = startDate;\n let monthCount = 0;\n const targetAbs = dateAbs;\n\n while (getAbsoluteDay(cursor) <= targetAbs) {\n const cursorAbs = getAbsoluteDay(cursor);\n if (cursorAbs === targetAbs && monthCount % rule.interval === 0) {\n return true;\n }\n if (cursorAbs > targetAbs) break;\n cursor = addMonths(cursor, rule.interval, \"constrain\");\n monthCount += rule.interval;\n\n if (monthCount > 12 * MAX_YEARS_AHEAD) break;\n }\n return false;\n }\n\n case \"yearly\": {\n let cursor = startDate;\n let yearCount = 0;\n const targetAbs = dateAbs;\n\n while (getAbsoluteDay(cursor) <= targetAbs) {\n if (getAbsoluteDay(cursor) === targetAbs) return true;\n cursor = addYears(cursor, rule.interval, \"constrain\");\n yearCount += rule.interval;\n if (yearCount > MAX_YEARS_AHEAD) break;\n }\n return false;\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// Fluent Builder Implementation\n// ---------------------------------------------------------------------------\n\nclass RecurrenceRuleBuilderImpl implements RecurrenceRuleBuilder {\n private _startDate: DateOrCalendarDate;\n private _frequency: RecurrenceFrequency;\n private _interval: number;\n private _daysOfWeek?: DayOfWeek[];\n private _until?: DateOrCalendarDate;\n private _count?: number;\n\n constructor(\n startDate: DateOrCalendarDate,\n frequency: RecurrenceFrequency,\n interval: number,\n ) {\n this._startDate = startDate;\n this._frequency = frequency;\n this._interval = interval;\n }\n\n on(days: DayOfWeek[]): RecurrenceRuleBuilder {\n this._daysOfWeek = days;\n return this;\n }\n\n until(date: DateOrCalendarDate): RecurrenceRuleBuilder {\n this._until = date;\n return this;\n }\n\n count(n: number): RecurrenceRuleBuilder {\n this._count = n;\n return this;\n }\n\n build(): RecurrenceRule {\n const rule: {\n frequency: RecurrenceFrequency;\n interval: number;\n daysOfWeek?: DayOfWeek[];\n until?: DateOrCalendarDate;\n count?: number;\n } = {\n frequency: this._frequency,\n interval: this._interval,\n };\n if (this._daysOfWeek) rule.daysOfWeek = this._daysOfWeek;\n if (this._until) rule.until = this._until;\n if (this._count !== undefined) rule.count = this._count;\n return rule;\n }\n\n next(n: number): DateOrCalendarDate[] {\n return getOccurrences(this._startDate, this.build(), n);\n }\n\n occurrences(): DateOrCalendarDate[] {\n return getOccurrences(this._startDate, this.build());\n }\n}\n\nclass FrequencySelectorImpl implements FrequencySelector {\n private _startDate: DateOrCalendarDate;\n private _interval: number;\n\n constructor(startDate: DateOrCalendarDate, interval: number) {\n this._startDate = startDate;\n this._interval = interval;\n }\n\n days(): RecurrenceRuleBuilder {\n return new RecurrenceRuleBuilderImpl(\n this._startDate,\n \"daily\",\n this._interval,\n );\n }\n\n weeks(): RecurrenceRuleBuilder {\n return new RecurrenceRuleBuilderImpl(\n this._startDate,\n \"weekly\",\n this._interval,\n );\n }\n\n months(): RecurrenceRuleBuilder {\n return new RecurrenceRuleBuilderImpl(\n this._startDate,\n \"monthly\",\n this._interval,\n );\n }\n\n years(): RecurrenceRuleBuilder {\n return new RecurrenceRuleBuilderImpl(\n this._startDate,\n \"yearly\",\n this._interval,\n );\n }\n}\n\nclass RecurrenceBuilderImpl implements RecurrenceBuilder {\n private _startDate: DateOrCalendarDate;\n\n constructor(startDate: DateOrCalendarDate) {\n this._startDate = startDate;\n }\n\n every(n: number): FrequencySelector {\n return new FrequencySelectorImpl(this._startDate, n);\n }\n}\n\n/**\n * Creates a fluent recurrence builder anchored to the given start date.\n *\n * @example\n * ```ts\n * const dates = recur(localDate(2026, 1, 1))\n * .every(1).weeks()\n * .on(['monday', 'wednesday', 'friday'])\n * .until(localDate(2026, 3, 31))\n * .occurrences();\n * ```\n */\nexport function recur<T extends DateOrCalendarDate>(\n startDate: T,\n): RecurrenceBuilder {\n return new RecurrenceBuilderImpl(startDate);\n}\n"]}
@@ -0,0 +1,58 @@
1
+ import type { DateOrCalendarDate } from "../public-types.js";
2
+ export type BucketUnit = "day" | "week" | "month" | "quarter" | "year";
3
+ export interface BucketKey {
4
+ readonly key: string;
5
+ readonly label: string;
6
+ readonly unit: BucketUnit;
7
+ }
8
+ export type BucketMap<T extends DateOrCalendarDate> = Map<string, T[]>;
9
+ /**
10
+ * Groups an array of dates into buckets by the given unit.
11
+ * Returns a Map of key → dates[].
12
+ * Keys are sorted chronologically.
13
+ *
14
+ * @example
15
+ * const dates = [localDate(2026,9,1), localDate(2026,9,15), localDate(2026,10,1)];
16
+ * bucketDates(dates, 'month')
17
+ * // Map { '2026-09' => [...], '2026-10' => [...] }
18
+ */
19
+ export declare function bucketDates<T extends DateOrCalendarDate>(dates: T[], unit: BucketUnit): BucketMap<T>;
20
+ export declare function bucketByDay<T extends DateOrCalendarDate>(dates: T[]): BucketMap<T>;
21
+ export declare function bucketByWeek<T extends DateOrCalendarDate>(dates: T[]): BucketMap<T>;
22
+ export declare function bucketByMonth<T extends DateOrCalendarDate>(dates: T[]): BucketMap<T>;
23
+ export declare function bucketByQuarter<T extends DateOrCalendarDate>(dates: T[]): BucketMap<T>;
24
+ export declare function bucketByYear<T extends DateOrCalendarDate>(dates: T[]): BucketMap<T>;
25
+ export interface HistogramEntry {
26
+ readonly key: string;
27
+ readonly label: string;
28
+ readonly count: number;
29
+ }
30
+ /**
31
+ * Returns a frequency histogram of dates grouped by the given unit.
32
+ * Result is sorted chronologically.
33
+ *
34
+ * @example
35
+ * histogram(dates, 'month')
36
+ * // [{ key: '2026-09', label: '2026-09', count: 12 }, ...]
37
+ */
38
+ export declare function histogram(dates: DateOrCalendarDate[], unit: BucketUnit): HistogramEntry[];
39
+ /**
40
+ * Sorts an array of dates chronologically (ascending).
41
+ */
42
+ export declare function sortDates<T extends DateOrCalendarDate>(dates: T[], direction?: "asc" | "desc"): T[];
43
+ /**
44
+ * Finds the minimum (earliest) date in an array.
45
+ */
46
+ export declare function minDate<T extends DateOrCalendarDate>(dates: T[]): T;
47
+ /**
48
+ * Finds the maximum (latest) date in an array.
49
+ */
50
+ export declare function maxDate<T extends DateOrCalendarDate>(dates: T[]): T;
51
+ /**
52
+ * Finds the date nearest to the reference date.
53
+ */
54
+ export declare function nearestDate<T extends DateOrCalendarDate>(dates: T[], ref: T): T;
55
+ /**
56
+ * Removes duplicate dates from an array (keeps first occurrence).
57
+ */
58
+ export declare function uniqueDates<T extends DateOrCalendarDate>(dates: T[]): T[];
@@ -0,0 +1,153 @@
1
+ import { absoluteDayFromGregorianFields, gregorianFieldsFromAbsoluteDay, } from "../core/absolute-day.js";
2
+ import { getAbsoluteDay } from "./convenience.js";
3
+ function toLocalDate(date) {
4
+ if (date.kind === "local-date")
5
+ return date;
6
+ const abs = getAbsoluteDay(date);
7
+ const f = gregorianFieldsFromAbsoluteDay(abs);
8
+ return { kind: "local-date", year: f.year, month: f.month, day: f.day };
9
+ }
10
+ function pad(n, w = 2) {
11
+ return String(n).padStart(w, "0");
12
+ }
13
+ function bucketKeyFor(date, unit) {
14
+ switch (unit) {
15
+ case "day":
16
+ return `${date.year}-${pad(date.month)}-${pad(date.day)}`;
17
+ case "week": {
18
+ // ISO week: find Monday of that week
19
+ const abs = absoluteDayFromGregorianFields(date.year, date.month, date.day);
20
+ const dow = abs % 7; // 0=Mon
21
+ const monday = gregorianFieldsFromAbsoluteDay(abs - dow);
22
+ return `${monday.year}-W${pad(monday.month)}-${pad(monday.day)}`;
23
+ }
24
+ case "month":
25
+ return `${date.year}-${pad(date.month)}`;
26
+ case "quarter": {
27
+ const q = Math.ceil(date.month / 3);
28
+ return `${date.year}-Q${q}`;
29
+ }
30
+ case "year":
31
+ return `${date.year}`;
32
+ }
33
+ }
34
+ function bucketLabelFor(key, unit) {
35
+ switch (unit) {
36
+ case "day":
37
+ return key; // YYYY-MM-DD
38
+ case "week":
39
+ return key; // YYYY-WMM-DD (start of week)
40
+ case "month":
41
+ return key; // YYYY-MM
42
+ case "quarter":
43
+ return key; // YYYY-Q1
44
+ case "year":
45
+ return key; // YYYY
46
+ }
47
+ }
48
+ /**
49
+ * Groups an array of dates into buckets by the given unit.
50
+ * Returns a Map of key → dates[].
51
+ * Keys are sorted chronologically.
52
+ *
53
+ * @example
54
+ * const dates = [localDate(2026,9,1), localDate(2026,9,15), localDate(2026,10,1)];
55
+ * bucketDates(dates, 'month')
56
+ * // Map { '2026-09' => [...], '2026-10' => [...] }
57
+ */
58
+ export function bucketDates(dates, unit) {
59
+ const map = new Map();
60
+ for (const date of dates) {
61
+ const local = toLocalDate(date);
62
+ const key = bucketKeyFor(local, unit);
63
+ if (!map.has(key))
64
+ map.set(key, []);
65
+ map.get(key).push(date);
66
+ }
67
+ // Sort keys chronologically
68
+ const sorted = new Map([...map.entries()].sort(([a], [b]) => a.localeCompare(b)));
69
+ return sorted;
70
+ }
71
+ export function bucketByDay(dates) {
72
+ return bucketDates(dates, "day");
73
+ }
74
+ export function bucketByWeek(dates) {
75
+ return bucketDates(dates, "week");
76
+ }
77
+ export function bucketByMonth(dates) {
78
+ return bucketDates(dates, "month");
79
+ }
80
+ export function bucketByQuarter(dates) {
81
+ return bucketDates(dates, "quarter");
82
+ }
83
+ export function bucketByYear(dates) {
84
+ return bucketDates(dates, "year");
85
+ }
86
+ /**
87
+ * Returns a frequency histogram of dates grouped by the given unit.
88
+ * Result is sorted chronologically.
89
+ *
90
+ * @example
91
+ * histogram(dates, 'month')
92
+ * // [{ key: '2026-09', label: '2026-09', count: 12 }, ...]
93
+ */
94
+ export function histogram(dates, unit) {
95
+ const bucketed = bucketDates(dates, unit);
96
+ return [...bucketed.entries()].map(([key, items]) => ({
97
+ key,
98
+ label: bucketLabelFor(key, unit),
99
+ count: items.length,
100
+ }));
101
+ }
102
+ // ---------------------------------------------------------------------------
103
+ // Sorting & Finding
104
+ // ---------------------------------------------------------------------------
105
+ /**
106
+ * Sorts an array of dates chronologically (ascending).
107
+ */
108
+ export function sortDates(dates, direction = "asc") {
109
+ const sorted = [...dates].sort((a, b) => getAbsoluteDay(a) - getAbsoluteDay(b));
110
+ return direction === "desc" ? sorted.reverse() : sorted;
111
+ }
112
+ /**
113
+ * Finds the minimum (earliest) date in an array.
114
+ */
115
+ export function minDate(dates) {
116
+ if (dates.length === 0)
117
+ throw new RangeError("Cannot find min of empty array.");
118
+ return dates.reduce((m, d) => getAbsoluteDay(d) < getAbsoluteDay(m) ? d : m);
119
+ }
120
+ /**
121
+ * Finds the maximum (latest) date in an array.
122
+ */
123
+ export function maxDate(dates) {
124
+ if (dates.length === 0)
125
+ throw new RangeError("Cannot find max of empty array.");
126
+ return dates.reduce((m, d) => getAbsoluteDay(d) > getAbsoluteDay(m) ? d : m);
127
+ }
128
+ /**
129
+ * Finds the date nearest to the reference date.
130
+ */
131
+ export function nearestDate(dates, ref) {
132
+ if (dates.length === 0)
133
+ throw new RangeError("Cannot find nearest in empty array.");
134
+ const refAbs = getAbsoluteDay(ref);
135
+ return dates.reduce((nearest, d) => Math.abs(getAbsoluteDay(d) - refAbs) <
136
+ Math.abs(getAbsoluteDay(nearest) - refAbs)
137
+ ? d
138
+ : nearest);
139
+ }
140
+ /**
141
+ * Removes duplicate dates from an array (keeps first occurrence).
142
+ */
143
+ export function uniqueDates(dates) {
144
+ const seen = new Set();
145
+ return dates.filter((d) => {
146
+ const abs = getAbsoluteDay(d);
147
+ if (seen.has(abs))
148
+ return false;
149
+ seen.add(abs);
150
+ return true;
151
+ });
152
+ }
153
+ //# sourceMappingURL=time-series.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time-series.js","sourceRoot":"","sources":["../../src/operations/time-series.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n absoluteDayFromGregorianFields,\n gregorianFieldsFromAbsoluteDay,\n} from \"../core/absolute-day.js\";\nimport { getAbsoluteDay } from \"./convenience.js\";\nimport type { DateOrCalendarDate, LocalDate } from \"../public-types.js\";\n\n// ---------------------------------------------------------------------------\n// Time Series & Bucketing Engine\n// ---------------------------------------------------------------------------\n\nexport type BucketUnit = \"day\" | \"week\" | \"month\" | \"quarter\" | \"year\";\n\nexport interface BucketKey {\n readonly key: string;\n readonly label: string;\n readonly unit: BucketUnit;\n}\n\nexport type BucketMap<T extends DateOrCalendarDate> = Map<string, T[]>;\n\nfunction toLocalDate(date: DateOrCalendarDate): LocalDate {\n if (date.kind === \"local-date\") return date;\n const abs = getAbsoluteDay(date);\n const f = gregorianFieldsFromAbsoluteDay(abs);\n return { kind: \"local-date\", year: f.year, month: f.month, day: f.day };\n}\n\nfunction pad(n: number, w = 2): string {\n return String(n).padStart(w, \"0\");\n}\n\nfunction bucketKeyFor(date: LocalDate, unit: BucketUnit): string {\n switch (unit) {\n case \"day\":\n return `${date.year}-${pad(date.month)}-${pad(date.day)}`;\n case \"week\": {\n // ISO week: find Monday of that week\n const abs = absoluteDayFromGregorianFields(\n date.year,\n date.month,\n date.day,\n );\n const dow = abs % 7; // 0=Mon\n const monday = gregorianFieldsFromAbsoluteDay(abs - dow);\n return `${monday.year}-W${pad(monday.month)}-${pad(monday.day)}`;\n }\n case \"month\":\n return `${date.year}-${pad(date.month)}`;\n case \"quarter\": {\n const q = Math.ceil(date.month / 3);\n return `${date.year}-Q${q}`;\n }\n case \"year\":\n return `${date.year}`;\n }\n}\n\nfunction bucketLabelFor(key: string, unit: BucketUnit): string {\n switch (unit) {\n case \"day\":\n return key; // YYYY-MM-DD\n case \"week\":\n return key; // YYYY-WMM-DD (start of week)\n case \"month\":\n return key; // YYYY-MM\n case \"quarter\":\n return key; // YYYY-Q1\n case \"year\":\n return key; // YYYY\n }\n}\n\n/**\n * Groups an array of dates into buckets by the given unit.\n * Returns a Map of key → dates[].\n * Keys are sorted chronologically.\n *\n * @example\n * const dates = [localDate(2026,9,1), localDate(2026,9,15), localDate(2026,10,1)];\n * bucketDates(dates, 'month')\n * // Map { '2026-09' => [...], '2026-10' => [...] }\n */\nexport function bucketDates<T extends DateOrCalendarDate>(\n dates: T[],\n unit: BucketUnit,\n): BucketMap<T> {\n const map = new Map<string, T[]>();\n for (const date of dates) {\n const local = toLocalDate(date);\n const key = bucketKeyFor(local, unit);\n if (!map.has(key)) map.set(key, []);\n map.get(key)!.push(date);\n }\n // Sort keys chronologically\n const sorted = new Map<string, T[]>(\n [...map.entries()].sort(([a], [b]) => a.localeCompare(b)),\n );\n return sorted;\n}\n\nexport function bucketByDay<T extends DateOrCalendarDate>(\n dates: T[],\n): BucketMap<T> {\n return bucketDates(dates, \"day\");\n}\n\nexport function bucketByWeek<T extends DateOrCalendarDate>(\n dates: T[],\n): BucketMap<T> {\n return bucketDates(dates, \"week\");\n}\n\nexport function bucketByMonth<T extends DateOrCalendarDate>(\n dates: T[],\n): BucketMap<T> {\n return bucketDates(dates, \"month\");\n}\n\nexport function bucketByQuarter<T extends DateOrCalendarDate>(\n dates: T[],\n): BucketMap<T> {\n return bucketDates(dates, \"quarter\");\n}\n\nexport function bucketByYear<T extends DateOrCalendarDate>(\n dates: T[],\n): BucketMap<T> {\n return bucketDates(dates, \"year\");\n}\n\n// ---------------------------------------------------------------------------\n// Histogram\n// ---------------------------------------------------------------------------\n\nexport interface HistogramEntry {\n readonly key: string;\n readonly label: string;\n readonly count: number;\n}\n\n/**\n * Returns a frequency histogram of dates grouped by the given unit.\n * Result is sorted chronologically.\n *\n * @example\n * histogram(dates, 'month')\n * // [{ key: '2026-09', label: '2026-09', count: 12 }, ...]\n */\nexport function histogram(\n dates: DateOrCalendarDate[],\n unit: BucketUnit,\n): HistogramEntry[] {\n const bucketed = bucketDates(dates, unit);\n return [...bucketed.entries()].map(([key, items]) => ({\n key,\n label: bucketLabelFor(key, unit),\n count: items.length,\n }));\n}\n\n// ---------------------------------------------------------------------------\n// Sorting & Finding\n// ---------------------------------------------------------------------------\n\n/**\n * Sorts an array of dates chronologically (ascending).\n */\nexport function sortDates<T extends DateOrCalendarDate>(\n dates: T[],\n direction: \"asc\" | \"desc\" = \"asc\",\n): T[] {\n const sorted = [...dates].sort(\n (a, b) => getAbsoluteDay(a) - getAbsoluteDay(b),\n );\n return direction === \"desc\" ? sorted.reverse() : sorted;\n}\n\n/**\n * Finds the minimum (earliest) date in an array.\n */\nexport function minDate<T extends DateOrCalendarDate>(dates: T[]): T {\n if (dates.length === 0)\n throw new RangeError(\"Cannot find min of empty array.\");\n return dates.reduce((m, d) =>\n getAbsoluteDay(d) < getAbsoluteDay(m) ? d : m,\n );\n}\n\n/**\n * Finds the maximum (latest) date in an array.\n */\nexport function maxDate<T extends DateOrCalendarDate>(dates: T[]): T {\n if (dates.length === 0)\n throw new RangeError(\"Cannot find max of empty array.\");\n return dates.reduce((m, d) =>\n getAbsoluteDay(d) > getAbsoluteDay(m) ? d : m,\n );\n}\n\n/**\n * Finds the date nearest to the reference date.\n */\nexport function nearestDate<T extends DateOrCalendarDate>(\n dates: T[],\n ref: T,\n): T {\n if (dates.length === 0)\n throw new RangeError(\"Cannot find nearest in empty array.\");\n const refAbs = getAbsoluteDay(ref);\n return dates.reduce((nearest, d) =>\n Math.abs(getAbsoluteDay(d) - refAbs) <\n Math.abs(getAbsoluteDay(nearest) - refAbs)\n ? d\n : nearest,\n );\n}\n\n/**\n * Removes duplicate dates from an array (keeps first occurrence).\n */\nexport function uniqueDates<T extends DateOrCalendarDate>(dates: T[]): T[] {\n const seen = new Set<number>();\n return dates.filter((d) => {\n const abs = getAbsoluteDay(d);\n if (seen.has(abs)) return false;\n seen.add(abs);\n return true;\n });\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intech-software/chronera",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A universal, type-safe date, time, calendar, era, locale, and timezone toolkit for JavaScript and TypeScript.",
5
5
  "keywords": [
6
6
  "date",
@@ -86,7 +86,7 @@
86
86
  {
87
87
  "name": "Root (@intech-software/chronera)",
88
88
  "path": "dist/index.js",
89
- "limit": "25 KB"
89
+ "limit": "30 KB"
90
90
  },
91
91
  {
92
92
  "name": "Calendar (@intech-software/chronera/calendar)",