chronos-ts 2.0.0 → 2.0.2

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.
@@ -322,6 +322,7 @@ export declare class ChronosPeriod implements Iterable<Chronos> {
322
322
  union(other: ChronosPeriod): ChronosPeriod | null;
323
323
  /**
324
324
  * Get the difference between two periods
325
+ * Returns the parts of this period that don't overlap with the other period
325
326
  */
326
327
  diff(other: ChronosPeriod): ChronosPeriod[];
327
328
  /**
@@ -55,6 +55,32 @@ class ChronosPeriod {
55
55
  interval instanceof interval_1.ChronosInterval
56
56
  ? interval
57
57
  : interval_1.ChronosInterval.create(interval || { days: 1 });
58
+ // Validate interval is not zero to prevent infinite loops
59
+ if (this._interval.isZero()) {
60
+ throw new Error('ChronosPeriod: Interval cannot be zero');
61
+ }
62
+ // Validate interval is not negative
63
+ if (this._interval.isNegative()) {
64
+ throw new Error('ChronosPeriod: Interval cannot be negative');
65
+ }
66
+ // Warn about potentially large iterations (optional safety check)
67
+ if (this._end !== null) {
68
+ const durationMs = Math.abs(this._end.valueOf() - this._start.valueOf());
69
+ const intervalMs = Math.abs(this._interval.totalMilliseconds());
70
+ if (intervalMs > 0) {
71
+ const estimatedIterations = durationMs / intervalMs;
72
+ // Warn if period would generate more than 1 million iterations
73
+ if (estimatedIterations > 1000000) {
74
+ console.warn(`ChronosPeriod: Large number of iterations detected (~${Math.floor(estimatedIterations).toLocaleString()}). ` +
75
+ `This may cause performance issues. Consider using a larger interval or setting a recurrence limit.`);
76
+ }
77
+ // Hard limit: throw error if more than 10 million iterations
78
+ if (estimatedIterations > 10000000) {
79
+ throw new Error(`ChronosPeriod: Period would generate ~${Math.floor(estimatedIterations).toLocaleString()} iterations, ` +
80
+ `which exceeds the safety limit of 10 million. Use a larger interval or set explicit recurrence limits.`);
81
+ }
82
+ }
83
+ }
58
84
  this._recurrences = null;
59
85
  this._options = {
60
86
  excludeStart: (_a = options.excludeStart) !== null && _a !== void 0 ? _a : false,
@@ -372,10 +398,18 @@ class ChronosPeriod {
372
398
  */
373
399
  setInterval(interval) {
374
400
  const period = this._cloneForModification();
375
- period._interval =
376
- interval instanceof interval_1.ChronosInterval
377
- ? interval
378
- : interval_1.ChronosInterval.create(interval);
401
+ const newInterval = interval instanceof interval_1.ChronosInterval
402
+ ? interval
403
+ : interval_1.ChronosInterval.create(interval);
404
+ // Validate interval is not zero to prevent infinite loops
405
+ if (newInterval.isZero()) {
406
+ throw new Error('ChronosPeriod: Interval cannot be zero');
407
+ }
408
+ // Validate interval is not negative
409
+ if (newInterval.isNegative()) {
410
+ throw new Error('ChronosPeriod: Interval cannot be negative');
411
+ }
412
+ period._interval = newInterval;
379
413
  return period;
380
414
  }
381
415
  /**
@@ -390,6 +424,10 @@ class ChronosPeriod {
390
424
  * Set interval by unit
391
425
  */
392
426
  every(amount, unit) {
427
+ // Validate amount is positive
428
+ if (amount <= 0) {
429
+ throw new Error('ChronosPeriod: Amount must be positive');
430
+ }
393
431
  const normalizedUnit = (0, utils_1.normalizeUnit)(unit);
394
432
  const duration = {};
395
433
  switch (normalizedUnit) {
@@ -607,8 +645,11 @@ class ChronosPeriod {
607
645
  * Get the last date in the period
608
646
  */
609
647
  last() {
610
- const array = this.toArray();
611
- return array.length > 0 ? array[array.length - 1] : null;
648
+ let lastDate = null;
649
+ for (const date of this) {
650
+ lastDate = date;
651
+ }
652
+ return lastDate;
612
653
  }
613
654
  /**
614
655
  * Get a date at a specific index
@@ -628,7 +669,12 @@ class ChronosPeriod {
628
669
  */
629
670
  contains(date) {
630
671
  const target = chronos_1.Chronos.parse(date);
631
- return this.toArray().some((d) => d.isSame(target, 'day'));
672
+ for (const d of this) {
673
+ if (d.isSame(target, 'day')) {
674
+ return true;
675
+ }
676
+ }
677
+ return false;
632
678
  }
633
679
  /**
634
680
  * For each iteration
@@ -720,6 +766,7 @@ class ChronosPeriod {
720
766
  }
721
767
  /**
722
768
  * Get the difference between two periods
769
+ * Returns the parts of this period that don't overlap with the other period
723
770
  */
724
771
  diff(other) {
725
772
  var _a, _b;
@@ -728,14 +775,14 @@ class ChronosPeriod {
728
775
  }
729
776
  const results = [];
730
777
  const thisEnd = (_a = this._end) !== null && _a !== void 0 ? _a : this.last();
731
- // Before the other period starts
778
+ const otherEnd = (_b = other._end) !== null && _b !== void 0 ? _b : other.last();
779
+ // Before the other period starts - create gap from this start to other start
732
780
  if (this._start.isBefore(other._start)) {
733
- results.push(new ChronosPeriod(this._start, other._start.subtract(this._interval.toDuration()), this._interval));
781
+ results.push(new ChronosPeriod(this._start, other._start, this._interval));
734
782
  }
735
- // After the other period ends
736
- const otherEnd = (_b = other._end) !== null && _b !== void 0 ? _b : other.last();
783
+ // After the other period ends - create gap from other end to this end
737
784
  if (otherEnd && thisEnd && thisEnd.isAfter(otherEnd)) {
738
- results.push(new ChronosPeriod(otherEnd.add(this._interval.toDuration()), thisEnd, this._interval));
785
+ results.push(new ChronosPeriod(otherEnd, thisEnd, this._interval));
739
786
  }
740
787
  return results;
741
788
  }
@@ -832,6 +879,14 @@ class ChronosPeriod {
832
879
  const splitInterval = interval instanceof interval_1.ChronosInterval
833
880
  ? interval
834
881
  : interval_1.ChronosInterval.create(interval);
882
+ // Validate that the interval is not zero
883
+ if (splitInterval.isZero()) {
884
+ throw new Error('Cannot split by zero interval');
885
+ }
886
+ // Validate that the interval is positive
887
+ if (splitInterval.isNegative()) {
888
+ throw new Error('Cannot split by negative interval');
889
+ }
835
890
  const chunks = [];
836
891
  let current = this._start.clone();
837
892
  while (current.isSameOrBefore(this._end)) {
@@ -101,6 +101,7 @@ export type TimezoneId = keyof typeof TIMEZONES | string;
101
101
  export declare class ChronosTimezone {
102
102
  private _identifier;
103
103
  private _originalOffset;
104
+ private _extraMinutes;
104
105
  private _cachedOffset;
105
106
  private _cachedDate;
106
107
  /**
@@ -120,11 +120,13 @@ class ChronosTimezone {
120
120
  */
121
121
  constructor(identifier = 'UTC') {
122
122
  this._originalOffset = null;
123
+ this._extraMinutes = 0; // For non-whole-hour offsets like +05:30
123
124
  this._cachedOffset = null;
124
125
  this._cachedDate = null;
125
126
  const normalized = this._normalizeIdentifier(identifier);
126
127
  this._identifier = normalized.identifier;
127
128
  this._originalOffset = normalized.originalOffset;
129
+ this._extraMinutes = normalized.extraMinutes;
128
130
  }
129
131
  /**
130
132
  * Normalize timezone identifier
@@ -133,16 +135,21 @@ class ChronosTimezone {
133
135
  // Handle UTC aliases
134
136
  if (identifier.toUpperCase() === 'Z' ||
135
137
  identifier.toUpperCase() === 'GMT') {
136
- return { identifier: 'UTC', originalOffset: null };
138
+ return { identifier: 'UTC', originalOffset: null, extraMinutes: 0 };
137
139
  }
138
140
  // Handle offset strings like +05:30, -08:00
139
141
  if (/^[+-]\d{2}:\d{2}$/.test(identifier)) {
140
142
  // Store original offset and convert to Etc/GMT for internal use
141
143
  const offsetHours = this._parseOffsetString(identifier);
142
- const etcGmt = `Etc/GMT${offsetHours >= 0 ? '-' : '+'}${Math.abs(Math.floor(offsetHours))}`;
143
- return { identifier: etcGmt, originalOffset: identifier };
144
+ const sign = offsetHours >= 0 ? 1 : -1;
145
+ const absHours = Math.abs(offsetHours);
146
+ const wholeHours = Math.floor(absHours);
147
+ // Calculate extra minutes for non-whole-hour offsets (e.g., +05:30 has 30 extra minutes)
148
+ const extraMinutes = Math.round((absHours - wholeHours) * 60) * sign;
149
+ const etcGmt = `Etc/GMT${offsetHours >= 0 ? '-' : '+'}${wholeHours}`;
150
+ return { identifier: etcGmt, originalOffset: identifier, extraMinutes };
144
151
  }
145
- return { identifier, originalOffset: null };
152
+ return { identifier, originalOffset: null, extraMinutes: 0 };
146
153
  }
147
154
  /**
148
155
  * Parse offset string to hours
@@ -286,10 +293,11 @@ class ChronosTimezone {
286
293
  const tzParts = this._parseIntlParts(tzFormatter.formatToParts(date));
287
294
  const utcDate = new Date(Date.UTC(utcParts.year, utcParts.month - 1, utcParts.day, utcParts.hour, utcParts.minute));
288
295
  const tzDate = new Date(Date.UTC(tzParts.year, tzParts.month - 1, tzParts.day, tzParts.hour, tzParts.minute));
289
- return (tzDate.getTime() - utcDate.getTime()) / 60000;
296
+ // Add extra minutes for non-whole-hour offsets (e.g., +05:30)
297
+ return ((tzDate.getTime() - utcDate.getTime()) / 60000 + this._extraMinutes);
290
298
  }
291
299
  catch (_a) {
292
- return 0;
300
+ return this._extraMinutes;
293
301
  }
294
302
  }
295
303
  /**
@@ -158,7 +158,8 @@ const UNIT_ALIASES = {
158
158
  */
159
159
  function normalizeUnit(unit) {
160
160
  var _a;
161
- const normalized = (_a = UNIT_ALIASES[unit.toLowerCase()]) !== null && _a !== void 0 ? _a : UNIT_ALIASES[unit];
161
+ // Check original case first for case-sensitive short codes (M vs m, etc.)
162
+ const normalized = (_a = UNIT_ALIASES[unit]) !== null && _a !== void 0 ? _a : UNIT_ALIASES[unit.toLowerCase()];
162
163
  if (!normalized) {
163
164
  throw new Error(`Invalid time unit: ${unit}`);
164
165
  }
@@ -237,7 +238,8 @@ function getDaysInYear(year) {
237
238
  * Get the day of year (1-366)
238
239
  */
239
240
  function getDayOfYear(date) {
240
- const start = new Date(Date.UTC(date.getFullYear(), 0, 0));
241
+ // Use local time consistently (not UTC) to avoid timezone issues
242
+ const start = new Date(date.getFullYear(), 0, 0);
241
243
  const diff = date.getTime() - start.getTime();
242
244
  return Math.floor(diff / types_1.MILLISECONDS_PER_DAY);
243
245
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chronos-ts",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "A comprehensive TypeScript library for date and time manipulation, inspired by Carbon PHP. Features immutable API, intervals, periods, timezones, and i18n support.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",