chronos-ts 1.0.3 → 2.0.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.
@@ -0,0 +1,562 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChronosPeriodCollection = void 0;
4
+ /**
5
+ * ChronosPeriodCollection - Manage collections of ChronosPeriod
6
+ * Inspired by spatie/period PHP library
7
+ * @see https://github.com/spatie/period
8
+ */
9
+ const period_1 = require("./period");
10
+ const chronos_1 = require("./chronos");
11
+ /**
12
+ * ChronosPeriodCollection - A collection of periods with powerful operations
13
+ *
14
+ * Inspired by spatie/period, this class provides a rich API for working with
15
+ * collections of time periods including overlap detection, gap analysis,
16
+ * and set operations.
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * const collection = new ChronosPeriodCollection([
21
+ * ChronosPeriod.create('2024-01-01', '2024-01-15'),
22
+ * ChronosPeriod.create('2024-01-10', '2024-01-25'),
23
+ * ChronosPeriod.create('2024-02-01', '2024-02-15'),
24
+ * ]);
25
+ *
26
+ * // Find overlapping periods
27
+ * const overlapping = collection.overlapAll();
28
+ *
29
+ * // Get gaps between periods
30
+ * const gaps = collection.gaps();
31
+ *
32
+ * // Get boundaries
33
+ * const boundaries = collection.boundaries();
34
+ * ```
35
+ */
36
+ class ChronosPeriodCollection {
37
+ // ============================================================================
38
+ // Constructor & Factory Methods
39
+ // ============================================================================
40
+ constructor(periods = []) {
41
+ this._periods = [...periods];
42
+ }
43
+ /**
44
+ * Create a new collection from periods
45
+ */
46
+ static create(...periods) {
47
+ return new ChronosPeriodCollection(periods);
48
+ }
49
+ /**
50
+ * Create an empty collection
51
+ */
52
+ static empty() {
53
+ return new ChronosPeriodCollection();
54
+ }
55
+ /**
56
+ * Create a collection from an array of date pairs
57
+ */
58
+ static fromDatePairs(pairs) {
59
+ const periods = pairs.map(([start, end]) => period_1.ChronosPeriod.create(start, end));
60
+ return new ChronosPeriodCollection(periods);
61
+ }
62
+ // ============================================================================
63
+ // Basic Operations
64
+ // ============================================================================
65
+ /** Add a period to the collection */
66
+ add(period) {
67
+ this._periods.push(period);
68
+ return this;
69
+ }
70
+ /** Add multiple periods */
71
+ addAll(periods) {
72
+ this._periods.push(...periods);
73
+ return this;
74
+ }
75
+ /** Get a shallow copy of periods */
76
+ toArray() {
77
+ return [...this._periods];
78
+ }
79
+ /** Get the number of periods in the collection */
80
+ get length() {
81
+ return this._periods.length;
82
+ }
83
+ /** Check if collection is empty */
84
+ isEmpty() {
85
+ return this._periods.length === 0;
86
+ }
87
+ /** Check if collection is not empty */
88
+ isNotEmpty() {
89
+ return this._periods.length > 0;
90
+ }
91
+ /** Get a period at a specific index */
92
+ get(index) {
93
+ return this._periods[index];
94
+ }
95
+ /** Get the first period */
96
+ first() {
97
+ return this._periods[0];
98
+ }
99
+ /** Get the last period */
100
+ last() {
101
+ return this._periods[this._periods.length - 1];
102
+ }
103
+ /** Clear collection */
104
+ clear() {
105
+ this._periods = [];
106
+ return this;
107
+ }
108
+ // ============================================================================
109
+ // Iteration
110
+ // ============================================================================
111
+ /** Iterator implementation */
112
+ *[Symbol.iterator]() {
113
+ for (const period of this._periods) {
114
+ yield period;
115
+ }
116
+ }
117
+ /** ForEach iteration */
118
+ forEach(callback) {
119
+ this._periods.forEach(callback);
120
+ }
121
+ /** Map periods to a new array */
122
+ map(callback) {
123
+ return this._periods.map(callback);
124
+ }
125
+ /** Filter periods */
126
+ filter(predicate) {
127
+ return new ChronosPeriodCollection(this._periods.filter(predicate));
128
+ }
129
+ /** Reduce periods to a single value */
130
+ reduce(callback, initial) {
131
+ return this._periods.reduce(callback, initial);
132
+ }
133
+ /** Find a period matching a predicate */
134
+ find(predicate) {
135
+ return this._periods.find(predicate);
136
+ }
137
+ /** Check if any period matches a predicate */
138
+ some(predicate) {
139
+ return this._periods.some(predicate);
140
+ }
141
+ /** Check if all periods match a predicate */
142
+ every(predicate) {
143
+ return this._periods.every(predicate);
144
+ }
145
+ // ============================================================================
146
+ // Boundaries (spatie/period inspired)
147
+ // ============================================================================
148
+ /**
149
+ * Get the overall boundaries of the collection
150
+ * Returns a period from the earliest start to the latest end
151
+ */
152
+ boundaries() {
153
+ var _a;
154
+ if (this._periods.length === 0)
155
+ return null;
156
+ let earliestStart = null;
157
+ let latestEnd = null;
158
+ for (const period of this._periods) {
159
+ if (!earliestStart || period.start.isBefore(earliestStart)) {
160
+ earliestStart = period.start;
161
+ }
162
+ const end = (_a = period.end) !== null && _a !== void 0 ? _a : period.last();
163
+ if (end && (!latestEnd || end.isAfter(latestEnd))) {
164
+ latestEnd = end;
165
+ }
166
+ }
167
+ if (!earliestStart)
168
+ return null;
169
+ return period_1.ChronosPeriod.create(earliestStart, latestEnd !== null && latestEnd !== void 0 ? latestEnd : earliestStart);
170
+ }
171
+ /**
172
+ * Get the earliest start date across all periods
173
+ */
174
+ start() {
175
+ if (this._periods.length === 0)
176
+ return null;
177
+ let earliest = null;
178
+ for (const period of this._periods) {
179
+ if (!earliest || period.start.isBefore(earliest)) {
180
+ earliest = period.start;
181
+ }
182
+ }
183
+ return earliest;
184
+ }
185
+ /**
186
+ * Get the latest end date across all periods
187
+ */
188
+ end() {
189
+ var _a;
190
+ if (this._periods.length === 0)
191
+ return null;
192
+ let latest = null;
193
+ for (const period of this._periods) {
194
+ const end = (_a = period.end) !== null && _a !== void 0 ? _a : period.last();
195
+ if (end && (!latest || end.isAfter(latest))) {
196
+ latest = end;
197
+ }
198
+ }
199
+ return latest;
200
+ }
201
+ // ============================================================================
202
+ // Overlap Operations (spatie/period inspired)
203
+ // ============================================================================
204
+ /** Normalize and merge overlapping/adjacent periods */
205
+ normalize() {
206
+ if (this._periods.length === 0)
207
+ return [];
208
+ // Sort by start
209
+ const sorted = this._periods.slice().sort((a, b) => {
210
+ const aStart = a.start.toDate().getTime();
211
+ const bStart = b.start.toDate().getTime();
212
+ return aStart - bStart;
213
+ });
214
+ const merged = [];
215
+ let current = sorted[0].clone();
216
+ for (let i = 1; i < sorted.length; i++) {
217
+ const next = sorted[i];
218
+ const union = current.union(next);
219
+ if (union) {
220
+ current = union;
221
+ }
222
+ else {
223
+ merged.push(current);
224
+ current = next.clone();
225
+ }
226
+ }
227
+ merged.push(current);
228
+ return merged;
229
+ }
230
+ /** Check if any period overlaps with the provided period */
231
+ overlaps(period) {
232
+ return this._periods.some((p) => p.overlaps(period));
233
+ }
234
+ /**
235
+ * Check if any period in the collection overlaps with any other period
236
+ * in the collection (internal overlaps)
237
+ */
238
+ overlapAny() {
239
+ for (let i = 0; i < this._periods.length; i++) {
240
+ for (let j = i + 1; j < this._periods.length; j++) {
241
+ if (this._periods[i].overlaps(this._periods[j])) {
242
+ return true;
243
+ }
244
+ }
245
+ }
246
+ return false;
247
+ }
248
+ /**
249
+ * Get all overlapping period segments across the collection
250
+ * Returns periods where two or more periods in the collection overlap
251
+ */
252
+ overlapAll() {
253
+ if (this._periods.length < 2) {
254
+ return ChronosPeriodCollection.empty();
255
+ }
256
+ const overlaps = [];
257
+ // Sort periods by start date
258
+ const sorted = this._periods
259
+ .slice()
260
+ .sort((a, b) => a.start.toDate().getTime() - b.start.toDate().getTime());
261
+ // Find all pairwise intersections
262
+ for (let i = 0; i < sorted.length; i++) {
263
+ for (let j = i + 1; j < sorted.length; j++) {
264
+ const intersection = sorted[i].intersect(sorted[j]);
265
+ if (intersection) {
266
+ // Check if this intersection is not already covered
267
+ const isDuplicate = overlaps.some((existing) => {
268
+ var _a, _b;
269
+ return existing.start.isSame(intersection.start, 'day') &&
270
+ ((_a = existing.end) === null || _a === void 0 ? void 0 : _a.isSame((_b = intersection.end) !== null && _b !== void 0 ? _b : null, 'day'));
271
+ });
272
+ if (!isDuplicate) {
273
+ overlaps.push(intersection);
274
+ }
275
+ }
276
+ }
277
+ }
278
+ return new ChronosPeriodCollection(overlaps);
279
+ }
280
+ /** Return intersections between collection and a given period */
281
+ intersect(period) {
282
+ const intersections = [];
283
+ for (const p of this._periods) {
284
+ const inter = p.intersect(period);
285
+ if (inter)
286
+ intersections.push(inter);
287
+ }
288
+ return new ChronosPeriodCollection(intersections);
289
+ }
290
+ /**
291
+ * Get intersection of all periods in the collection
292
+ * Returns the period where ALL periods overlap (if any)
293
+ */
294
+ intersectAll() {
295
+ if (this._periods.length === 0)
296
+ return null;
297
+ if (this._periods.length === 1)
298
+ return this._periods[0].clone();
299
+ let result = this._periods[0].clone();
300
+ for (let i = 1; i < this._periods.length; i++) {
301
+ if (!result)
302
+ return null;
303
+ result = result.intersect(this._periods[i]);
304
+ }
305
+ return result;
306
+ }
307
+ // ============================================================================
308
+ // Union Operations
309
+ // ============================================================================
310
+ /** Return the union (merged) of all periods in the collection */
311
+ union() {
312
+ return new ChronosPeriodCollection(this.normalize());
313
+ }
314
+ /**
315
+ * Alias for normalize() - returns merged periods
316
+ * @deprecated Use union() instead
317
+ */
318
+ unionAll() {
319
+ return this.normalize();
320
+ }
321
+ /** Merge collection into a single union period if contiguous/overlapping */
322
+ mergeToSingle() {
323
+ const merged = this.normalize();
324
+ if (merged.length === 0)
325
+ return null;
326
+ if (merged.length === 1)
327
+ return merged[0];
328
+ // If there are multiple, they are not adjacent/overlapping, cannot merge into single
329
+ return null;
330
+ }
331
+ // ============================================================================
332
+ // Gap Operations (spatie/period inspired)
333
+ // ============================================================================
334
+ /** Return gaps between merged periods */
335
+ gaps() {
336
+ var _a;
337
+ const merged = this.normalize();
338
+ const gaps = [];
339
+ for (let i = 0; i < merged.length - 1; i++) {
340
+ const current = merged[i];
341
+ const next = merged[i + 1];
342
+ const end = (_a = current.end) !== null && _a !== void 0 ? _a : current.last();
343
+ const startNext = next.start;
344
+ if (end && startNext) {
345
+ const gapStart = end.add(current.interval.toDuration());
346
+ const gapEnd = startNext.subtract(next.interval.toDuration());
347
+ // Only create gap if there's actual space between periods
348
+ if (gapStart.isSameOrBefore(gapEnd)) {
349
+ gaps.push(period_1.ChronosPeriod.create(gapStart, gapEnd, current.interval));
350
+ }
351
+ }
352
+ }
353
+ return new ChronosPeriodCollection(gaps);
354
+ }
355
+ /**
356
+ * Check if there are any gaps between periods
357
+ */
358
+ hasGaps() {
359
+ return this.gaps().isNotEmpty();
360
+ }
361
+ // ============================================================================
362
+ // Subtraction Operations (spatie/period inspired)
363
+ // ============================================================================
364
+ /**
365
+ * Subtract a period from all periods in the collection
366
+ * Returns periods with the subtracted portion removed
367
+ */
368
+ subtract(period) {
369
+ const results = [];
370
+ for (const p of this._periods) {
371
+ const diffs = p.diff(period);
372
+ results.push(...diffs);
373
+ }
374
+ return new ChronosPeriodCollection(results);
375
+ }
376
+ /**
377
+ * Subtract multiple periods from the collection
378
+ */
379
+ subtractAll(periods) {
380
+ let result = new ChronosPeriodCollection([...this._periods]);
381
+ for (const period of periods) {
382
+ result = result.subtract(period);
383
+ }
384
+ return result;
385
+ }
386
+ // ============================================================================
387
+ // Touching/Adjacent Operations (spatie/period inspired)
388
+ // ============================================================================
389
+ /**
390
+ * Check if any period touches (is adjacent to) the given period
391
+ * Two periods touch if one ends exactly where the other begins
392
+ */
393
+ touchesWith(period) {
394
+ return this._periods.some((p) => this._periodsTouch(p, period));
395
+ }
396
+ /**
397
+ * Check if two periods touch (are adjacent)
398
+ */
399
+ _periodsTouch(a, b) {
400
+ var _a, _b;
401
+ const aEnd = (_a = a.end) !== null && _a !== void 0 ? _a : a.last();
402
+ const bEnd = (_b = b.end) !== null && _b !== void 0 ? _b : b.last();
403
+ if (!aEnd || !bEnd)
404
+ return false;
405
+ // a ends exactly where b starts
406
+ if (aEnd.add(a.interval.toDuration()).isSame(b.start))
407
+ return true;
408
+ // b ends exactly where a starts
409
+ if (bEnd.add(b.interval.toDuration()).isSame(a.start))
410
+ return true;
411
+ return false;
412
+ }
413
+ /**
414
+ * Get all periods that touch the given period
415
+ */
416
+ touchingPeriods(period) {
417
+ return this.filter((p) => this._periodsTouch(p, period));
418
+ }
419
+ // ============================================================================
420
+ // Contains Operations (spatie/period inspired)
421
+ // ============================================================================
422
+ /**
423
+ * Check if a date is contained in any period of the collection
424
+ */
425
+ contains(date) {
426
+ const target = chronos_1.Chronos.parse(date);
427
+ return this._periods.some((p) => p.contains(target));
428
+ }
429
+ /**
430
+ * Check if a period is fully contained in any period of the collection
431
+ */
432
+ containsPeriod(period) {
433
+ return this._periods.some((p) => {
434
+ var _a, _b;
435
+ const pEnd = (_a = p.end) !== null && _a !== void 0 ? _a : p.last();
436
+ const periodEnd = (_b = period.end) !== null && _b !== void 0 ? _b : period.last();
437
+ if (!pEnd || !periodEnd)
438
+ return false;
439
+ return (p.start.isSameOrBefore(period.start) && pEnd.isSameOrAfter(periodEnd));
440
+ });
441
+ }
442
+ // ============================================================================
443
+ // Equality Operations (spatie/period inspired)
444
+ // ============================================================================
445
+ /**
446
+ * Check if two collections are equal (same periods)
447
+ */
448
+ equals(other) {
449
+ var _a, _b;
450
+ if (this._periods.length !== other._periods.length) {
451
+ return false;
452
+ }
453
+ const thisSorted = this._sortedByStart();
454
+ const otherSorted = other._sortedByStart();
455
+ for (let i = 0; i < thisSorted.length; i++) {
456
+ const thisEnd = (_a = thisSorted[i].end) !== null && _a !== void 0 ? _a : thisSorted[i].last();
457
+ const otherEnd = (_b = otherSorted[i].end) !== null && _b !== void 0 ? _b : otherSorted[i].last();
458
+ if (!thisSorted[i].start.isSame(otherSorted[i].start, 'day')) {
459
+ return false;
460
+ }
461
+ if (thisEnd && otherEnd && !thisEnd.isSame(otherEnd, 'day')) {
462
+ return false;
463
+ }
464
+ }
465
+ return true;
466
+ }
467
+ /**
468
+ * Get periods sorted by start date
469
+ */
470
+ _sortedByStart() {
471
+ return this._periods
472
+ .slice()
473
+ .sort((a, b) => a.start.toDate().getTime() - b.start.toDate().getTime());
474
+ }
475
+ // ============================================================================
476
+ // Sorting & Reversing
477
+ // ============================================================================
478
+ /**
479
+ * Sort periods by start date (ascending)
480
+ */
481
+ sortByStart() {
482
+ return new ChronosPeriodCollection(this._sortedByStart());
483
+ }
484
+ /**
485
+ * Sort periods by end date (ascending)
486
+ */
487
+ sortByEnd() {
488
+ const sorted = this._periods.slice().sort((a, b) => {
489
+ var _a, _b, _c, _d, _e, _f;
490
+ const aEnd = (_c = (_b = ((_a = a.end) !== null && _a !== void 0 ? _a : a.last())) === null || _b === void 0 ? void 0 : _b.toDate().getTime()) !== null && _c !== void 0 ? _c : 0;
491
+ const bEnd = (_f = (_e = ((_d = b.end) !== null && _d !== void 0 ? _d : b.last())) === null || _e === void 0 ? void 0 : _e.toDate().getTime()) !== null && _f !== void 0 ? _f : 0;
492
+ return aEnd - bEnd;
493
+ });
494
+ return new ChronosPeriodCollection(sorted);
495
+ }
496
+ /**
497
+ * Sort periods by duration (ascending)
498
+ */
499
+ sortByDuration() {
500
+ const sorted = this._periods.slice().sort((a, b) => {
501
+ try {
502
+ return a.days() - b.days();
503
+ }
504
+ catch (_a) {
505
+ return 0;
506
+ }
507
+ });
508
+ return new ChronosPeriodCollection(sorted);
509
+ }
510
+ /**
511
+ * Reverse the order of periods
512
+ */
513
+ reverse() {
514
+ return new ChronosPeriodCollection([...this._periods].reverse());
515
+ }
516
+ // ============================================================================
517
+ // Conversion & Output
518
+ // ============================================================================
519
+ /**
520
+ * Convert to JSON
521
+ */
522
+ toJSON() {
523
+ return this._periods.map((p) => p.toJSON());
524
+ }
525
+ /**
526
+ * Convert to string
527
+ */
528
+ toString() {
529
+ if (this._periods.length === 0)
530
+ return '(empty collection)';
531
+ return this._periods.map((p) => p.toString()).join(', ');
532
+ }
533
+ /**
534
+ * Get total duration across all periods (in days)
535
+ * Note: Overlapping portions may be counted multiple times
536
+ */
537
+ totalDays() {
538
+ return this._periods.reduce((sum, p) => {
539
+ try {
540
+ return sum + p.days();
541
+ }
542
+ catch (_a) {
543
+ return sum;
544
+ }
545
+ }, 0);
546
+ }
547
+ /**
548
+ * Get total unique duration (merged periods, no double-counting)
549
+ */
550
+ uniqueDays() {
551
+ const merged = this.normalize();
552
+ return merged.reduce((sum, p) => {
553
+ try {
554
+ return sum + p.days();
555
+ }
556
+ catch (_a) {
557
+ return sum;
558
+ }
559
+ }, 0);
560
+ }
561
+ }
562
+ exports.ChronosPeriodCollection = ChronosPeriodCollection;