chronos-ts 2.0.3 → 2.0.4

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.mts CHANGED
@@ -630,6 +630,13 @@ declare class Chronos implements ChronosLike {
630
630
  * ```
631
631
  */
632
632
  add(amount: number | Duration, unit?: AnyTimeUnit): Chronos;
633
+ /**
634
+ * Add an amount of a single unit while respecting the instance timezone.
635
+ * Calendar units (day/week/month/quarter/year/…) preserve the wall-clock time
636
+ * in the zone (DST-safe); absolute units (hour/minute/second/millisecond) add a
637
+ * fixed offset to the instant. Day clamping mirrors the local-time path.
638
+ */
639
+ private addInZone;
633
640
  /**
634
641
  * Subtract time from the date
635
642
  */
@@ -656,6 +663,14 @@ declare class Chronos implements ChronosLike {
656
663
  * ```
657
664
  */
658
665
  endOf(unit: AnyTimeUnit): Chronos;
666
+ /**
667
+ * Compute the start/end boundary of a unit in the instance timezone.
668
+ * The plain `startOf`/`endOf` helpers operate on local wall-clock fields of the
669
+ * underlying Date, which is wrong when an explicit timezone is attached. Here we
670
+ * read the wall-clock components in the target zone, snap them to the boundary,
671
+ * and rebuild the instant (dateFromComponents handles DST).
672
+ */
673
+ private boundaryInZone;
659
674
  addMilliseconds(amount: number): Chronos;
660
675
  addSeconds(amount: number): Chronos;
661
676
  addMinutes(amount: number): Chronos;
@@ -1216,7 +1231,6 @@ declare class ChronosPeriod implements Iterable<Chronos> {
1216
1231
  private _recurrences;
1217
1232
  private _options;
1218
1233
  private _filters;
1219
- private _current;
1220
1234
  private _locale;
1221
1235
  /**
1222
1236
  * Create a new ChronosPeriod
@@ -1484,6 +1498,12 @@ declare class ChronosPeriod implements Iterable<Chronos> {
1484
1498
  * Reduce dates to a single value
1485
1499
  */
1486
1500
  reduce<T>(callback: (acc: T, date: Chronos, index: number) => T, initial: T): T;
1501
+ /**
1502
+ * Resolve the effective end of the period.
1503
+ * Returns null for a truly unbounded period (no end and no recurrence limit),
1504
+ * which avoids calling last() — that would iterate until the safety limit and throw.
1505
+ */
1506
+ private _resolvedEnd;
1487
1507
  /**
1488
1508
  * Check if two periods overlap
1489
1509
  */
package/dist/index.d.ts CHANGED
@@ -630,6 +630,13 @@ declare class Chronos implements ChronosLike {
630
630
  * ```
631
631
  */
632
632
  add(amount: number | Duration, unit?: AnyTimeUnit): Chronos;
633
+ /**
634
+ * Add an amount of a single unit while respecting the instance timezone.
635
+ * Calendar units (day/week/month/quarter/year/…) preserve the wall-clock time
636
+ * in the zone (DST-safe); absolute units (hour/minute/second/millisecond) add a
637
+ * fixed offset to the instant. Day clamping mirrors the local-time path.
638
+ */
639
+ private addInZone;
633
640
  /**
634
641
  * Subtract time from the date
635
642
  */
@@ -656,6 +663,14 @@ declare class Chronos implements ChronosLike {
656
663
  * ```
657
664
  */
658
665
  endOf(unit: AnyTimeUnit): Chronos;
666
+ /**
667
+ * Compute the start/end boundary of a unit in the instance timezone.
668
+ * The plain `startOf`/`endOf` helpers operate on local wall-clock fields of the
669
+ * underlying Date, which is wrong when an explicit timezone is attached. Here we
670
+ * read the wall-clock components in the target zone, snap them to the boundary,
671
+ * and rebuild the instant (dateFromComponents handles DST).
672
+ */
673
+ private boundaryInZone;
659
674
  addMilliseconds(amount: number): Chronos;
660
675
  addSeconds(amount: number): Chronos;
661
676
  addMinutes(amount: number): Chronos;
@@ -1216,7 +1231,6 @@ declare class ChronosPeriod implements Iterable<Chronos> {
1216
1231
  private _recurrences;
1217
1232
  private _options;
1218
1233
  private _filters;
1219
- private _current;
1220
1234
  private _locale;
1221
1235
  /**
1222
1236
  * Create a new ChronosPeriod
@@ -1484,6 +1498,12 @@ declare class ChronosPeriod implements Iterable<Chronos> {
1484
1498
  * Reduce dates to a single value
1485
1499
  */
1486
1500
  reduce<T>(callback: (acc: T, date: Chronos, index: number) => T, initial: T): T;
1501
+ /**
1502
+ * Resolve the effective end of the period.
1503
+ * Returns null for a truly unbounded period (no end and no recurrence limit),
1504
+ * which avoids calling last() — that would iterate until the safety limit and throw.
1505
+ */
1506
+ private _resolvedEnd;
1487
1507
  /**
1488
1508
  * Check if two periods overlap
1489
1509
  */
package/dist/index.js CHANGED
@@ -2017,18 +2017,14 @@ var Chronos = class _Chronos {
2017
2017
  if (isValidDate(isoDate)) {
2018
2018
  return isoDate;
2019
2019
  }
2020
- const formats = [
2021
- /^(\d{4})-(\d{2})-(\d{2})$/,
2022
- /^(\d{2})\/(\d{2})\/(\d{4})$/,
2023
- /^(\d{4})\/(\d{2})\/(\d{2})$/
2024
- ];
2025
- for (const format of formats) {
2026
- const match = input.match(format);
2027
- if (match) {
2028
- const parsed = new Date(input);
2029
- if (isValidDate(parsed)) {
2030
- return parsed;
2031
- }
2020
+ const dmy = input.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
2021
+ if (dmy) {
2022
+ const day = parseInt(dmy[1], 10);
2023
+ const month = parseInt(dmy[2], 10);
2024
+ const year = parseInt(dmy[3], 10);
2025
+ const parsed = new Date(year, month - 1, day);
2026
+ if (isValidDate(parsed) && parsed.getMonth() === month - 1) {
2027
+ return parsed;
2032
2028
  }
2033
2029
  }
2034
2030
  throw new Error(`Unable to parse date: ${input}`);
@@ -2332,18 +2328,40 @@ var Chronos = class _Chronos {
2332
2328
  }
2333
2329
  /** Get the quarter (1-4) */
2334
2330
  get quarter() {
2331
+ if (this._timezone) {
2332
+ const month = new ChronosTimezone(this._timezone).getComponents(
2333
+ this._date
2334
+ ).month;
2335
+ return Math.floor((month - 1) / 3) + 1;
2336
+ }
2335
2337
  return getQuarter(this._date);
2336
2338
  }
2337
2339
  /** Get the day of year (1-366) */
2338
2340
  get dayOfYear() {
2341
+ if (this._timezone) {
2342
+ const c = new ChronosTimezone(this._timezone).getComponents(this._date);
2343
+ let days = c.day;
2344
+ for (let m = 1; m < c.month; m++) {
2345
+ days += getDaysInMonth(c.year, m - 1);
2346
+ }
2347
+ return days;
2348
+ }
2339
2349
  return getDayOfYear(this._date);
2340
2350
  }
2341
2351
  /** Get the ISO week number (1-53) */
2342
2352
  get week() {
2353
+ if (this._timezone) {
2354
+ const c = new ChronosTimezone(this._timezone).getComponents(this._date);
2355
+ return getISOWeek(new Date(c.year, c.month - 1, c.day));
2356
+ }
2343
2357
  return getISOWeek(this._date);
2344
2358
  }
2345
2359
  /** Get the ISO week year */
2346
2360
  get weekYear() {
2361
+ if (this._timezone) {
2362
+ const c = new ChronosTimezone(this._timezone).getComponents(this._date);
2363
+ return getISOWeekYear(new Date(c.year, c.month - 1, c.day));
2364
+ }
2347
2365
  return getISOWeekYear(this._date);
2348
2366
  }
2349
2367
  /** Get the number of days in the current month */
@@ -2364,6 +2382,9 @@ var Chronos = class _Chronos {
2364
2382
  }
2365
2383
  /** Get the timezone offset in minutes */
2366
2384
  get offset() {
2385
+ if (this._timezone) {
2386
+ return -new ChronosTimezone(this._timezone).getOffsetMinutes(this._date);
2387
+ }
2367
2388
  return this._date.getTimezoneOffset();
2368
2389
  }
2369
2390
  /** Get the timezone offset as string (+05:30) */
@@ -2475,9 +2496,82 @@ var Chronos = class _Chronos {
2475
2496
  throw new Error("Unit is required when amount is a number");
2476
2497
  }
2477
2498
  const normalizedUnit = normalizeUnit(unit);
2499
+ if (this._timezone) {
2500
+ return this.addInZone(amount, normalizedUnit);
2501
+ }
2478
2502
  const newDate = addUnits(this._date, amount, normalizedUnit);
2479
2503
  return new _Chronos(newDate, this._timezone);
2480
2504
  }
2505
+ /**
2506
+ * Add an amount of a single unit while respecting the instance timezone.
2507
+ * Calendar units (day/week/month/quarter/year/…) preserve the wall-clock time
2508
+ * in the zone (DST-safe); absolute units (hour/minute/second/millisecond) add a
2509
+ * fixed offset to the instant. Day clamping mirrors the local-time path.
2510
+ */
2511
+ addInZone(amount, unit) {
2512
+ const absoluteMs = {
2513
+ millisecond: 1,
2514
+ second: MILLISECONDS_PER_SECOND,
2515
+ minute: MILLISECONDS_PER_MINUTE,
2516
+ hour: MILLISECONDS_PER_HOUR
2517
+ };
2518
+ const perUnit = absoluteMs[unit];
2519
+ if (perUnit !== void 0) {
2520
+ const newDate = new Date(this._date.getTime() + amount * perUnit);
2521
+ return new _Chronos(newDate, this._timezone);
2522
+ }
2523
+ const tz = new ChronosTimezone(this._timezone);
2524
+ const c = tz.getComponents(this._date);
2525
+ let year = c.year;
2526
+ let month = c.month;
2527
+ let day = c.day;
2528
+ const calendarUnit = unit === "month" || unit === "quarter" || unit === "year" || unit === "decade" || unit === "century" || unit === "millennium";
2529
+ switch (unit) {
2530
+ case "day":
2531
+ day += amount;
2532
+ break;
2533
+ case "week":
2534
+ day += amount * 7;
2535
+ break;
2536
+ case "month":
2537
+ month += amount;
2538
+ break;
2539
+ case "quarter":
2540
+ month += amount * 3;
2541
+ break;
2542
+ case "year":
2543
+ year += amount;
2544
+ break;
2545
+ case "decade":
2546
+ year += amount * 10;
2547
+ break;
2548
+ case "century":
2549
+ year += amount * 100;
2550
+ break;
2551
+ case "millennium":
2552
+ year += amount * 1e3;
2553
+ break;
2554
+ }
2555
+ year += Math.floor((month - 1) / 12);
2556
+ month = ((month - 1) % 12 + 12) % 12 + 1;
2557
+ if (calendarUnit) {
2558
+ const maxDay = getDaysInMonth(year, month - 1);
2559
+ if (day > maxDay) day = maxDay;
2560
+ }
2561
+ const date = _Chronos.dateFromComponents(
2562
+ {
2563
+ year,
2564
+ month,
2565
+ day,
2566
+ hour: c.hour,
2567
+ minute: c.minute,
2568
+ second: c.second,
2569
+ millisecond: this._date.getMilliseconds()
2570
+ },
2571
+ this._timezone
2572
+ );
2573
+ return new _Chronos(date, this._timezone);
2574
+ }
2481
2575
  /**
2482
2576
  * Subtract time from the date
2483
2577
  */
@@ -2505,6 +2599,9 @@ var Chronos = class _Chronos {
2505
2599
  */
2506
2600
  startOf(unit) {
2507
2601
  const normalizedUnit = normalizeUnit(unit);
2602
+ if (this._timezone) {
2603
+ return this.boundaryInZone(normalizedUnit, false);
2604
+ }
2508
2605
  const newDate = startOf(this._date, normalizedUnit);
2509
2606
  return new _Chronos(newDate, this._timezone);
2510
2607
  }
@@ -2520,9 +2617,96 @@ var Chronos = class _Chronos {
2520
2617
  */
2521
2618
  endOf(unit) {
2522
2619
  const normalizedUnit = normalizeUnit(unit);
2620
+ if (this._timezone) {
2621
+ return this.boundaryInZone(normalizedUnit, true);
2622
+ }
2523
2623
  const newDate = endOf(this._date, normalizedUnit);
2524
2624
  return new _Chronos(newDate, this._timezone);
2525
2625
  }
2626
+ /**
2627
+ * Compute the start/end boundary of a unit in the instance timezone.
2628
+ * The plain `startOf`/`endOf` helpers operate on local wall-clock fields of the
2629
+ * underlying Date, which is wrong when an explicit timezone is attached. Here we
2630
+ * read the wall-clock components in the target zone, snap them to the boundary,
2631
+ * and rebuild the instant (dateFromComponents handles DST).
2632
+ */
2633
+ boundaryInZone(unit, end) {
2634
+ if (unit === "millisecond") {
2635
+ return this.clone();
2636
+ }
2637
+ const tz = new ChronosTimezone(this._timezone);
2638
+ const c = tz.getComponents(this._date);
2639
+ const comp = {
2640
+ year: c.year,
2641
+ month: c.month,
2642
+ day: c.day,
2643
+ hour: c.hour,
2644
+ minute: c.minute,
2645
+ second: c.second,
2646
+ millisecond: this._date.getMilliseconds()
2647
+ };
2648
+ const resetTime = () => {
2649
+ comp.hour = end ? 23 : 0;
2650
+ comp.minute = end ? 59 : 0;
2651
+ comp.second = end ? 59 : 0;
2652
+ comp.millisecond = end ? 999 : 0;
2653
+ };
2654
+ switch (unit) {
2655
+ case "second":
2656
+ comp.millisecond = end ? 999 : 0;
2657
+ break;
2658
+ case "minute":
2659
+ comp.second = end ? 59 : 0;
2660
+ comp.millisecond = end ? 999 : 0;
2661
+ break;
2662
+ case "hour":
2663
+ comp.minute = end ? 59 : 0;
2664
+ comp.second = end ? 59 : 0;
2665
+ comp.millisecond = end ? 999 : 0;
2666
+ break;
2667
+ case "day":
2668
+ resetTime();
2669
+ break;
2670
+ case "week":
2671
+ resetTime();
2672
+ comp.day = c.day + (end ? 6 - c.dayOfWeek : -c.dayOfWeek);
2673
+ break;
2674
+ case "month":
2675
+ resetTime();
2676
+ comp.day = end ? getDaysInMonth(c.year, c.month - 1) : 1;
2677
+ break;
2678
+ case "quarter": {
2679
+ resetTime();
2680
+ const qStartMonth = Math.floor((c.month - 1) / 3) * 3 + 1;
2681
+ if (end) {
2682
+ comp.month = qStartMonth + 2;
2683
+ comp.day = getDaysInMonth(c.year, comp.month - 1);
2684
+ } else {
2685
+ comp.month = qStartMonth;
2686
+ comp.day = 1;
2687
+ }
2688
+ break;
2689
+ }
2690
+ case "year":
2691
+ resetTime();
2692
+ comp.month = end ? 12 : 1;
2693
+ comp.day = end ? 31 : 1;
2694
+ break;
2695
+ case "decade":
2696
+ case "century":
2697
+ case "millennium": {
2698
+ resetTime();
2699
+ comp.month = end ? 12 : 1;
2700
+ comp.day = end ? 31 : 1;
2701
+ const span = unit === "decade" ? 10 : unit === "century" ? 100 : 1e3;
2702
+ const base = Math.floor(c.year / span) * span;
2703
+ comp.year = end ? base + span - 1 : base;
2704
+ break;
2705
+ }
2706
+ }
2707
+ const date = _Chronos.dateFromComponents(comp, this._timezone);
2708
+ return new _Chronos(date, this._timezone);
2709
+ }
2526
2710
  // ============================================================================
2527
2711
  // Convenience Add/Subtract Methods
2528
2712
  // ============================================================================
@@ -2717,6 +2901,12 @@ var Chronos = class _Chronos {
2717
2901
  return diffMs / MILLISECONDS_PER_DAY;
2718
2902
  case "week":
2719
2903
  return diffMs / (MILLISECONDS_PER_DAY * 7);
2904
+ case "month":
2905
+ return diffMs / MILLISECONDS_PER_MONTH;
2906
+ case "quarter":
2907
+ return diffMs / (MILLISECONDS_PER_MONTH * 3);
2908
+ case "year":
2909
+ return diffMs / MILLISECONDS_PER_YEAR;
2720
2910
  }
2721
2911
  }
2722
2912
  return diffInUnits(this._date, otherDate._date, normalizedUnit);
@@ -2774,12 +2964,12 @@ var Chronos = class _Chronos {
2774
2964
  * Get relative time from another date
2775
2965
  */
2776
2966
  from(other, options = {}) {
2777
- var _a, _b;
2967
+ var _a, _b, _c;
2778
2968
  const otherDate = _Chronos.parse(other);
2779
2969
  const diffMs = this._date.getTime() - otherDate._date.getTime();
2780
2970
  const absDiff = Math.abs(diffMs);
2781
2971
  const isFuture = diffMs > 0;
2782
- const { short: _short = false, absolute = false } = options;
2972
+ const { short = false, absolute = false } = options;
2783
2973
  const relative = this._locale.relativeTime;
2784
2974
  let value;
2785
2975
  let unit;
@@ -2805,10 +2995,26 @@ var Chronos = class _Chronos {
2805
2995
  value = Math.round(absDiff / (MILLISECONDS_PER_DAY * 365));
2806
2996
  unit = value === 1 ? "y" : "yy";
2807
2997
  }
2808
- const relativeStr = (_b = (_a = relative[unit]) == null ? void 0 : _a.replace(
2998
+ const shortUnits = {
2999
+ s: "s",
3000
+ ss: "s",
3001
+ m: "m",
3002
+ mm: "m",
3003
+ h: "h",
3004
+ hh: "h",
3005
+ d: "d",
3006
+ dd: "d",
3007
+ w: "w",
3008
+ ww: "w",
3009
+ M: "mo",
3010
+ MM: "mo",
3011
+ y: "y",
3012
+ yy: "y"
3013
+ };
3014
+ const relativeStr = short ? `${value}${(_a = shortUnits[unit]) != null ? _a : unit}` : (_c = (_b = relative[unit]) == null ? void 0 : _b.replace(
2809
3015
  "%d",
2810
3016
  String(value)
2811
- )) != null ? _b : `${value} ${unit}`;
3017
+ )) != null ? _c : `${value} ${unit}`;
2812
3018
  if (absolute) {
2813
3019
  return relativeStr;
2814
3020
  }
@@ -2887,7 +3093,7 @@ var Chronos = class _Chronos {
2887
3093
  Z: () => this.offsetString,
2888
3094
  ZZ: () => this.offsetString.replace(":", ""),
2889
3095
  Q: () => String(this.quarter),
2890
- Do: () => ordinalSuffix(this.date),
3096
+ Do: () => this._locale.ordinal(this.date),
2891
3097
  W: () => String(this.week),
2892
3098
  WW: () => padStart(this.week, 2),
2893
3099
  X: () => String(this.unix),
@@ -3161,15 +3367,16 @@ var ChronosInterval = class _ChronosInterval {
3161
3367
  // Constructor
3162
3368
  // ============================================================================
3163
3369
  constructor(duration = {}, inverted = false) {
3164
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
3370
+ var _a, _b, _c, _d, _e, _f, _g, _h;
3165
3371
  this._years = (_a = duration.years) != null ? _a : 0;
3166
3372
  this._months = (_b = duration.months) != null ? _b : 0;
3167
3373
  this._weeks = (_c = duration.weeks) != null ? _c : 0;
3168
3374
  this._days = (_d = duration.days) != null ? _d : 0;
3169
3375
  this._hours = (_e = duration.hours) != null ? _e : 0;
3170
3376
  this._minutes = (_f = duration.minutes) != null ? _f : 0;
3171
- this._seconds = Math.floor((_g = duration.seconds) != null ? _g : 0);
3172
- this._milliseconds = (_i = duration.milliseconds) != null ? _i : ((_h = duration.seconds) != null ? _h : 0) % 1 * 1e3;
3377
+ const totalSeconds = (_g = duration.seconds) != null ? _g : 0;
3378
+ this._seconds = Math.trunc(totalSeconds);
3379
+ this._milliseconds = (_h = duration.milliseconds) != null ? _h : Math.round((totalSeconds - this._seconds) * 1e3);
3173
3380
  this._locale = getLocale("en");
3174
3381
  this._inverted = inverted;
3175
3382
  }
@@ -3858,7 +4065,6 @@ var ChronosPeriod = class _ChronosPeriod {
3858
4065
  immutable: (_c = options.immutable) != null ? _c : true
3859
4066
  };
3860
4067
  this._filters = [];
3861
- this._current = 0;
3862
4068
  this._locale = getLocale("en");
3863
4069
  }
3864
4070
  // ============================================================================
@@ -4468,13 +4674,24 @@ var ChronosPeriod = class _ChronosPeriod {
4468
4674
  // ============================================================================
4469
4675
  // Range Operations
4470
4676
  // ============================================================================
4677
+ /**
4678
+ * Resolve the effective end of the period.
4679
+ * Returns null for a truly unbounded period (no end and no recurrence limit),
4680
+ * which avoids calling last() — that would iterate until the safety limit and throw.
4681
+ */
4682
+ _resolvedEnd() {
4683
+ var _a;
4684
+ if (this.isUnbounded) {
4685
+ return null;
4686
+ }
4687
+ return (_a = this._end) != null ? _a : this.last();
4688
+ }
4471
4689
  /**
4472
4690
  * Check if two periods overlap
4473
4691
  */
4474
4692
  overlaps(other) {
4475
- var _a, _b;
4476
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4477
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4693
+ const thisEnd = this._resolvedEnd();
4694
+ const otherEnd = other._resolvedEnd();
4478
4695
  if (!thisEnd || !otherEnd) {
4479
4696
  return true;
4480
4697
  }
@@ -4484,13 +4701,12 @@ var ChronosPeriod = class _ChronosPeriod {
4484
4701
  * Get the intersection of two periods
4485
4702
  */
4486
4703
  intersect(other) {
4487
- var _a, _b;
4488
4704
  if (!this.overlaps(other)) {
4489
4705
  return null;
4490
4706
  }
4491
4707
  const start = this._start.isAfter(other._start) ? this._start : other._start;
4492
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4493
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4708
+ const thisEnd = this._resolvedEnd();
4709
+ const otherEnd = other._resolvedEnd();
4494
4710
  if (!thisEnd || !otherEnd) {
4495
4711
  return new _ChronosPeriod(start, void 0, this._interval);
4496
4712
  }
@@ -4501,13 +4717,12 @@ var ChronosPeriod = class _ChronosPeriod {
4501
4717
  * Get the union of two periods
4502
4718
  */
4503
4719
  union(other) {
4504
- var _a, _b;
4505
4720
  if (!this.overlaps(other) && !this._adjacentTo(other)) {
4506
4721
  return null;
4507
4722
  }
4508
4723
  const start = this._start.isBefore(other._start) ? this._start : other._start;
4509
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4510
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4724
+ const thisEnd = this._resolvedEnd();
4725
+ const otherEnd = other._resolvedEnd();
4511
4726
  if (!thisEnd || !otherEnd) {
4512
4727
  return new _ChronosPeriod(start, void 0, this._interval);
4513
4728
  }
@@ -4519,13 +4734,12 @@ var ChronosPeriod = class _ChronosPeriod {
4519
4734
  * Returns the parts of this period that don't overlap with the other period
4520
4735
  */
4521
4736
  diff(other) {
4522
- var _a, _b;
4523
4737
  if (!this.overlaps(other)) {
4524
4738
  return [this.clone()];
4525
4739
  }
4526
4740
  const results = [];
4527
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4528
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4741
+ const thisEnd = this._resolvedEnd();
4742
+ const otherEnd = other._resolvedEnd();
4529
4743
  if (this._start.isBefore(other._start)) {
4530
4744
  results.push(
4531
4745
  new _ChronosPeriod(this._start, other._start, this._interval)
@@ -4540,9 +4754,8 @@ var ChronosPeriod = class _ChronosPeriod {
4540
4754
  * Check if this period is adjacent to another
4541
4755
  */
4542
4756
  _adjacentTo(other) {
4543
- var _a, _b;
4544
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4545
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4757
+ const thisEnd = this._resolvedEnd();
4758
+ const otherEnd = other._resolvedEnd();
4546
4759
  if (!thisEnd || !otherEnd) {
4547
4760
  return false;
4548
4761
  }
@@ -4634,11 +4847,13 @@ var ChronosPeriod = class _ChronosPeriod {
4634
4847
  }
4635
4848
  const chunks = [];
4636
4849
  let current = this._start.clone();
4850
+ const subStep = splitInterval.total("days") >= 1 ? { days: 1 } : { milliseconds: 1 };
4637
4851
  while (current.isSameOrBefore(this._end)) {
4638
- const chunkEnd = current.add(splitInterval.toDuration()).subtract({ days: 1 });
4852
+ const nextStart = current.add(splitInterval.toDuration());
4853
+ const chunkEnd = nextStart.subtract(subStep);
4639
4854
  const end = chunkEnd.isAfter(this._end) ? this._end : chunkEnd;
4640
4855
  chunks.push(new _ChronosPeriod(current, end, this._interval));
4641
- current = current.add(splitInterval.toDuration());
4856
+ current = nextStart;
4642
4857
  }
4643
4858
  return chunks;
4644
4859
  }
package/dist/index.mjs CHANGED
@@ -2013,18 +2013,14 @@ var Chronos = class _Chronos {
2013
2013
  if (isValidDate(isoDate)) {
2014
2014
  return isoDate;
2015
2015
  }
2016
- const formats = [
2017
- /^(\d{4})-(\d{2})-(\d{2})$/,
2018
- /^(\d{2})\/(\d{2})\/(\d{4})$/,
2019
- /^(\d{4})\/(\d{2})\/(\d{2})$/
2020
- ];
2021
- for (const format of formats) {
2022
- const match = input.match(format);
2023
- if (match) {
2024
- const parsed = new Date(input);
2025
- if (isValidDate(parsed)) {
2026
- return parsed;
2027
- }
2016
+ const dmy = input.match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
2017
+ if (dmy) {
2018
+ const day = parseInt(dmy[1], 10);
2019
+ const month = parseInt(dmy[2], 10);
2020
+ const year = parseInt(dmy[3], 10);
2021
+ const parsed = new Date(year, month - 1, day);
2022
+ if (isValidDate(parsed) && parsed.getMonth() === month - 1) {
2023
+ return parsed;
2028
2024
  }
2029
2025
  }
2030
2026
  throw new Error(`Unable to parse date: ${input}`);
@@ -2328,18 +2324,40 @@ var Chronos = class _Chronos {
2328
2324
  }
2329
2325
  /** Get the quarter (1-4) */
2330
2326
  get quarter() {
2327
+ if (this._timezone) {
2328
+ const month = new ChronosTimezone(this._timezone).getComponents(
2329
+ this._date
2330
+ ).month;
2331
+ return Math.floor((month - 1) / 3) + 1;
2332
+ }
2331
2333
  return getQuarter(this._date);
2332
2334
  }
2333
2335
  /** Get the day of year (1-366) */
2334
2336
  get dayOfYear() {
2337
+ if (this._timezone) {
2338
+ const c = new ChronosTimezone(this._timezone).getComponents(this._date);
2339
+ let days = c.day;
2340
+ for (let m = 1; m < c.month; m++) {
2341
+ days += getDaysInMonth(c.year, m - 1);
2342
+ }
2343
+ return days;
2344
+ }
2335
2345
  return getDayOfYear(this._date);
2336
2346
  }
2337
2347
  /** Get the ISO week number (1-53) */
2338
2348
  get week() {
2349
+ if (this._timezone) {
2350
+ const c = new ChronosTimezone(this._timezone).getComponents(this._date);
2351
+ return getISOWeek(new Date(c.year, c.month - 1, c.day));
2352
+ }
2339
2353
  return getISOWeek(this._date);
2340
2354
  }
2341
2355
  /** Get the ISO week year */
2342
2356
  get weekYear() {
2357
+ if (this._timezone) {
2358
+ const c = new ChronosTimezone(this._timezone).getComponents(this._date);
2359
+ return getISOWeekYear(new Date(c.year, c.month - 1, c.day));
2360
+ }
2343
2361
  return getISOWeekYear(this._date);
2344
2362
  }
2345
2363
  /** Get the number of days in the current month */
@@ -2360,6 +2378,9 @@ var Chronos = class _Chronos {
2360
2378
  }
2361
2379
  /** Get the timezone offset in minutes */
2362
2380
  get offset() {
2381
+ if (this._timezone) {
2382
+ return -new ChronosTimezone(this._timezone).getOffsetMinutes(this._date);
2383
+ }
2363
2384
  return this._date.getTimezoneOffset();
2364
2385
  }
2365
2386
  /** Get the timezone offset as string (+05:30) */
@@ -2471,9 +2492,82 @@ var Chronos = class _Chronos {
2471
2492
  throw new Error("Unit is required when amount is a number");
2472
2493
  }
2473
2494
  const normalizedUnit = normalizeUnit(unit);
2495
+ if (this._timezone) {
2496
+ return this.addInZone(amount, normalizedUnit);
2497
+ }
2474
2498
  const newDate = addUnits(this._date, amount, normalizedUnit);
2475
2499
  return new _Chronos(newDate, this._timezone);
2476
2500
  }
2501
+ /**
2502
+ * Add an amount of a single unit while respecting the instance timezone.
2503
+ * Calendar units (day/week/month/quarter/year/…) preserve the wall-clock time
2504
+ * in the zone (DST-safe); absolute units (hour/minute/second/millisecond) add a
2505
+ * fixed offset to the instant. Day clamping mirrors the local-time path.
2506
+ */
2507
+ addInZone(amount, unit) {
2508
+ const absoluteMs = {
2509
+ millisecond: 1,
2510
+ second: MILLISECONDS_PER_SECOND,
2511
+ minute: MILLISECONDS_PER_MINUTE,
2512
+ hour: MILLISECONDS_PER_HOUR
2513
+ };
2514
+ const perUnit = absoluteMs[unit];
2515
+ if (perUnit !== void 0) {
2516
+ const newDate = new Date(this._date.getTime() + amount * perUnit);
2517
+ return new _Chronos(newDate, this._timezone);
2518
+ }
2519
+ const tz = new ChronosTimezone(this._timezone);
2520
+ const c = tz.getComponents(this._date);
2521
+ let year = c.year;
2522
+ let month = c.month;
2523
+ let day = c.day;
2524
+ const calendarUnit = unit === "month" || unit === "quarter" || unit === "year" || unit === "decade" || unit === "century" || unit === "millennium";
2525
+ switch (unit) {
2526
+ case "day":
2527
+ day += amount;
2528
+ break;
2529
+ case "week":
2530
+ day += amount * 7;
2531
+ break;
2532
+ case "month":
2533
+ month += amount;
2534
+ break;
2535
+ case "quarter":
2536
+ month += amount * 3;
2537
+ break;
2538
+ case "year":
2539
+ year += amount;
2540
+ break;
2541
+ case "decade":
2542
+ year += amount * 10;
2543
+ break;
2544
+ case "century":
2545
+ year += amount * 100;
2546
+ break;
2547
+ case "millennium":
2548
+ year += amount * 1e3;
2549
+ break;
2550
+ }
2551
+ year += Math.floor((month - 1) / 12);
2552
+ month = ((month - 1) % 12 + 12) % 12 + 1;
2553
+ if (calendarUnit) {
2554
+ const maxDay = getDaysInMonth(year, month - 1);
2555
+ if (day > maxDay) day = maxDay;
2556
+ }
2557
+ const date = _Chronos.dateFromComponents(
2558
+ {
2559
+ year,
2560
+ month,
2561
+ day,
2562
+ hour: c.hour,
2563
+ minute: c.minute,
2564
+ second: c.second,
2565
+ millisecond: this._date.getMilliseconds()
2566
+ },
2567
+ this._timezone
2568
+ );
2569
+ return new _Chronos(date, this._timezone);
2570
+ }
2477
2571
  /**
2478
2572
  * Subtract time from the date
2479
2573
  */
@@ -2501,6 +2595,9 @@ var Chronos = class _Chronos {
2501
2595
  */
2502
2596
  startOf(unit) {
2503
2597
  const normalizedUnit = normalizeUnit(unit);
2598
+ if (this._timezone) {
2599
+ return this.boundaryInZone(normalizedUnit, false);
2600
+ }
2504
2601
  const newDate = startOf(this._date, normalizedUnit);
2505
2602
  return new _Chronos(newDate, this._timezone);
2506
2603
  }
@@ -2516,9 +2613,96 @@ var Chronos = class _Chronos {
2516
2613
  */
2517
2614
  endOf(unit) {
2518
2615
  const normalizedUnit = normalizeUnit(unit);
2616
+ if (this._timezone) {
2617
+ return this.boundaryInZone(normalizedUnit, true);
2618
+ }
2519
2619
  const newDate = endOf(this._date, normalizedUnit);
2520
2620
  return new _Chronos(newDate, this._timezone);
2521
2621
  }
2622
+ /**
2623
+ * Compute the start/end boundary of a unit in the instance timezone.
2624
+ * The plain `startOf`/`endOf` helpers operate on local wall-clock fields of the
2625
+ * underlying Date, which is wrong when an explicit timezone is attached. Here we
2626
+ * read the wall-clock components in the target zone, snap them to the boundary,
2627
+ * and rebuild the instant (dateFromComponents handles DST).
2628
+ */
2629
+ boundaryInZone(unit, end) {
2630
+ if (unit === "millisecond") {
2631
+ return this.clone();
2632
+ }
2633
+ const tz = new ChronosTimezone(this._timezone);
2634
+ const c = tz.getComponents(this._date);
2635
+ const comp = {
2636
+ year: c.year,
2637
+ month: c.month,
2638
+ day: c.day,
2639
+ hour: c.hour,
2640
+ minute: c.minute,
2641
+ second: c.second,
2642
+ millisecond: this._date.getMilliseconds()
2643
+ };
2644
+ const resetTime = () => {
2645
+ comp.hour = end ? 23 : 0;
2646
+ comp.minute = end ? 59 : 0;
2647
+ comp.second = end ? 59 : 0;
2648
+ comp.millisecond = end ? 999 : 0;
2649
+ };
2650
+ switch (unit) {
2651
+ case "second":
2652
+ comp.millisecond = end ? 999 : 0;
2653
+ break;
2654
+ case "minute":
2655
+ comp.second = end ? 59 : 0;
2656
+ comp.millisecond = end ? 999 : 0;
2657
+ break;
2658
+ case "hour":
2659
+ comp.minute = end ? 59 : 0;
2660
+ comp.second = end ? 59 : 0;
2661
+ comp.millisecond = end ? 999 : 0;
2662
+ break;
2663
+ case "day":
2664
+ resetTime();
2665
+ break;
2666
+ case "week":
2667
+ resetTime();
2668
+ comp.day = c.day + (end ? 6 - c.dayOfWeek : -c.dayOfWeek);
2669
+ break;
2670
+ case "month":
2671
+ resetTime();
2672
+ comp.day = end ? getDaysInMonth(c.year, c.month - 1) : 1;
2673
+ break;
2674
+ case "quarter": {
2675
+ resetTime();
2676
+ const qStartMonth = Math.floor((c.month - 1) / 3) * 3 + 1;
2677
+ if (end) {
2678
+ comp.month = qStartMonth + 2;
2679
+ comp.day = getDaysInMonth(c.year, comp.month - 1);
2680
+ } else {
2681
+ comp.month = qStartMonth;
2682
+ comp.day = 1;
2683
+ }
2684
+ break;
2685
+ }
2686
+ case "year":
2687
+ resetTime();
2688
+ comp.month = end ? 12 : 1;
2689
+ comp.day = end ? 31 : 1;
2690
+ break;
2691
+ case "decade":
2692
+ case "century":
2693
+ case "millennium": {
2694
+ resetTime();
2695
+ comp.month = end ? 12 : 1;
2696
+ comp.day = end ? 31 : 1;
2697
+ const span = unit === "decade" ? 10 : unit === "century" ? 100 : 1e3;
2698
+ const base = Math.floor(c.year / span) * span;
2699
+ comp.year = end ? base + span - 1 : base;
2700
+ break;
2701
+ }
2702
+ }
2703
+ const date = _Chronos.dateFromComponents(comp, this._timezone);
2704
+ return new _Chronos(date, this._timezone);
2705
+ }
2522
2706
  // ============================================================================
2523
2707
  // Convenience Add/Subtract Methods
2524
2708
  // ============================================================================
@@ -2713,6 +2897,12 @@ var Chronos = class _Chronos {
2713
2897
  return diffMs / MILLISECONDS_PER_DAY;
2714
2898
  case "week":
2715
2899
  return diffMs / (MILLISECONDS_PER_DAY * 7);
2900
+ case "month":
2901
+ return diffMs / MILLISECONDS_PER_MONTH;
2902
+ case "quarter":
2903
+ return diffMs / (MILLISECONDS_PER_MONTH * 3);
2904
+ case "year":
2905
+ return diffMs / MILLISECONDS_PER_YEAR;
2716
2906
  }
2717
2907
  }
2718
2908
  return diffInUnits(this._date, otherDate._date, normalizedUnit);
@@ -2770,12 +2960,12 @@ var Chronos = class _Chronos {
2770
2960
  * Get relative time from another date
2771
2961
  */
2772
2962
  from(other, options = {}) {
2773
- var _a, _b;
2963
+ var _a, _b, _c;
2774
2964
  const otherDate = _Chronos.parse(other);
2775
2965
  const diffMs = this._date.getTime() - otherDate._date.getTime();
2776
2966
  const absDiff = Math.abs(diffMs);
2777
2967
  const isFuture = diffMs > 0;
2778
- const { short: _short = false, absolute = false } = options;
2968
+ const { short = false, absolute = false } = options;
2779
2969
  const relative = this._locale.relativeTime;
2780
2970
  let value;
2781
2971
  let unit;
@@ -2801,10 +2991,26 @@ var Chronos = class _Chronos {
2801
2991
  value = Math.round(absDiff / (MILLISECONDS_PER_DAY * 365));
2802
2992
  unit = value === 1 ? "y" : "yy";
2803
2993
  }
2804
- const relativeStr = (_b = (_a = relative[unit]) == null ? void 0 : _a.replace(
2994
+ const shortUnits = {
2995
+ s: "s",
2996
+ ss: "s",
2997
+ m: "m",
2998
+ mm: "m",
2999
+ h: "h",
3000
+ hh: "h",
3001
+ d: "d",
3002
+ dd: "d",
3003
+ w: "w",
3004
+ ww: "w",
3005
+ M: "mo",
3006
+ MM: "mo",
3007
+ y: "y",
3008
+ yy: "y"
3009
+ };
3010
+ const relativeStr = short ? `${value}${(_a = shortUnits[unit]) != null ? _a : unit}` : (_c = (_b = relative[unit]) == null ? void 0 : _b.replace(
2805
3011
  "%d",
2806
3012
  String(value)
2807
- )) != null ? _b : `${value} ${unit}`;
3013
+ )) != null ? _c : `${value} ${unit}`;
2808
3014
  if (absolute) {
2809
3015
  return relativeStr;
2810
3016
  }
@@ -2883,7 +3089,7 @@ var Chronos = class _Chronos {
2883
3089
  Z: () => this.offsetString,
2884
3090
  ZZ: () => this.offsetString.replace(":", ""),
2885
3091
  Q: () => String(this.quarter),
2886
- Do: () => ordinalSuffix(this.date),
3092
+ Do: () => this._locale.ordinal(this.date),
2887
3093
  W: () => String(this.week),
2888
3094
  WW: () => padStart(this.week, 2),
2889
3095
  X: () => String(this.unix),
@@ -3157,15 +3363,16 @@ var ChronosInterval = class _ChronosInterval {
3157
3363
  // Constructor
3158
3364
  // ============================================================================
3159
3365
  constructor(duration = {}, inverted = false) {
3160
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
3366
+ var _a, _b, _c, _d, _e, _f, _g, _h;
3161
3367
  this._years = (_a = duration.years) != null ? _a : 0;
3162
3368
  this._months = (_b = duration.months) != null ? _b : 0;
3163
3369
  this._weeks = (_c = duration.weeks) != null ? _c : 0;
3164
3370
  this._days = (_d = duration.days) != null ? _d : 0;
3165
3371
  this._hours = (_e = duration.hours) != null ? _e : 0;
3166
3372
  this._minutes = (_f = duration.minutes) != null ? _f : 0;
3167
- this._seconds = Math.floor((_g = duration.seconds) != null ? _g : 0);
3168
- this._milliseconds = (_i = duration.milliseconds) != null ? _i : ((_h = duration.seconds) != null ? _h : 0) % 1 * 1e3;
3373
+ const totalSeconds = (_g = duration.seconds) != null ? _g : 0;
3374
+ this._seconds = Math.trunc(totalSeconds);
3375
+ this._milliseconds = (_h = duration.milliseconds) != null ? _h : Math.round((totalSeconds - this._seconds) * 1e3);
3169
3376
  this._locale = getLocale("en");
3170
3377
  this._inverted = inverted;
3171
3378
  }
@@ -3854,7 +4061,6 @@ var ChronosPeriod = class _ChronosPeriod {
3854
4061
  immutable: (_c = options.immutable) != null ? _c : true
3855
4062
  };
3856
4063
  this._filters = [];
3857
- this._current = 0;
3858
4064
  this._locale = getLocale("en");
3859
4065
  }
3860
4066
  // ============================================================================
@@ -4464,13 +4670,24 @@ var ChronosPeriod = class _ChronosPeriod {
4464
4670
  // ============================================================================
4465
4671
  // Range Operations
4466
4672
  // ============================================================================
4673
+ /**
4674
+ * Resolve the effective end of the period.
4675
+ * Returns null for a truly unbounded period (no end and no recurrence limit),
4676
+ * which avoids calling last() — that would iterate until the safety limit and throw.
4677
+ */
4678
+ _resolvedEnd() {
4679
+ var _a;
4680
+ if (this.isUnbounded) {
4681
+ return null;
4682
+ }
4683
+ return (_a = this._end) != null ? _a : this.last();
4684
+ }
4467
4685
  /**
4468
4686
  * Check if two periods overlap
4469
4687
  */
4470
4688
  overlaps(other) {
4471
- var _a, _b;
4472
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4473
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4689
+ const thisEnd = this._resolvedEnd();
4690
+ const otherEnd = other._resolvedEnd();
4474
4691
  if (!thisEnd || !otherEnd) {
4475
4692
  return true;
4476
4693
  }
@@ -4480,13 +4697,12 @@ var ChronosPeriod = class _ChronosPeriod {
4480
4697
  * Get the intersection of two periods
4481
4698
  */
4482
4699
  intersect(other) {
4483
- var _a, _b;
4484
4700
  if (!this.overlaps(other)) {
4485
4701
  return null;
4486
4702
  }
4487
4703
  const start = this._start.isAfter(other._start) ? this._start : other._start;
4488
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4489
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4704
+ const thisEnd = this._resolvedEnd();
4705
+ const otherEnd = other._resolvedEnd();
4490
4706
  if (!thisEnd || !otherEnd) {
4491
4707
  return new _ChronosPeriod(start, void 0, this._interval);
4492
4708
  }
@@ -4497,13 +4713,12 @@ var ChronosPeriod = class _ChronosPeriod {
4497
4713
  * Get the union of two periods
4498
4714
  */
4499
4715
  union(other) {
4500
- var _a, _b;
4501
4716
  if (!this.overlaps(other) && !this._adjacentTo(other)) {
4502
4717
  return null;
4503
4718
  }
4504
4719
  const start = this._start.isBefore(other._start) ? this._start : other._start;
4505
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4506
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4720
+ const thisEnd = this._resolvedEnd();
4721
+ const otherEnd = other._resolvedEnd();
4507
4722
  if (!thisEnd || !otherEnd) {
4508
4723
  return new _ChronosPeriod(start, void 0, this._interval);
4509
4724
  }
@@ -4515,13 +4730,12 @@ var ChronosPeriod = class _ChronosPeriod {
4515
4730
  * Returns the parts of this period that don't overlap with the other period
4516
4731
  */
4517
4732
  diff(other) {
4518
- var _a, _b;
4519
4733
  if (!this.overlaps(other)) {
4520
4734
  return [this.clone()];
4521
4735
  }
4522
4736
  const results = [];
4523
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4524
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4737
+ const thisEnd = this._resolvedEnd();
4738
+ const otherEnd = other._resolvedEnd();
4525
4739
  if (this._start.isBefore(other._start)) {
4526
4740
  results.push(
4527
4741
  new _ChronosPeriod(this._start, other._start, this._interval)
@@ -4536,9 +4750,8 @@ var ChronosPeriod = class _ChronosPeriod {
4536
4750
  * Check if this period is adjacent to another
4537
4751
  */
4538
4752
  _adjacentTo(other) {
4539
- var _a, _b;
4540
- const thisEnd = (_a = this._end) != null ? _a : this.last();
4541
- const otherEnd = (_b = other._end) != null ? _b : other.last();
4753
+ const thisEnd = this._resolvedEnd();
4754
+ const otherEnd = other._resolvedEnd();
4542
4755
  if (!thisEnd || !otherEnd) {
4543
4756
  return false;
4544
4757
  }
@@ -4630,11 +4843,13 @@ var ChronosPeriod = class _ChronosPeriod {
4630
4843
  }
4631
4844
  const chunks = [];
4632
4845
  let current = this._start.clone();
4846
+ const subStep = splitInterval.total("days") >= 1 ? { days: 1 } : { milliseconds: 1 };
4633
4847
  while (current.isSameOrBefore(this._end)) {
4634
- const chunkEnd = current.add(splitInterval.toDuration()).subtract({ days: 1 });
4848
+ const nextStart = current.add(splitInterval.toDuration());
4849
+ const chunkEnd = nextStart.subtract(subStep);
4635
4850
  const end = chunkEnd.isAfter(this._end) ? this._end : chunkEnd;
4636
4851
  chunks.push(new _ChronosPeriod(current, end, this._interval));
4637
- current = current.add(splitInterval.toDuration());
4852
+ current = nextStart;
4638
4853
  }
4639
4854
  return chunks;
4640
4855
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chronos-ts",
3
- "version": "2.0.3",
3
+ "version": "2.0.4",
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",