atomic-fns 1.1.6 → 1.2.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.
Files changed (51) hide show
  1. package/README.md +51 -12
  2. package/dist/collections/BTree.d.ts +11 -10
  3. package/dist/collections/BTree.js +58 -80
  4. package/dist/collections/Heap.d.ts +2 -2
  5. package/dist/collections/Heap.js +1 -1
  6. package/dist/collections/LRUCache.d.ts +8 -5
  7. package/dist/collections/LRUCache.js +24 -11
  8. package/dist/collections/SortedMap.d.ts +4 -4
  9. package/dist/collections/SortedMap.js +4 -3
  10. package/dist/collections/SortedSet.d.ts +2 -2
  11. package/dist/collections/SortedTree.d.ts +9 -9
  12. package/dist/collections/SortedTree.js +7 -6
  13. package/dist/collections/SplayTree.d.ts +17 -15
  14. package/dist/collections/SplayTree.js +42 -29
  15. package/dist/collections/Trie.d.ts +2 -2
  16. package/dist/collections/Trie.js +26 -26
  17. package/dist/collections/deque.d.ts +11 -11
  18. package/dist/collections/deque.js +17 -10
  19. package/dist/collections/frozenset.js +4 -1
  20. package/dist/collections/index.d.ts +30 -48
  21. package/dist/collections/index.js +210 -82
  22. package/dist/decimal/index.d.ts +7 -5
  23. package/dist/decimal/index.js +63 -28
  24. package/dist/functools/index.d.ts +10 -34
  25. package/dist/functools/index.js +25 -57
  26. package/dist/globals/index.d.ts +5 -22
  27. package/dist/globals/index.js +24 -40
  28. package/dist/i18n/currency.d.ts +10 -7
  29. package/dist/i18n/currency.js +22 -7
  30. package/dist/i18n/date/index.d.ts +22 -16
  31. package/dist/i18n/date/index.js +89 -67
  32. package/dist/i18n/date/utils.d.ts +6 -7
  33. package/dist/i18n/date/utils.js +15 -113
  34. package/dist/i18n/duration.d.ts +13 -12
  35. package/dist/i18n/duration.js +38 -32
  36. package/dist/i18n/format.d.ts +10 -2
  37. package/dist/i18n/format.js +100 -217
  38. package/dist/i18n/index.d.ts +2 -1
  39. package/dist/itertools/index.d.ts +21 -25
  40. package/dist/itertools/index.js +41 -44
  41. package/dist/math/index.d.ts +12 -29
  42. package/dist/math/index.js +16 -33
  43. package/dist/mod.d.ts +1 -0
  44. package/dist/mod.js +1 -0
  45. package/dist/operators/index.d.ts +3 -3
  46. package/dist/operators/index.js +62 -38
  47. package/dist/random/index.js +9 -6
  48. package/dist/string/index.d.ts +1 -1
  49. package/dist/string/index.js +2 -4
  50. package/dist/types.d.ts +3 -3
  51. package/package.json +16 -21
