@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.
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/operations/age-countdown.d.ts +60 -0
- package/dist/operations/age-countdown.js +199 -0
- package/dist/operations/age-countdown.js.map +1 -0
- package/dist/operations/duration-ops.d.ts +43 -0
- package/dist/operations/duration-ops.js +393 -0
- package/dist/operations/duration-ops.js.map +1 -0
- package/dist/operations/interval.d.ts +55 -0
- package/dist/operations/interval.js +253 -0
- package/dist/operations/interval.js.map +1 -0
- package/dist/operations/natural-language.d.ts +13 -0
- package/dist/operations/natural-language.js +428 -0
- package/dist/operations/natural-language.js.map +1 -0
- package/dist/operations/recurrence.d.ts +48 -0
- package/dist/operations/recurrence.js +279 -0
- package/dist/operations/recurrence.js.map +1 -0
- package/dist/operations/time-series.d.ts +58 -0
- package/dist/operations/time-series.js +153 -0
- package/dist/operations/time-series.js.map +1 -0
- package/package.json +2 -2
|
@@ -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":"AAAA,OAAO,EACL,8BAA8B,EAC9B,8BAA8B,GAC/B,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAiBlD,SAAS,WAAW,CAAC,IAAwB;IAC3C,IAAI,IAAI,CAAC,IAAI,KAAK,YAAY;QAAE,OAAO,IAAI,CAAC;IAC5C,MAAM,GAAG,GAAG,cAAc,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,CAAC,GAAG,8BAA8B,CAAC,GAAG,CAAC,CAAC;IAC9C,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,CAAC;AAC1E,CAAC;AAED,SAAS,GAAG,CAAC,CAAS,EAAE,CAAC,GAAG,CAAC;IAC3B,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,SAAS,YAAY,CAAC,IAAe,EAAE,IAAgB;IACrD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,KAAK;YACR,OAAO,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5D,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,qCAAqC;YACrC,MAAM,GAAG,GAAG,8BAA8B,CACxC,IAAI,CAAC,IAAI,EACT,IAAI,CAAC,KAAK,EACV,IAAI,CAAC,GAAG,CACT,CAAC;YACF,MAAM,GAAG,GAAG,GAAG,GAAG,CAAC,CAAC,CAAC,QAAQ;YAC7B,MAAM,MAAM,GAAG,8BAA8B,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC;YACzD,OAAO,GAAG,MAAM,CAAC,IAAI,KAAK,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;QACnE,CAAC;QACD,KAAK,OAAO;YACV,OAAO,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3C,KAAK,SAAS,CAAC,CAAC,CAAC;YACf,MAAM,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YACpC,OAAO,GAAG,IAAI,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC9B,CAAC;QACD,KAAK,MAAM;YACT,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC1B,CAAC;AACH,CAAC;AAED,SAAS,cAAc,CAAC,GAAW,EAAE,IAAgB;IACnD,QAAQ,IAAI,EAAE,CAAC;QACb,KAAK,KAAK;YACR,OAAO,GAAG,CAAC,CAAC,aAAa;QAC3B,KAAK,MAAM;YACT,OAAO,GAAG,CAAC,CAAC,8BAA8B;QAC5C,KAAK,OAAO;YACV,OAAO,GAAG,CAAC,CAAC,UAAU;QACxB,KAAK,SAAS;YACZ,OAAO,GAAG,CAAC,CAAC,UAAU;QACxB,KAAK,MAAM;YACT,OAAO,GAAG,CAAC,CAAC,OAAO;IACvB,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,WAAW,CACzB,KAAU,EACV,IAAgB;IAEhB,MAAM,GAAG,GAAG,IAAI,GAAG,EAAe,CAAC;IACnC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,CAAC;QAChC,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QACpC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3B,CAAC;IACD,4BAA4B;IAC5B,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,CAAC,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAC1D,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,UAAU,WAAW,CACzB,KAAU;IAEV,OAAO,WAAW,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,KAAU;IAEV,OAAO,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,KAAU;IAEV,OAAO,WAAW,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,eAAe,CAC7B,KAAU;IAEV,OAAO,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,YAAY,CAC1B,KAAU;IAEV,OAAO,WAAW,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;AACpC,CAAC;AAYD;;;;;;;GAOG;AACH,MAAM,UAAU,SAAS,CACvB,KAA2B,EAC3B,IAAgB;IAEhB,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;QACpD,GAAG;QACH,KAAK,EAAE,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC;QAChC,KAAK,EAAE,KAAK,CAAC,MAAM;KACpB,CAAC,CAAC,CAAC;AACN,CAAC;AAED,8EAA8E;AAC9E,oBAAoB;AACpB,8EAA8E;AAE9E;;GAEG;AACH,MAAM,UAAU,SAAS,CACvB,KAAU,EACV,YAA4B,KAAK;IAEjC,MAAM,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,IAAI,CAC5B,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAChD,CAAC;IACF,OAAO,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;AAC1D,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAA+B,KAAU;IAC9D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QACpB,MAAM,IAAI,UAAU,CAAC,iCAAiC,CAAC,CAAC;IAC1D,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAC3B,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC9C,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,OAAO,CAA+B,KAAU;IAC9D,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QACpB,MAAM,IAAI,UAAU,CAAC,iCAAiC,CAAC,CAAC;IAC1D,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAC3B,cAAc,CAAC,CAAC,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAC9C,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CACzB,KAAU,EACV,GAAM;IAEN,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QACpB,MAAM,IAAI,UAAU,CAAC,qCAAqC,CAAC,CAAC;IAC9D,MAAM,MAAM,GAAG,cAAc,CAAC,GAAG,CAAC,CAAC;IACnC,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,CACjC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC;QACpC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,MAAM,CAAC;QACxC,CAAC,CAAC,CAAC;QACH,CAAC,CAAC,OAAO,CACZ,CAAC;AACJ,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,WAAW,CAA+B,KAAU;IAClE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACxB,MAAM,GAAG,GAAG,cAAc,CAAC,CAAC,CAAC,CAAC;QAC9B,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAChC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,OAAO,IAAI,CAAC;IACd,CAAC,CAAC,CAAC;AACL,CAAC","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.
|
|
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": "
|
|
89
|
+
"limit": "30 KB"
|
|
90
90
|
},
|
|
91
91
|
{
|
|
92
92
|
"name": "Calendar (@intech-software/chronera/calendar)",
|