@naturalcycles/js-lib 14.99.2 → 14.100.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.
@@ -218,15 +218,23 @@ class LocalDate {
218
218
  const [big, small] = sign === 1 ? [this, d] : [d, this];
219
219
  if (unit === 'year') {
220
220
  let years = big.$year - small.$year;
221
- if (big.$month < small.$month || (big.$month === small.$month && big.$day < small.$day)) {
221
+ if (big.$month < small.$month ||
222
+ (big.$month === small.$month &&
223
+ big.$day < small.$day &&
224
+ !(big.$day === LocalDate.getMonthLength(big.$year, big.$month) &&
225
+ small.$day === LocalDate.getMonthLength(small.$year, small.$month)))) {
222
226
  years--;
223
227
  }
224
228
  return years * sign || 0;
225
229
  }
226
230
  if (unit === 'month') {
227
231
  let months = (big.$year - small.$year) * 12 + (big.$month - small.$month);
228
- if (big.$day < small.$day)
229
- months--;
232
+ if (big.$day < small.$day) {
233
+ const bigMonthLen = LocalDate.getMonthLength(big.$year, big.$month);
234
+ if (big.$day !== bigMonthLen || small.$day < bigMonthLen) {
235
+ months--;
236
+ }
237
+ }
230
238
  return months * sign || 0;
231
239
  }
232
240
  // unit is 'day' or 'week'
@@ -266,20 +274,36 @@ class LocalDate {
266
274
  else if (unit === 'year') {
267
275
  $year += num;
268
276
  }
277
+ // check month overflow
278
+ while ($month > 12) {
279
+ $year += 1;
280
+ $month -= 12;
281
+ }
282
+ while ($month < 1) {
283
+ $year -= 1;
284
+ $month += 12;
285
+ }
269
286
  // check day overflow
270
- if (unit === 'day') {
271
- if ($day < 1) {
272
- while ($day < 1) {
273
- $month -= 1;
274
- if ($month < 1) {
275
- $year -= 1;
276
- $month += 12;
277
- }
278
- $day += LocalDate.getMonthLength($year, $month);
287
+ // Applies not only for 'day' unit, but also e.g 2022-05-31 plus 1 month should be 2022-06-30 (not 31!)
288
+ if ($day < 1) {
289
+ while ($day < 1) {
290
+ $month -= 1;
291
+ if ($month < 1) {
292
+ $year -= 1;
293
+ $month += 12;
294
+ }
295
+ $day += LocalDate.getMonthLength($year, $month);
296
+ }
297
+ }
298
+ else {
299
+ let monLen = LocalDate.getMonthLength($year, $month);
300
+ if (unit !== 'day') {
301
+ if ($day > monLen) {
302
+ // Case of 2022-05-31 plus 1 month should be 2022-06-30, not 31
303
+ $day = monLen;
279
304
  }
280
305
  }
281
306
  else {
282
- let monLen = LocalDate.getMonthLength($year, $month);
283
307
  while ($day > monLen) {
284
308
  $day -= monLen;
285
309
  $month += 1;
@@ -291,15 +315,6 @@ class LocalDate {
291
315
  }
292
316
  }
293
317
  }
294
- // check month overflow
295
- while ($month > 12) {
296
- $year += 1;
297
- $month -= 12;
298
- }
299
- while ($month < 1) {
300
- $year -= 1;
301
- $month += 12;
302
- }
303
318
  if (mutate) {
304
319
  this.$year = $year;
305
320
  this.$month = $month;
@@ -31,6 +31,10 @@ export declare class LocalTime {
31
31
  * Input can already be a LocalDate - it is returned as-is in that case.
32
32
  */
33
33
  static of(d: LocalTimeConfig): LocalTime;
34
+ /**
35
+ * Create LocalTime from unixTimestamp in milliseconds (not in seconds).
36
+ */
37
+ static ofMillis(millis: number): LocalTime;
34
38
  /**
35
39
  * Returns null if invalid
36
40
  */
@@ -39,6 +39,12 @@ class LocalTime {
39
39
  }
40
40
  return t;
41
41
  }
42
+ /**
43
+ * Create LocalTime from unixTimestamp in milliseconds (not in seconds).
44
+ */
45
+ static ofMillis(millis) {
46
+ return LocalTime.of(new Date(millis));
47
+ }
42
48
  /**
43
49
  * Returns null if invalid
44
50
  */
@@ -96,7 +102,7 @@ class LocalTime {
96
102
  return new LocalTime(new Date());
97
103
  }
98
104
  static fromComponents(c) {
99
- return new LocalTime(new Date(c.year, c.month - 1, c.day, c.hour, c.minute, c.second));
105
+ return new LocalTime(new Date(c.year, c.month - 1, c.day || 1, c.hour || 0, c.minute || 0, c.second || 0));
100
106
  }
101
107
  get(unit) {
102
108
  if (unit === 'year') {
@@ -177,14 +183,9 @@ class LocalTime {
177
183
  }
178
184
  setComponents(c, mutate = false) {
179
185
  const d = mutate ? this.$date : new Date(this.$date);
180
- if (c.year) {
181
- d.setFullYear(c.year);
182
- }
183
- if (c.month) {
184
- d.setMonth(c.month - 1);
185
- }
186
- if (c.day) {
187
- d.setDate(c.day);
186
+ // Year, month and day set all-at-once, to avoid 30/31 (and 28/29) mishap
187
+ if (c.day || c.month !== undefined || c.year !== undefined) {
188
+ d.setFullYear(c.year ?? d.getFullYear(), c.month ? c.month - 1 : d.getMonth(), c.day || d.getDate());
188
189
  }
189
190
  if (c.hour !== undefined) {
190
191
  d.setHours(c.hour);
@@ -202,6 +203,10 @@ class LocalTime {
202
203
  num *= 7;
203
204
  unit = 'day';
204
205
  }
206
+ if (unit === 'year' || unit === 'month') {
207
+ const d = addMonths(this.$date, unit === 'month' ? num : num * 12, mutate);
208
+ return mutate ? this : LocalTime.of(d);
209
+ }
205
210
  return this.set(unit, this.get(unit) + num, mutate);
206
211
  }
207
212
  subtract(num, unit, mutate = false) {
@@ -215,33 +220,14 @@ class LocalTime {
215
220
  const secDiff = (this.$date.valueOf() - date2.valueOf()) / 1000;
216
221
  if (!secDiff)
217
222
  return 0;
218
- if (unit === 'year' || unit === 'month') {
219
- const sign = secDiff > 0 ? 1 : -1;
220
- // Put items in descending order: "big minus small"
221
- const [big, small] = sign === 1 ? [this.$date, date2] : [date2, this.$date];
222
- if (unit === 'year') {
223
- let years = big.getFullYear() - small.getFullYear();
224
- const big2 = new Date(big);
225
- const small2 = new Date(small);
226
- big2.setFullYear(1584);
227
- small2.setFullYear(1584);
228
- if (big2 < small2)
229
- years--;
230
- return years * sign || 0;
231
- }
232
- if (unit === 'month') {
233
- let months = (big.getFullYear() - small.getFullYear()) * 12 + big.getMonth() - small.getMonth();
234
- const big2 = new Date(big);
235
- const small2 = new Date(small);
236
- big2.setFullYear(1584, 0);
237
- small2.setFullYear(1584, 0);
238
- if (big2 < small2)
239
- months--;
240
- return months * sign || 0;
241
- }
242
- }
243
223
  let r;
244
- if (unit === 'day') {
224
+ if (unit === 'year') {
225
+ r = differenceInMonths(this.getDate(), date2) / 12;
226
+ }
227
+ else if (unit === 'month') {
228
+ r = differenceInMonths(this.getDate(), date2);
229
+ }
230
+ else if (unit === 'day') {
245
231
  r = secDiff / SECONDS_IN_DAY;
246
232
  }
247
233
  else if (unit === 'week') {
@@ -486,7 +472,7 @@ class LocalTime {
486
472
  ].join('');
487
473
  }
488
474
  toString() {
489
- return String(this.unix());
475
+ return this.toISODateTime();
490
476
  }
491
477
  toJSON() {
492
478
  return this.unix();
@@ -543,29 +529,6 @@ function getWeekYear(date) {
543
529
  return year - 1;
544
530
  }
545
531
  }
546
- // function setWeekYear(
547
- // date: Date,
548
- // year: number,
549
- // ): Date {
550
- // const diff = differenceInCalendarDays(date, startOfWeekYear(date))
551
- // const fourthOfJanuary = new Date(0)
552
- // fourthOfJanuary.setFullYear(year, 0, 4)
553
- // fourthOfJanuary.setHours(0, 0, 0, 0)
554
- // date = startOfWeekYear(fourthOfJanuary)
555
- // date.setDate(date.getDate() + diff)
556
- // return date
557
- // }
558
- // function differenceInCalendarDays(
559
- // dateLeft: Date,
560
- // dateRight: Date,
561
- // ): number {
562
- // return Math.round((startOfDay(dateLeft).getTime() - startOfDay(dateRight).getTime()) / MILLISECONDS_IN_DAY)
563
- // }
564
- // function startOfDay(date: Date, mutate = false): Date {
565
- // const d = mutate ? date : new Date(date)
566
- // d.setHours(0, 0, 0, 0)
567
- // return d
568
- // }
569
532
  // based on: https://github.com/date-fns/date-fns/blob/fd6bb1a0bab143f2da068c05a9c562b9bee1357d/src/startOfWeek/index.ts
570
533
  function startOfWeek(date, mutate = false) {
571
534
  const d = mutate ? date : new Date(date);
@@ -583,3 +546,36 @@ function endOfWeek(date, mutate = false) {
583
546
  d.setDate(d.getDate() + diff);
584
547
  return d;
585
548
  }
549
+ function addMonths(d, num, mutate = false) {
550
+ if (!mutate)
551
+ d = new Date(d);
552
+ let day = d.getDate();
553
+ let month = d.getMonth() + 1 + num;
554
+ if (day < 29) {
555
+ d.setMonth(month - 1);
556
+ return d;
557
+ }
558
+ let year = d.getFullYear();
559
+ while (month > 12) {
560
+ year++;
561
+ month -= 12;
562
+ }
563
+ while (month < 1) {
564
+ year--;
565
+ month += 12;
566
+ }
567
+ const monthLen = localDate_1.LocalDate.getMonthLength(year, month);
568
+ if (day > monthLen)
569
+ day = monthLen;
570
+ d.setFullYear(year, month - 1, day);
571
+ return d;
572
+ }
573
+ function differenceInMonths(a, b) {
574
+ if (a.getDate() < b.getDate())
575
+ return -differenceInMonths(b, a);
576
+ const wholeMonthDiff = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
577
+ const anchor = addMonths(a, wholeMonthDiff).getTime();
578
+ const sign = b.getTime() - anchor >= 0 ? 1 : -1;
579
+ const anchor2 = addMonths(a, wholeMonthDiff + sign).getTime();
580
+ return -(wholeMonthDiff + ((b.getTime() - anchor) / (anchor2 - anchor)) * sign);
581
+ }
@@ -215,15 +215,23 @@ export class LocalDate {
215
215
  const [big, small] = sign === 1 ? [this, d] : [d, this];
216
216
  if (unit === 'year') {
217
217
  let years = big.$year - small.$year;
218
- if (big.$month < small.$month || (big.$month === small.$month && big.$day < small.$day)) {
218
+ if (big.$month < small.$month ||
219
+ (big.$month === small.$month &&
220
+ big.$day < small.$day &&
221
+ !(big.$day === LocalDate.getMonthLength(big.$year, big.$month) &&
222
+ small.$day === LocalDate.getMonthLength(small.$year, small.$month)))) {
219
223
  years--;
220
224
  }
221
225
  return years * sign || 0;
222
226
  }
223
227
  if (unit === 'month') {
224
228
  let months = (big.$year - small.$year) * 12 + (big.$month - small.$month);
225
- if (big.$day < small.$day)
226
- months--;
229
+ if (big.$day < small.$day) {
230
+ const bigMonthLen = LocalDate.getMonthLength(big.$year, big.$month);
231
+ if (big.$day !== bigMonthLen || small.$day < bigMonthLen) {
232
+ months--;
233
+ }
234
+ }
227
235
  return months * sign || 0;
228
236
  }
229
237
  // unit is 'day' or 'week'
@@ -263,20 +271,36 @@ export class LocalDate {
263
271
  else if (unit === 'year') {
264
272
  $year += num;
265
273
  }
274
+ // check month overflow
275
+ while ($month > 12) {
276
+ $year += 1;
277
+ $month -= 12;
278
+ }
279
+ while ($month < 1) {
280
+ $year -= 1;
281
+ $month += 12;
282
+ }
266
283
  // check day overflow
267
- if (unit === 'day') {
268
- if ($day < 1) {
269
- while ($day < 1) {
270
- $month -= 1;
271
- if ($month < 1) {
272
- $year -= 1;
273
- $month += 12;
274
- }
275
- $day += LocalDate.getMonthLength($year, $month);
284
+ // Applies not only for 'day' unit, but also e.g 2022-05-31 plus 1 month should be 2022-06-30 (not 31!)
285
+ if ($day < 1) {
286
+ while ($day < 1) {
287
+ $month -= 1;
288
+ if ($month < 1) {
289
+ $year -= 1;
290
+ $month += 12;
291
+ }
292
+ $day += LocalDate.getMonthLength($year, $month);
293
+ }
294
+ }
295
+ else {
296
+ let monLen = LocalDate.getMonthLength($year, $month);
297
+ if (unit !== 'day') {
298
+ if ($day > monLen) {
299
+ // Case of 2022-05-31 plus 1 month should be 2022-06-30, not 31
300
+ $day = monLen;
276
301
  }
277
302
  }
278
303
  else {
279
- let monLen = LocalDate.getMonthLength($year, $month);
280
304
  while ($day > monLen) {
281
305
  $day -= monLen;
282
306
  $month += 1;
@@ -288,15 +312,6 @@ export class LocalDate {
288
312
  }
289
313
  }
290
314
  }
291
- // check month overflow
292
- while ($month > 12) {
293
- $year += 1;
294
- $month -= 12;
295
- }
296
- while ($month < 1) {
297
- $year -= 1;
298
- $month += 12;
299
- }
300
315
  if (mutate) {
301
316
  this.$year = $year;
302
317
  this.$month = $month;
@@ -36,6 +36,12 @@ export class LocalTime {
36
36
  }
37
37
  return t;
38
38
  }
39
+ /**
40
+ * Create LocalTime from unixTimestamp in milliseconds (not in seconds).
41
+ */
42
+ static ofMillis(millis) {
43
+ return LocalTime.of(new Date(millis));
44
+ }
39
45
  /**
40
46
  * Returns null if invalid
41
47
  */
@@ -93,7 +99,7 @@ export class LocalTime {
93
99
  return new LocalTime(new Date());
94
100
  }
95
101
  static fromComponents(c) {
96
- return new LocalTime(new Date(c.year, c.month - 1, c.day, c.hour, c.minute, c.second));
102
+ return new LocalTime(new Date(c.year, c.month - 1, c.day || 1, c.hour || 0, c.minute || 0, c.second || 0));
97
103
  }
98
104
  get(unit) {
99
105
  if (unit === 'year') {
@@ -173,15 +179,11 @@ export class LocalTime {
173
179
  return v === undefined ? this.get('second') : this.set('second', v);
174
180
  }
175
181
  setComponents(c, mutate = false) {
182
+ var _a;
176
183
  const d = mutate ? this.$date : new Date(this.$date);
177
- if (c.year) {
178
- d.setFullYear(c.year);
179
- }
180
- if (c.month) {
181
- d.setMonth(c.month - 1);
182
- }
183
- if (c.day) {
184
- d.setDate(c.day);
184
+ // Year, month and day set all-at-once, to avoid 30/31 (and 28/29) mishap
185
+ if (c.day || c.month !== undefined || c.year !== undefined) {
186
+ d.setFullYear((_a = c.year) !== null && _a !== void 0 ? _a : d.getFullYear(), c.month ? c.month - 1 : d.getMonth(), c.day || d.getDate());
185
187
  }
186
188
  if (c.hour !== undefined) {
187
189
  d.setHours(c.hour);
@@ -199,6 +201,10 @@ export class LocalTime {
199
201
  num *= 7;
200
202
  unit = 'day';
201
203
  }
204
+ if (unit === 'year' || unit === 'month') {
205
+ const d = addMonths(this.$date, unit === 'month' ? num : num * 12, mutate);
206
+ return mutate ? this : LocalTime.of(d);
207
+ }
202
208
  return this.set(unit, this.get(unit) + num, mutate);
203
209
  }
204
210
  subtract(num, unit, mutate = false) {
@@ -212,33 +218,14 @@ export class LocalTime {
212
218
  const secDiff = (this.$date.valueOf() - date2.valueOf()) / 1000;
213
219
  if (!secDiff)
214
220
  return 0;
215
- if (unit === 'year' || unit === 'month') {
216
- const sign = secDiff > 0 ? 1 : -1;
217
- // Put items in descending order: "big minus small"
218
- const [big, small] = sign === 1 ? [this.$date, date2] : [date2, this.$date];
219
- if (unit === 'year') {
220
- let years = big.getFullYear() - small.getFullYear();
221
- const big2 = new Date(big);
222
- const small2 = new Date(small);
223
- big2.setFullYear(1584);
224
- small2.setFullYear(1584);
225
- if (big2 < small2)
226
- years--;
227
- return years * sign || 0;
228
- }
229
- if (unit === 'month') {
230
- let months = (big.getFullYear() - small.getFullYear()) * 12 + big.getMonth() - small.getMonth();
231
- const big2 = new Date(big);
232
- const small2 = new Date(small);
233
- big2.setFullYear(1584, 0);
234
- small2.setFullYear(1584, 0);
235
- if (big2 < small2)
236
- months--;
237
- return months * sign || 0;
238
- }
239
- }
240
221
  let r;
241
- if (unit === 'day') {
222
+ if (unit === 'year') {
223
+ r = differenceInMonths(this.getDate(), date2) / 12;
224
+ }
225
+ else if (unit === 'month') {
226
+ r = differenceInMonths(this.getDate(), date2);
227
+ }
228
+ else if (unit === 'day') {
242
229
  r = secDiff / SECONDS_IN_DAY;
243
230
  }
244
231
  else if (unit === 'week') {
@@ -483,7 +470,7 @@ export class LocalTime {
483
470
  ].join('');
484
471
  }
485
472
  toString() {
486
- return String(this.unix());
473
+ return this.toISODateTime();
487
474
  }
488
475
  toJSON() {
489
476
  return this.unix();
@@ -538,29 +525,6 @@ function getWeekYear(date) {
538
525
  return year - 1;
539
526
  }
540
527
  }
541
- // function setWeekYear(
542
- // date: Date,
543
- // year: number,
544
- // ): Date {
545
- // const diff = differenceInCalendarDays(date, startOfWeekYear(date))
546
- // const fourthOfJanuary = new Date(0)
547
- // fourthOfJanuary.setFullYear(year, 0, 4)
548
- // fourthOfJanuary.setHours(0, 0, 0, 0)
549
- // date = startOfWeekYear(fourthOfJanuary)
550
- // date.setDate(date.getDate() + diff)
551
- // return date
552
- // }
553
- // function differenceInCalendarDays(
554
- // dateLeft: Date,
555
- // dateRight: Date,
556
- // ): number {
557
- // return Math.round((startOfDay(dateLeft).getTime() - startOfDay(dateRight).getTime()) / MILLISECONDS_IN_DAY)
558
- // }
559
- // function startOfDay(date: Date, mutate = false): Date {
560
- // const d = mutate ? date : new Date(date)
561
- // d.setHours(0, 0, 0, 0)
562
- // return d
563
- // }
564
528
  // based on: https://github.com/date-fns/date-fns/blob/fd6bb1a0bab143f2da068c05a9c562b9bee1357d/src/startOfWeek/index.ts
565
529
  function startOfWeek(date, mutate = false) {
566
530
  const d = mutate ? date : new Date(date);
@@ -578,3 +542,36 @@ function endOfWeek(date, mutate = false) {
578
542
  d.setDate(d.getDate() + diff);
579
543
  return d;
580
544
  }
545
+ function addMonths(d, num, mutate = false) {
546
+ if (!mutate)
547
+ d = new Date(d);
548
+ let day = d.getDate();
549
+ let month = d.getMonth() + 1 + num;
550
+ if (day < 29) {
551
+ d.setMonth(month - 1);
552
+ return d;
553
+ }
554
+ let year = d.getFullYear();
555
+ while (month > 12) {
556
+ year++;
557
+ month -= 12;
558
+ }
559
+ while (month < 1) {
560
+ year--;
561
+ month += 12;
562
+ }
563
+ const monthLen = LocalDate.getMonthLength(year, month);
564
+ if (day > monthLen)
565
+ day = monthLen;
566
+ d.setFullYear(year, month - 1, day);
567
+ return d;
568
+ }
569
+ function differenceInMonths(a, b) {
570
+ if (a.getDate() < b.getDate())
571
+ return -differenceInMonths(b, a);
572
+ const wholeMonthDiff = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth());
573
+ const anchor = addMonths(a, wholeMonthDiff).getTime();
574
+ const sign = b.getTime() - anchor >= 0 ? 1 : -1;
575
+ const anchor2 = addMonths(a, wholeMonthDiff + sign).getTime();
576
+ return -(wholeMonthDiff + ((b.getTime() - anchor) / (anchor2 - anchor)) * sign);
577
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@naturalcycles/js-lib",
3
- "version": "14.99.2",
3
+ "version": "14.100.0",
4
4
  "scripts": {
5
5
  "prepare": "husky install",
6
6
  "build-prod": "build-prod-esm-cjs",
@@ -269,7 +269,15 @@ export class LocalDate {
269
269
  if (unit === 'year') {
270
270
  let years = big.$year - small.$year
271
271
 
272
- if (big.$month < small.$month || (big.$month === small.$month && big.$day < small.$day)) {
272
+ if (
273
+ big.$month < small.$month ||
274
+ (big.$month === small.$month &&
275
+ big.$day < small.$day &&
276
+ !(
277
+ big.$day === LocalDate.getMonthLength(big.$year, big.$month) &&
278
+ small.$day === LocalDate.getMonthLength(small.$year, small.$month)
279
+ ))
280
+ ) {
273
281
  years--
274
282
  }
275
283
 
@@ -278,7 +286,12 @@ export class LocalDate {
278
286
 
279
287
  if (unit === 'month') {
280
288
  let months = (big.$year - small.$year) * 12 + (big.$month - small.$month)
281
- if (big.$day < small.$day) months--
289
+ if (big.$day < small.$day) {
290
+ const bigMonthLen = LocalDate.getMonthLength(big.$year, big.$month)
291
+ if (big.$day !== bigMonthLen || small.$day < bigMonthLen) {
292
+ months--
293
+ }
294
+ }
282
295
  return months * sign || 0
283
296
  }
284
297
 
@@ -324,21 +337,37 @@ export class LocalDate {
324
337
  $year += num
325
338
  }
326
339
 
340
+ // check month overflow
341
+ while ($month > 12) {
342
+ $year += 1
343
+ $month -= 12
344
+ }
345
+ while ($month < 1) {
346
+ $year -= 1
347
+ $month += 12
348
+ }
349
+
327
350
  // check day overflow
328
- if (unit === 'day') {
329
- if ($day < 1) {
330
- while ($day < 1) {
331
- $month -= 1
332
- if ($month < 1) {
333
- $year -= 1
334
- $month += 12
335
- }
351
+ // Applies not only for 'day' unit, but also e.g 2022-05-31 plus 1 month should be 2022-06-30 (not 31!)
352
+ if ($day < 1) {
353
+ while ($day < 1) {
354
+ $month -= 1
355
+ if ($month < 1) {
356
+ $year -= 1
357
+ $month += 12
358
+ }
336
359
 
337
- $day += LocalDate.getMonthLength($year, $month)
360
+ $day += LocalDate.getMonthLength($year, $month)
361
+ }
362
+ } else {
363
+ let monLen = LocalDate.getMonthLength($year, $month)
364
+
365
+ if (unit !== 'day') {
366
+ if ($day > monLen) {
367
+ // Case of 2022-05-31 plus 1 month should be 2022-06-30, not 31
368
+ $day = monLen
338
369
  }
339
370
  } else {
340
- let monLen = LocalDate.getMonthLength($year, $month)
341
-
342
371
  while ($day > monLen) {
343
372
  $day -= monLen
344
373
  $month += 1
@@ -352,16 +381,6 @@ export class LocalDate {
352
381
  }
353
382
  }
354
383
 
355
- // check month overflow
356
- while ($month > 12) {
357
- $year += 1
358
- $month -= 12
359
- }
360
- while ($month < 1) {
361
- $year -= 1
362
- $month += 12
363
- }
364
-
365
384
  if (mutate) {
366
385
  this.$year = $year
367
386
  this.$month = $month
@@ -56,6 +56,13 @@ export class LocalTime {
56
56
  return t
57
57
  }
58
58
 
59
+ /**
60
+ * Create LocalTime from unixTimestamp in milliseconds (not in seconds).
61
+ */
62
+ static ofMillis(millis: number): LocalTime {
63
+ return LocalTime.of(new Date(millis))
64
+ }
65
+
59
66
  /**
60
67
  * Returns null if invalid
61
68
  */
@@ -123,7 +130,9 @@ export class LocalTime {
123
130
  static fromComponents(
124
131
  c: { year: number; month: number } & Partial<LocalTimeComponents>,
125
132
  ): LocalTime {
126
- return new LocalTime(new Date(c.year, c.month - 1, c.day, c.hour, c.minute, c.second))
133
+ return new LocalTime(
134
+ new Date(c.year, c.month - 1, c.day || 1, c.hour || 0, c.minute || 0, c.second || 0),
135
+ )
127
136
  }
128
137
 
129
138
  get(unit: LocalTimeUnit): number {
@@ -227,15 +236,15 @@ export class LocalTime {
227
236
  setComponents(c: Partial<LocalTimeComponents>, mutate = false): LocalTime {
228
237
  const d = mutate ? this.$date : new Date(this.$date)
229
238
 
230
- if (c.year) {
231
- d.setFullYear(c.year)
232
- }
233
- if (c.month) {
234
- d.setMonth(c.month - 1)
235
- }
236
- if (c.day) {
237
- d.setDate(c.day)
239
+ // Year, month and day set all-at-once, to avoid 30/31 (and 28/29) mishap
240
+ if (c.day || c.month !== undefined || c.year !== undefined) {
241
+ d.setFullYear(
242
+ c.year ?? d.getFullYear(),
243
+ c.month ? c.month - 1 : d.getMonth(),
244
+ c.day || d.getDate(),
245
+ )
238
246
  }
247
+
239
248
  if (c.hour !== undefined) {
240
249
  d.setHours(c.hour)
241
250
  }
@@ -254,6 +263,12 @@ export class LocalTime {
254
263
  num *= 7
255
264
  unit = 'day'
256
265
  }
266
+
267
+ if (unit === 'year' || unit === 'month') {
268
+ const d = addMonths(this.$date, unit === 'month' ? num : num * 12, mutate)
269
+ return mutate ? this : LocalTime.of(d)
270
+ }
271
+
257
272
  return this.set(unit, this.get(unit) + num, mutate)
258
273
  }
259
274
 
@@ -271,37 +286,13 @@ export class LocalTime {
271
286
  const secDiff = (this.$date.valueOf() - date2.valueOf()) / 1000
272
287
  if (!secDiff) return 0
273
288
 
274
- if (unit === 'year' || unit === 'month') {
275
- const sign = secDiff > 0 ? 1 : -1
276
-
277
- // Put items in descending order: "big minus small"
278
- const [big, small] = sign === 1 ? [this.$date, date2] : [date2, this.$date]
279
-
280
- if (unit === 'year') {
281
- let years = big.getFullYear() - small.getFullYear()
282
- const big2 = new Date(big)
283
- const small2 = new Date(small)
284
- big2.setFullYear(1584)
285
- small2.setFullYear(1584)
286
- if (big2 < small2) years--
287
- return years * sign || 0
288
- }
289
-
290
- if (unit === 'month') {
291
- let months =
292
- (big.getFullYear() - small.getFullYear()) * 12 + big.getMonth() - small.getMonth()
293
- const big2 = new Date(big)
294
- const small2 = new Date(small)
295
- big2.setFullYear(1584, 0)
296
- small2.setFullYear(1584, 0)
297
- if (big2 < small2) months--
298
- return months * sign || 0
299
- }
300
- }
301
-
302
289
  let r
303
290
 
304
- if (unit === 'day') {
291
+ if (unit === 'year') {
292
+ r = differenceInMonths(this.getDate(), date2) / 12
293
+ } else if (unit === 'month') {
294
+ r = differenceInMonths(this.getDate(), date2)
295
+ } else if (unit === 'day') {
305
296
  r = secDiff / SECONDS_IN_DAY
306
297
  } else if (unit === 'week') {
307
298
  r = secDiff / (7 * 24 * 60 * 60)
@@ -585,7 +576,7 @@ export class LocalTime {
585
576
  }
586
577
 
587
578
  toString(): string {
588
- return String(this.unix())
579
+ return this.toISODateTime()
589
580
  }
590
581
 
591
582
  toJSON(): UnixTimestampNumber {
@@ -649,32 +640,6 @@ function getWeekYear(date: Date): number {
649
640
  }
650
641
  }
651
642
 
652
- // function setWeekYear(
653
- // date: Date,
654
- // year: number,
655
- // ): Date {
656
- // const diff = differenceInCalendarDays(date, startOfWeekYear(date))
657
- // const fourthOfJanuary = new Date(0)
658
- // fourthOfJanuary.setFullYear(year, 0, 4)
659
- // fourthOfJanuary.setHours(0, 0, 0, 0)
660
- // date = startOfWeekYear(fourthOfJanuary)
661
- // date.setDate(date.getDate() + diff)
662
- // return date
663
- // }
664
-
665
- // function differenceInCalendarDays(
666
- // dateLeft: Date,
667
- // dateRight: Date,
668
- // ): number {
669
- // return Math.round((startOfDay(dateLeft).getTime() - startOfDay(dateRight).getTime()) / MILLISECONDS_IN_DAY)
670
- // }
671
-
672
- // function startOfDay(date: Date, mutate = false): Date {
673
- // const d = mutate ? date : new Date(date)
674
- // d.setHours(0, 0, 0, 0)
675
- // return d
676
- // }
677
-
678
643
  // based on: https://github.com/date-fns/date-fns/blob/fd6bb1a0bab143f2da068c05a9c562b9bee1357d/src/startOfWeek/index.ts
679
644
  function startOfWeek(date: Date, mutate = false): Date {
680
645
  const d = mutate ? date : new Date(date)
@@ -697,3 +662,41 @@ function endOfWeek(date: Date, mutate = false): Date {
697
662
  d.setDate(d.getDate() + diff)
698
663
  return d
699
664
  }
665
+
666
+ function addMonths(d: Date, num: number, mutate = false): Date {
667
+ if (!mutate) d = new Date(d)
668
+
669
+ let day = d.getDate()
670
+ let month = d.getMonth() + 1 + num
671
+
672
+ if (day < 29) {
673
+ d.setMonth(month - 1)
674
+ return d
675
+ }
676
+
677
+ let year = d.getFullYear()
678
+
679
+ while (month > 12) {
680
+ year++
681
+ month -= 12
682
+ }
683
+ while (month < 1) {
684
+ year--
685
+ month += 12
686
+ }
687
+
688
+ const monthLen = LocalDate.getMonthLength(year, month)
689
+ if (day > monthLen) day = monthLen
690
+
691
+ d.setFullYear(year, month - 1, day)
692
+ return d
693
+ }
694
+
695
+ function differenceInMonths(a: Date, b: Date): number {
696
+ if (a.getDate() < b.getDate()) return -differenceInMonths(b, a)
697
+ const wholeMonthDiff = (b.getFullYear() - a.getFullYear()) * 12 + (b.getMonth() - a.getMonth())
698
+ const anchor = addMonths(a, wholeMonthDiff).getTime()
699
+ const sign = b.getTime() - anchor >= 0 ? 1 : -1
700
+ const anchor2 = addMonths(a, wholeMonthDiff + sign).getTime()
701
+ return -(wholeMonthDiff + ((b.getTime() - anchor) / (anchor2 - anchor)) * sign)
702
+ }