@@ -18,13 +18,16 @@ export class CustomError extends Error {
18
18
  /**
19
19
  * Returns a new CustomError with the specified message.
20
20
  * @param {string} message
21
+ * @param {ErrorOptions} [options] Standard error options such as `cause`.
21
22
  * @constructor
22
23
  */
23
- constructor(message) {
24
- super(message);
24
+ constructor(message, options) {
25
+ super(message, options);
25
26
  this.name = this.constructor.name;
26
- if (typeof Error.captureStackTrace === 'function') {
27
- Error.captureStackTrace(this, this.constructor);
27
+ // V8 and Bun have it, and only Node's types declare it, which JSR's type check does not load
28
+ const errorType = Error;
29
+ if (typeof errorType.captureStackTrace === 'function') {
30
+ errorType.captureStackTrace(this, this.constructor);
28
31
  }
29
32
  else {
30
33
  this.stack = new Error(message).stack;
@@ -90,7 +93,7 @@ export function type(value) {
90
93
  return 'error';
91
94
  if (value[Symbol.toStringTag])
92
95
  return value[Symbol.toStringTag];
93
- const className = str(value);
96
+ const className = Object.prototype.toString.call(value);
94
97
  if (className === '[object Window]')
95
98
  return 'object';
96
99
  if (className.endsWith('Array]') || Array.isArray(value)) {
@@ -150,10 +153,6 @@ export function isObject(x) {
150
153
  export function isString(x) {
151
154
  return type(x) === 'string';
152
155
  }
153
- /** Check if value is an Array type. */
154
- export function isArray(x) {
155
- return Array.isArray(x);
156
- }
157
156
  /**
158
157
  * Check if value is Array-like type.
159
158
  * A value is considered array-like if it's not a function and has a `.length` number property.
@@ -166,10 +165,6 @@ export function isArrayLike(x) {
166
165
  export function isNumber(x) {
167
166
  return type(x) === 'number';
168
167
  }
169
- /** Check if value is an integer number type. */
170
- export function isInteger(x) {
171
- return type(x) === 'number' && Math.trunc(x) === x;
172
- }
173
168
  /** Check if value is a bigint type. */
174
169
  export function isBigint(x) {
175
170
  return type(x) === 'bigint';
@@ -180,15 +175,15 @@ export function isPromise(x) {
180
175
  }
181
176
  /** Check if value is a function type. */
182
177
  export function isFunction(x) {
183
- return x?.constructor === Function;
178
+ return typeof x === 'function';
184
179
  }
185
180
  /** Check if value is an async function type. */
186
181
  export function isAsyncFunction(x) {
187
- return x?.constructor.name === 'AsyncFunction';
182
+ return x?.constructor?.name === 'AsyncFunction';
188
183
  }
189
184
  /** Check if value is a generator function type. */
190
185
  export function isGenerator(x) {
191
- return x?.constructor.constructor?.name === 'GeneratorFunction';
186
+ return x?.constructor?.constructor?.name === 'GeneratorFunction';
192
187
  }
193
188
  /** Check if value is `null` or `undefined`. */
194
189
  export function isNull(x) {
@@ -282,7 +277,7 @@ uuid()
282
277
  */
283
278
  export function uuid() {
284
279
  const u = Date.now().toString(16) + Math.random().toString(16) + '0'.repeat(16);
285
- return [u.substr(0, 8), u.substr(8, 4), '4000-8' + u.substr(13, 3), u.substr(16, 12)].join('-');
280
+ return [u.slice(0, 8), u.slice(8, 12), `4000-8${u.slice(13, 16)}`, u.slice(16, 28)].join('-');
286
281
  }
287
282
  /**
288
283
  * Checks if `obj.key` is a function, and calls it with any given `args`.
@@ -292,7 +287,7 @@ export function uuid() {
292
287
  * @return `obj.key(...args)` or `undefined`.
293
288
  */
294
289
  export function call(obj, key, ...args) {
295
- if (isFunction(get(key, obj))) {
290
+ if (isFunction(get([key], obj))) {
296
291
  return obj[key](...args);
297
292
  }
298
293
  }
@@ -302,25 +297,7 @@ export function chr(x) {
302
297
  }
303
298
  /** Convert character to Unicode code point */
304
299
  export function ord(x) {
305
- return x.charCodeAt(0);
306
- }
307
- /**
308
- * Returns an array of the own enumerable property names of `object`.
309
- * @param object The given object.
310
- * @returns The array of object keys.
311
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys Object.keys()}
312
- */
313
- export function keys(object) {
314
- return Object.keys(object);
315
- }
316
- /**
317
- * Returns an array of the own enumerable property values of `object`.
318
- * @param object The given object.
319
- * @returns The array of object keys.
320
- * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values Object.values()}
321
- */
322
- export function values(object) {
323
- return Object.values(object);
300
+ return x.codePointAt(0) ?? Number.NaN;
324
301
  }
325
302
  /**
326
303
  * Check if a given property is present in a given object.
@@ -329,7 +306,7 @@ export function values(object) {
329
306
  * @returns `true` if the object has the property name.
330
307
  */
331
308
  export function has(obj, attr) {
332
- return obj && attr in obj;
309
+ return obj != null && attr in Object(obj);
333
310
  }
334
311
  export function get(key, obj, value) {
335
312
  if (obj == null)
@@ -372,19 +349,26 @@ export function del(attr, x) {
372
349
  export function set(attr, obj, value, writable = false, enumerable = false) {
373
350
  return Object.defineProperty(obj, attr, {
374
351
  value,
352
+ configurable: true,
375
353
  enumerable,
376
354
  writable
377
355
  });
378
356
  }
379
357
  export const HASH_KEY = Symbol.for('HASH_KEY');
358
+ const sealedHashes = new WeakMap();
380
359
  export function hash(obj) {
381
360
  if (typeof obj === 'string') {
382
361
  return hashCode(obj);
383
362
  }
384
- if (typeof obj === 'object') {
385
- if (get(HASH_KEY, obj) == null) {
363
+ if (typeof obj === 'object' && obj !== null) {
364
+ if (get(HASH_KEY, obj) != null) {
386
365
  return obj[HASH_KEY];
387
366
  }
367
+ if (!Object.isExtensible(obj)) {
368
+ if (!sealedHashes.has(obj))
369
+ sealedHashes.set(obj, uniqueId());
370
+ return sealedHashes.get(obj);
371
+ }
388
372
  obj[HASH_KEY] = uniqueId();
389
373
  return obj[HASH_KEY];
390
374
  }
@@ -6,19 +6,22 @@ export declare class Currency extends Decimal {
6
6
  private readonly locale;
7
7
  private readonly currency;
8
8
  private readonly numberFormat;
9
+ private readonly overrides;
9
10
  private readonly options;
10
11
  constructor(amount: any, currency: string, locale?: string, opts?: Intl.NumberFormatOptions);
11
- clone(n: any): Currency;
12
+ clone(n: Currency): Currency;
12
13
  negated(): Currency;
13
- add(x: any): Currency;
14
- sub(x: any): Currency;
15
- div(x: any): Currency;
16
- mul(x: any): Currency;
17
- toDecimal(): any;
14
+ add(x: Currency | string): Currency;
15
+ sub(x: Currency | string): Currency;
16
+ div(x: Currency | Decimal | string | number): Currency;
17
+ mul(x: Currency | Decimal): Currency;
18
+ toDecimal(): Decimal;
19
+ /** `Intl` reads a decimal string exactly, while a `Number` past 2^53 has lost digits. */
20
+ private formattable;
18
21
  toString(): string;
19
22
  format(opts?: Intl.NumberFormatOptions, locale?: string): string;
20
23
  displayName(): string;
21
24
  accounting(): string;
22
25
  precision(digits?: number): string;
23
- eq(x: any): any;
26
+ eq(x: any): boolean;
24
27
  }
@@ -1,6 +1,8 @@
1
1
  import { defaults } from '../collections/index.js';
2
2
  import { isObject } from '../globals/index.js';
3
3
  import { Decimal } from '../decimal/index.js';
4
+ /** `Intl` options that set the digits shown. Some reject each other, so they replace as a group. */
5
+ const DIGIT_OPTION = /^(minimum|maximum)(Fraction|Significant)Digits$|^rounding(Increment|Priority)$/;
4
6
  /**
5
7
  * Currency represents a local currency amount that can be formatted using the `Intl` apis.
6
8
  */
@@ -8,6 +10,7 @@ export class Currency extends Decimal {
8
10
  locale;
9
11
  currency;
10
12
  numberFormat;
13
+ overrides;
11
14
  options = {
12
15
  style: 'currency',
13
16
  currencyDisplay: 'symbol',
@@ -19,16 +22,17 @@ export class Currency extends Decimal {
19
22
  opts = locale;
20
23
  locale = undefined;
21
24
  }
22
- const systemLocale = new Intl.NumberFormat(locale, defaults(opts, { currency }, this.options));
25
+ const systemLocale = new Intl.NumberFormat(locale, defaults({}, opts, { currency }, this.options));
23
26
  const resolved = systemLocale.resolvedOptions();
24
27
  this.numberFormat = systemLocale;
25
28
  this.options.currency = currency;
29
+ this.overrides = defaults({}, opts);
26
30
  this.currency = resolved.currency;
27
31
  this.locale = resolved.locale;
28
32
  }
29
33
  // @ts-expect-error overrides
30
34
  clone(n) {
31
- return new Currency(n, this.currency, this.locale, this.options);
35
+ return new Currency(n === undefined ? this : n, this.currency, this.locale, this.overrides);
32
36
  }
33
37
  // @ts-expect-error overrides
34
38
  negated() {
@@ -58,14 +62,25 @@ export class Currency extends Decimal {
58
62
  toDecimal() {
59
63
  return super.clone.call(this);
60
64
  }
61
- toString() {
65
+ /** `Intl` reads a decimal string exactly, while a `Number` past 2^53 has lost digits. */
66
+ formattable() {
62
67
  const number = this.toNumber();
63
- return this.numberFormat.format(number);
68
+ if (Math.abs(number) <= Number.MAX_SAFE_INTEGER)
69
+ return number;
70
+ return Decimal.prototype.toString.call(this);
71
+ }
72
+ toString() {
73
+ return this.numberFormat.format(this.formattable());
64
74
  }
65
75
  format(opts = {}, locale) {
66
- const number = this.toNumber();
67
- opts = defaults(opts, this.options);
68
- return new Intl.NumberFormat(locale ?? this.locale, opts).format(number);
76
+ const overrides = { ...this.overrides };
77
+ if (Object.keys(opts).some((key) => DIGIT_OPTION.test(key) && opts[key] !== undefined)) {
78
+ for (const key in overrides)
79
+ if (DIGIT_OPTION.test(key))
80
+ delete overrides[key];
81
+ }
82
+ opts = defaults({}, opts, overrides, this.options);
83
+ return new Intl.NumberFormat(locale ?? this.locale, opts).format(this.formattable());
69
84
  }
70
85
  displayName() {
71
86
  return this.format({ currencyDisplay: 'name' });
@@ -1,10 +1,9 @@
1
- import { Duration, DurationUnit, TDuration } from '../duration.js';
1
+ import { Duration, type DurationUnit, type TDuration } from '../duration.js';
2
2
  import { asDate, formatDate, strftime } from '../format.js';
3
- import { DateObject, daysInMonth, daysInYear, isLeapYear, maxDate, minDate, weeksInYear } from './utils.js';
3
+ import { type DateObject, daysInMonth, daysInYear, isLeapYear, maxDate, minDate, weeksInYear } from './utils.js';
4
4
  export { asDate, daysInMonth, daysInYear, formatDate, isLeapYear, maxDate, minDate, strftime, weeksInYear };
5
- export { DateObject };
6
- export declare type DateInput = number | string | Date;
7
- export declare type DateLike = DateInput | IntlDate | DateObject;
5
+ export type { DateObject };
6
+ export type DateLike = number | string | Date | IntlDate | DateObject;
8
7
  /**
9
8
  * Creates a date tied to a given locale (default system locale) which can be formatted in that locale's language using the native {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl Intl Apis} directly or using a formatting string.
10
9
  * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl Intl Apis}
@@ -15,7 +14,7 @@ export declare class IntlDate {
15
14
  intlRelativeFormat: Intl.RelativeTimeFormat;
16
15
  /** Parses the provided value as UTC date or returns the current UTC date. */
17
16
  static UTC(value?: DateLike, locale?: string): IntlDate;
18
- /** Returns the current local date and time.*/
17
+ /** Returns the current local date and time. */
19
18
  static now(): IntlDate;
20
19
  /** Creates a new date from a Unix timestamp (seconds since the unix epoch) */
21
20
  static unix(seconds: number): IntlDate;
@@ -53,14 +52,14 @@ export declare class IntlDate {
53
52
  dayOfYear(): number;
54
53
  /** Returns `true` if this date's year is a leap year. */
55
54
  isLeapYear(): boolean;
56
- /** Gets the number of weeks according to locale in the current year. */
55
+ /** Gets the number of Monday-first weeks in the current year, where week 1 is the first full week. */
57
56
  weeksInYear(): number;
58
57
  /** Gets the number of weeks in the current year, according to ISO weeks. */
59
58
  isoWeeksInYear(): number;
60
59
  /** Returns the current week of the year. */
61
- week(): any;
60
+ week(): number;
62
61
  /** Returns the ISO week of the year. */
63
- isoWeek(): any;
62
+ isoWeek(): number;
64
63
  /**
65
64
  * Returns a localized string representation of this date.
66
65
  * @param {Intl.DateTimeFormatOptions} opts The options to use for the format
@@ -129,13 +128,13 @@ export declare class IntlDate {
129
128
  /** Returns the time part formatted as ISO8601.
130
129
  * @example
131
130
  ```js
132
- new IntlDate().toISOTime() // 'T22:44:30.652Z'
131
+ new IntlDate().toISOTime() // '22:44:30.652Z'
133
132
  ```
134
133
  */
135
134
  toISOTime(): string;
136
135
  /**
137
136
  * Returns an object containing year, month, day-of-month, hours, minutes, seconds, milliseconds.
138
- * @returns {Object} An object like `{year, month, date, hours, minutes, seconds, ms}`
137
+ * @returns {Object} An object like `{year, month, day, hour, minute, second, millisecond}`
139
138
  */
140
139
  toObject(): DateObject;
141
140
  /** Returns the number of milliseconds since the Unix Epoch (January 1, 1970 UTC) */
@@ -186,19 +185,26 @@ export declare class IntlDate {
186
185
  diff(other: DateLike, unit?: DurationUnit, exact?: boolean): number;
187
186
  add(duration: TDuration, exact?: boolean): IntlDate;
188
187
  subtract(duration: TDuration, exact?: boolean): IntlDate;
189
- startOf(unit: DurationUnit): IntlDate;
190
- endOf(unit: DurationUnit): IntlDate;
188
+ startOf(unit: DurationUnit): any;
189
+ endOf(unit: DurationUnit): any;
191
190
  clone(date?: DateLike): IntlDate;
192
191
  static max(...args: any[]): any;
193
192
  static min(...args: any[]): any;
194
193
  inspect(): string;
195
194
  }
196
- export declare function addDuration(date: DateInput, duration: number | Duration | TDuration, exact?: boolean): Date;
195
+ /**
196
+ * Add the specified years, months, weeks, days, hours, minutes and seconds to the given date
197
+ * @param date
198
+ * @param duration
199
+ * @param exact Kept for callers that pass it, it no longer changes the result.
200
+ * @returns
201
+ */
202
+ export declare function addDuration(date: number | string | Date, duration: number | Duration | TDuration, exact?: boolean): Date;
197
203
  /**
198
204
  * Subtract the specified years, months, weeks, days, hours, minutes and seconds from the given date.
199
205
  * @param date
200
206
  * @param duration
201
- * @param exact
207
+ * @param exact Kept for callers that pass it, it no longer changes the result.
202
208
  * @returns
203
209
  */
204
- export declare function subDuration(date: DateInput, duration: number | Duration | TDuration, exact?: boolean): Date;
210
+ export declare function subDuration(date: number | string | Date, duration: number | Duration | TDuration, exact?: boolean): Date;
@@ -1,6 +1,6 @@
1
1
  import { isObject, ValueError } from '../../globals/index.js';
2
2
  import { Duration, UNITS_PLURAL } from '../duration.js';
3
- import { asDate, formatDate, strftime } from '../format.js';
3
+ import { asDate, formatDate, fromFields, strftime } from '../format.js';
4
4
  import { asNumber, dayOfYear, daysInMonth, daysInYear, isLeapYear, maxDate, minDate, weekOfYear, weeksInYear } from './utils.js';
5
5
  // Export date functions
6
6
  export { asDate, daysInMonth, daysInYear, formatDate, isLeapYear, maxDate, minDate, strftime, weeksInYear };
@@ -17,7 +17,7 @@ export class IntlDate {
17
17
  static UTC(value, locale) {
18
18
  return new IntlDate(value, { utc: true, locale });
19
19
  }
20
- /** Returns the current local date and time.*/
20
+ /** Returns the current local date and time. */
21
21
  static now() {
22
22
  return new IntlDate();
23
23
  }
@@ -45,16 +45,8 @@ export class IntlDate {
45
45
  else if (isObject(obj)) {
46
46
  let { year, month, day, hour, minute, second, millisecond } = obj;
47
47
  // NOTE month is from 1-12, not from 0-11
48
- if (typeof month === 'number') {
49
- month -= 1;
50
- }
51
- if (utc) {
52
- // from UTC to current
53
- this._date = new Date(Date.UTC(year, month, day || 1, hour || 0, minute || 0, second || 0, millisecond || 0));
54
- }
55
- else {
56
- this._date = new Date(year, month, day || 1, hour || 0, minute || 0, second || 0, millisecond || 0);
57
- }
48
+ month = (month ?? 1) - 1;
49
+ this._date = fromFields(utc, Number(year), month, day || 1, hour || 0, minute || 0, second || 0, millisecond || 0);
58
50
  }
59
51
  else {
60
52
  this._date = asDate(obj, utc);
@@ -113,7 +105,7 @@ export class IntlDate {
113
105
  isLeapYear() {
114
106
  return isLeapYear(this.year);
115
107
  }
116
- /** Gets the number of weeks according to locale in the current year. */
108
+ /** Gets the number of Monday-first weeks in the current year, where week 1 is the first full week. */
117
109
  weeksInYear() {
118
110
  return weeksInYear(this.year);
119
111
  }
@@ -208,20 +200,20 @@ export class IntlDate {
208
200
  ```
209
201
  */
210
202
  toISODate() {
211
- return this._date.toISOString().slice(0, 10);
203
+ return this._date.toISOString().split('T')[0];
212
204
  }
213
205
  /** Returns the time part formatted as ISO8601.
214
206
  * @example
215
207
  ```js
216
- new IntlDate().toISOTime() // 'T22:44:30.652Z'
208
+ new IntlDate().toISOTime() // '22:44:30.652Z'
217
209
  ```
218
210
  */
219
211
  toISOTime() {
220
- return this._date.toISOString().slice(11);
212
+ return this._date.toISOString().split('T')[1];
221
213
  }
222
214
  /**
223
215
  * Returns an object containing year, month, day-of-month, hours, minutes, seconds, milliseconds.
224
- * @returns {Object} An object like `{year, month, date, hours, minutes, seconds, ms}`
216
+ * @returns {Object} An object like `{year, month, day, hour, minute, second, millisecond}`
225
217
  */
226
218
  toObject() {
227
219
  return {
@@ -248,7 +240,10 @@ export class IntlDate {
248
240
  }
249
241
  /** Returns the timezone GMT offset as a string */
250
242
  zone() {
251
- return this.format('ZZ');
243
+ if (!this.isValid())
244
+ return INVALID_DATE_STRING;
245
+ const zoneFormat = new Intl.DateTimeFormat(this.locale, { timeZoneName: 'longOffset' });
246
+ return zoneFormat.formatToParts(this._date).find((part) => part.type === 'timeZoneName').value;
252
247
  }
253
248
  /** Returns the difference in `minutes` between this date and UTC */
254
249
  utcOffset() {
@@ -308,7 +303,7 @@ export class IntlDate {
308
303
  compare(other) {
309
304
  if (!this.isValid())
310
305
  return 1;
311
- let dt = new IntlDate(other);
306
+ const dt = new IntlDate(other);
312
307
  if (!dt.isValid())
313
308
  return -1;
314
309
  return this.getTime() - dt.getTime();
@@ -317,13 +312,18 @@ export class IntlDate {
317
312
  if (!this.isValid())
318
313
  return this;
319
314
  const normalized = normalizeObject(values, normalizeUnit);
320
- let mixed = { ...this.toObject(), ...normalized };
315
+ const mixed = { ...this.toObject(), ...normalized };
321
316
  // if we didn't set the day but we ended up on an overflow date,
322
317
  // use the last day of the right month
323
318
  if (normalized.day === undefined) {
324
- mixed.day = Math.min(daysInMonth(this._date), mixed.day);
319
+ const target = new Date(this._date);
320
+ target.setFullYear(mixed.year, mixed.month - 1, 1);
321
+ mixed.day = Math.min(daysInMonth(target), mixed.day);
325
322
  }
326
- return this.clone(mixed);
323
+ const result = this.clone(mixed);
324
+ return normalized.weekday === undefined
325
+ ? result
326
+ : result.add({ days: normalized.weekday - result.dayOfWeek });
327
327
  }
328
328
  get relativeFormat() {
329
329
  if (this.intlRelativeFormat)
@@ -332,27 +332,42 @@ export class IntlDate {
332
332
  return new Intl.RelativeTimeFormat(this.locale);
333
333
  }
334
334
  relativeTime(n, unit = 'seconds') {
335
- return this.relativeFormat.format(n, unit);
335
+ return Number.isNaN(n) ? INVALID_DATE_STRING : this.relativeFormat.format(n, unit);
336
336
  }
337
337
  fromNow(unit = 'seconds') {
338
- let diff = this.diff(new Date(), unit);
338
+ const diff = this.diff(new Date(), unit);
339
339
  return this.relativeTime(diff, unit);
340
340
  }
341
341
  from(other, unit = 'seconds') {
342
- let diff = this.diff(other, unit);
342
+ const diff = this.diff(other, unit);
343
343
  return this.relativeTime(diff, unit);
344
344
  }
345
345
  toNow(unit = 'seconds') {
346
- let diff = this.diff(new Date(), unit);
346
+ const diff = this.diff(new Date(), unit);
347
347
  return this.relativeTime(-diff, unit);
348
348
  }
349
349
  to(other, unit = 'seconds') {
350
- let diff = this.diff(other, unit);
350
+ const diff = this.diff(other, unit);
351
351
  return this.relativeTime(-diff, unit);
352
352
  }
353
353
  diff(other, unit = 'milliseconds', exact = false) {
354
- let dt = new IntlDate(other);
355
- let dur = new Duration(this.getTime() - dt.getTime(), exact);
354
+ const dt = new IntlDate(other);
355
+ const normalizedUnit = UNITS_PLURAL[unit.toLowerCase()];
356
+ const monthsInUnit = MONTHS_IN_UNIT[normalizedUnit];
357
+ if (monthsInUnit && !exact) {
358
+ // whole months are the ones `add` can take from `other` without passing this date
359
+ let months = (this.year - dt.year) * 12 + this.month - dt.month;
360
+ const sign = Math.sign(this.getTime() - dt.getTime());
361
+ if (sign * (dt.add({ months }).getTime() - this.getTime()) > 0)
362
+ months -= sign;
363
+ return Math.trunc(months / monthsInUnit);
364
+ }
365
+ let elapsed = this.getTime() - dt.getTime();
366
+ if (normalizedUnit === 'days' || normalizedUnit === 'weeks') {
367
+ // a calendar day can last 23 or 25 hours, so measure it on the wall clock
368
+ elapsed += (dt._date.getTimezoneOffset() - this._date.getTimezoneOffset()) * 6e4;
369
+ }
370
+ const dur = new Duration(elapsed, exact);
356
371
  let total = dur.total(unit);
357
372
  if (!exact)
358
373
  total = Math.trunc(total);
@@ -367,7 +382,8 @@ export class IntlDate {
367
382
  startOf(unit) {
368
383
  if (!this.isValid())
369
384
  return this;
370
- const obj = {}, normalizedUnit = UNITS_PLURAL[unit.toLowerCase()];
385
+ const obj = {};
386
+ const normalizedUnit = UNITS_PLURAL[unit.toLowerCase()];
371
387
  switch (normalizedUnit) {
372
388
  case 'years':
373
389
  obj.month = 1;
@@ -391,16 +407,22 @@ export class IntlDate {
391
407
  break;
392
408
  case 'milliseconds':
393
409
  break;
394
- // no default, invalid units throw in normalizeUnit()
410
+ // no default, an unknown unit changes nothing
395
411
  }
396
412
  if (normalizedUnit === 'weeks') {
397
- obj.weekday = 1;
413
+ return this.subtract({ days: this.dayOfWeek - 1 }).startOf('days');
398
414
  }
399
415
  if (normalizedUnit === 'quarters') {
400
416
  const q = Math.ceil(this.month / 3);
401
417
  obj.month = (q - 1) * 3 + 1;
402
418
  }
403
- return this.set(obj);
419
+ const start = this.set(obj);
420
+ if (obj.hour !== undefined)
421
+ return start;
422
+ // Date reads a repeated wall clock as its first pass, this date may be on the second
423
+ const offset = this._date.getTimezoneOffset();
424
+ const kept = new Date(start.getTime() + (offset - start._date.getTimezoneOffset()) * 6e4);
425
+ return kept.getTimezoneOffset() === offset ? this.clone(kept) : start;
404
426
  }
405
427
  endOf(unit) {
406
428
  return this.isValid()
@@ -419,57 +441,56 @@ export class IntlDate {
419
441
  return minDate.apply(undefined, args);
420
442
  }
421
443
  inspect() {
422
- return `new IntlDate("${this.toISOString()}")`;
444
+ return this.isValid() ? `new IntlDate("${this.toISOString()}")` : 'new IntlDate(NaN)';
423
445
  }
424
446
  }
425
- // Check if the new offset is different because we crossed to DST
426
- function fixOffset(startDate, endDate) {
427
- let startOffset = startDate.getTimezoneOffset();
428
- let endOffset = endDate.getTimezoneOffset();
429
- if (startOffset === endOffset)
430
- return new Date(endDate);
431
- let postoffset = (startOffset - endOffset) * 60 * 1000;
432
- return new Date(+endDate + postoffset);
433
- }
447
+ /**
448
+ * Add the specified years, months, weeks, days, hours, minutes and seconds to the given date
449
+ * @param date
450
+ * @param duration
451
+ * @param exact Kept for callers that pass it, it no longer changes the result.
452
+ * @returns
453
+ */
434
454
  export function addDuration(date, duration, exact = false) {
435
- // Get the current offset of this date timezone
436
- let newDate = new Date(date);
437
- // Get the number of ticks forward
438
- let { values } = new Duration(duration, exact).normalize();
439
- // Apply any time changes that may have happened
440
- for (const unit in values) {
441
- setDateUnit(newDate, unit, Math.floor(values[unit]));
442
- }
443
- return fixOffset(new Date(date), newDate);
455
+ const newDate = asDate(date);
456
+ const { values } = new Duration(duration, exact);
457
+ const { years = 0, quarters = 0, months = 0, weeks = 0, days = 0 } = values;
458
+ // largest unit first and all the months in one step, so a clamped day cannot carry over
459
+ setDateUnit(newDate, 'months', Math.trunc(years * 12 + quarters * 3 + Number(months)));
460
+ setDateUnit(newDate, 'days', Math.trunc(weeks * 7 + Number(days)));
461
+ for (const unit of ['hours', 'minutes', 'seconds', 'milliseconds']) {
462
+ setDateUnit(newDate, unit, Number(values[unit] ?? 0));
463
+ }
464
+ return newDate;
465
+ }
466
+ function addMonths(date, months) {
467
+ const target = new Date(date);
468
+ target.setFullYear(date.getFullYear(), date.getMonth() + months, 1);
469
+ const day = Math.min(date.getDate(), daysInMonth(target));
470
+ date.setFullYear(target.getFullYear(), target.getMonth(), day);
444
471
  }
445
472
  function setDateUnit(date, unit, value) {
473
+ // writing a date field again would move a repeated hour to its first pass
474
+ if (value === 0)
475
+ return date;
446
476
  switch (unit) {
447
- case 'years':
448
- date.setFullYear(date.getFullYear() + value);
449
- break;
450
- case 'quarters':
451
- date.setMonth(date.getMonth() + value * 3);
452
- break;
453
477
  case 'months':
454
- date.setMonth(date.getMonth() + value);
455
- break;
456
- case 'weeks':
457
- date.setDate(date.getDate() + value * 7);
478
+ addMonths(date, value);
458
479
  break;
459
480
  case 'days':
460
481
  date.setDate(date.getDate() + value);
461
482
  break;
462
483
  case 'hours':
463
- date.setHours(date.getHours() + value);
484
+ date.setTime(date.getTime() + value * 36e5);
464
485
  break;
465
486
  case 'minutes':
466
- date.setMinutes(date.getMinutes() + value);
487
+ date.setTime(date.getTime() + value * 6e4);
467
488
  break;
468
489
  case 'seconds':
469
- date.setSeconds(date.getSeconds() + value);
490
+ date.setTime(date.getTime() + value * 1e3);
470
491
  break;
471
492
  case 'milliseconds':
472
- date.setMilliseconds(date.getMilliseconds() + value);
493
+ date.setTime(date.getTime() + value);
473
494
  break;
474
495
  default:
475
496
  break;
@@ -480,12 +501,13 @@ function setDateUnit(date, unit, value) {
480
501
  * Subtract the specified years, months, weeks, days, hours, minutes and seconds from the given date.
481
502
  * @param date
482
503
  * @param duration
483
- * @param exact
504
+ * @param exact Kept for callers that pass it, it no longer changes the result.
484
505
  * @returns
485
506
  */
486
507
  export function subDuration(date, duration, exact = false) {
487
508
  return addDuration(date, new Duration(duration, exact).negated(), exact);
488
509
  }
510
+ const MONTHS_IN_UNIT = { years: 12, quarters: 3, months: 1 };
489
511
  const UNITS_SINGULAR = {
490
512
  year: 'year',
491
513
  years: 'year',