@fr0st/datetime 6.1.1 → 8.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,19 +1,20 @@
1
1
  import { getData, makeFormatter } from './../factory.js';
2
+ import { valuesRegExp } from './../helpers.js';
2
3
 
3
4
  /**
4
- * DateFormatter Values
5
+ * Formatter value caches.
5
6
  */
6
7
 
7
8
  /**
8
- * Get cached day period values.
9
+ * Gets cached localized day-period labels.
9
10
  * @param {string} locale The locale.
10
11
  * @param {string} [type=long] The formatting type.
11
- * @return {array} The cached values.
12
+ * @return {string[]} The localized day-period labels.
12
13
  */
13
14
  export function getDayPeriods(locale, type = 'long') {
14
15
  return getData(
15
16
  `periods.${locale}.${type}`,
16
- (_) => {
17
+ () => {
17
18
  const dayPeriodFormatter = makeFormatter(locale, { hour: 'numeric', hourCycle: 'h11' });
18
19
  return new Array(2)
19
20
  .fill()
@@ -27,16 +28,16 @@ export function getDayPeriods(locale, type = 'long') {
27
28
  };
28
29
 
29
30
  /**
30
- * Get cached day values.
31
+ * Gets cached localized weekday labels.
31
32
  * @param {string} locale The locale.
32
33
  * @param {string} [type=long] The formatting type.
33
- * @param {Boolean} [standalone=true] Whether the values are standalone.
34
- * @return {array} The cached values.
34
+ * @param {boolean} [standalone=true] Whether the values are standalone.
35
+ * @return {string[]} The localized weekday labels.
35
36
  */
36
37
  export function getDays(locale, type = 'long', standalone = true) {
37
38
  return getData(
38
39
  `days.${locale}.${type}.${standalone}`,
39
- (_) => {
40
+ () => {
40
41
  if (standalone) {
41
42
  const dayFormatter = makeFormatter(locale, { weekday: type });
42
43
  return new Array(7)
@@ -59,15 +60,15 @@ export function getDays(locale, type = 'long', standalone = true) {
59
60
  };
60
61
 
61
62
  /**
62
- * Get cached era values.
63
+ * Gets cached localized era labels.
63
64
  * @param {string} locale The locale.
64
65
  * @param {string} [type=long] The formatting type.
65
- * @return {array} The cached values.
66
+ * @return {string[]} The localized era labels.
66
67
  */
67
68
  export function getEras(locale, type = 'long') {
68
69
  return getData(
69
70
  `eras.${locale}.${type}`,
70
- (_) => {
71
+ () => {
71
72
  const eraFormatter = makeFormatter(locale, { era: type });
72
73
  return new Array(2)
73
74
  .fill()
@@ -81,16 +82,16 @@ export function getEras(locale, type = 'long') {
81
82
  };
82
83
 
83
84
  /**
84
- * Get cached month values.
85
+ * Gets cached localized month labels.
85
86
  * @param {string} locale The locale.
86
87
  * @param {string} [type=long] The formatting type.
87
- * @param {Boolean} [standalone=true] Whether the values are standalone.
88
- * @return {array} The cached values.
88
+ * @param {boolean} [standalone=true] Whether the values are standalone.
89
+ * @return {string[]} The localized month labels.
89
90
  */
90
91
  export function getMonths(locale, type = 'long', standalone = true) {
91
92
  return getData(
92
93
  `months.${locale}.${type}.${standalone}`,
93
- (_) => {
94
+ () => {
94
95
  if (standalone) {
95
96
  const monthFormatter = makeFormatter(locale, { month: type });
96
97
  return new Array(12)
@@ -113,14 +114,14 @@ export function getMonths(locale, type = 'long', standalone = true) {
113
114
  };
114
115
 
115
116
  /**
116
- * Get cached number values.
117
+ * Gets cached localized digit glyphs.
117
118
  * @param {string} locale The locale.
118
- * @return {array} The cached values.
119
+ * @return {string[]} The localized digit glyphs.
119
120
  */
120
121
  export function getNumbers(locale) {
121
122
  return getData(
122
123
  `numbers.${locale}`,
123
- (_) => {
124
+ () => {
124
125
  const numberFormatter = makeFormatter(locale, { minute: 'numeric' });
125
126
  return new Array(10)
126
127
  .fill()
@@ -132,11 +133,14 @@ export function getNumbers(locale) {
132
133
  };
133
134
 
134
135
  /**
135
- * Get the RegExp for the number values.
136
+ * Gets the RegExp for the number values.
136
137
  * @param {string} locale The locale.
138
+ * @param {number|null} [length=null] The exact number of digits to match.
137
139
  * @return {string} The number values RegExp.
138
140
  */
139
- export function numberRegExp(locale) {
140
- const numbers = getNumbers(locale).join('|');
141
- return `(?:${numbers})+`;
141
+ export function numberRegExp(locale, length = null) {
142
+ const quantifier = length === null ?
143
+ '+' :
144
+ `{${length}}`;
145
+ return `(?:${valuesRegExp(getNumbers(locale))})${quantifier}`;
142
146
  };
package/src/helpers.js CHANGED
@@ -1,18 +1,49 @@
1
1
  import { getDateFormatter } from './factory.js';
2
2
  import { diffMethods, thresholds } from './vars.js';
3
3
 
4
+ /** @typedef {import('./date-time.js').default} DateTime */
5
+
4
6
  /**
5
7
  * DateTime Helpers
6
8
  */
7
9
 
10
+ /**
11
+ * Escapes a string for safe use inside a RegExp source.
12
+ * @param {string} value The string to escape.
13
+ * @return {string} The escaped string.
14
+ */
15
+ function escapeRegExp(value) {
16
+ return value.replace(/[|\\{}()[\]^$+*?.-]/g, '\\$&');
17
+ }
18
+
19
+ /**
20
+ * Gets a stable day number from a DateTime's local calendar fields.
21
+ * @param {DateTime} date The DateTime.
22
+ * @return {number} The local calendar day number.
23
+ */
24
+ function calendarDay(date) {
25
+ const calendarDate = new Date(0);
26
+ calendarDate.setUTCFullYear(date.getYear(), date.getMonth() - 1, date.getDate());
27
+
28
+ return calendarDate.getTime() / 86400000;
29
+ }
30
+
31
+ /**
32
+ * Calculates the difference between two dates in a given time unit.
33
+ * @param {DateTime} date The base DateTime.
34
+ * @param {DateTime} other The DateTime to compare to.
35
+ * @param {'year' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second'} timeUnit The time unit to compare in.
36
+ * @param {boolean} [relative=true] Whether to use relative boundaries when calculating the difference.
37
+ * @return {number} The difference between the dates in the given time unit.
38
+ */
8
39
  export function calculateDiff(date, other, timeUnit, relative = true) {
9
- other = other.setTimeZone(date.getTimeZone());
40
+ other = other.withTimeZone(date.getTimeZone());
10
41
 
11
42
  switch (timeUnit) {
12
43
  case 'year':
13
44
  return compensateDiff(
14
45
  date,
15
- other.setYear(
46
+ other.withYear(
16
47
  date.getYear(),
17
48
  ),
18
49
  date.getYear() - other.getYear(),
@@ -22,7 +53,7 @@ export function calculateDiff(date, other, timeUnit, relative = true) {
22
53
  case 'month':
23
54
  return compensateDiff(
24
55
  date,
25
- other.setYear(
56
+ other.withYear(
26
57
  date.getYear(),
27
58
  date.getMonth(),
28
59
  ),
@@ -31,9 +62,19 @@ export function calculateDiff(date, other, timeUnit, relative = true) {
31
62
  -1,
32
63
  );
33
64
  case 'week':
65
+ if (relative) {
66
+ const dateWeek = date.startOfWeek();
67
+ const otherWeek = other.withLocale(date.getLocale()).startOfWeek();
68
+
69
+ return (
70
+ calendarDay(dateWeek) -
71
+ calendarDay(otherWeek)
72
+ ) / 7;
73
+ }
74
+
34
75
  return compensateDiff(
35
76
  date,
36
- other.setWeekYear(
77
+ other.withWeekYear(
37
78
  date.getWeekYear(),
38
79
  date.getWeek(),
39
80
  ),
@@ -41,9 +82,13 @@ export function calculateDiff(date, other, timeUnit, relative = true) {
41
82
  relative,
42
83
  );
43
84
  case 'day':
85
+ if (relative) {
86
+ return calendarDay(date) - calendarDay(other);
87
+ }
88
+
44
89
  return compensateDiff(
45
90
  date,
46
- other.setYear(
91
+ other.withYear(
47
92
  date.getYear(),
48
93
  date.getMonth(),
49
94
  date.getDate(),
@@ -54,11 +99,11 @@ export function calculateDiff(date, other, timeUnit, relative = true) {
54
99
  case 'hour':
55
100
  return compensateDiff(
56
101
  date,
57
- other.setYear(
102
+ other.withYear(
58
103
  date.getYear(),
59
104
  date.getMonth(),
60
105
  date.getDate(),
61
- ).setHours(
106
+ ).withHours(
62
107
  date.getHours(),
63
108
  ),
64
109
  (date - other) / 3600000,
@@ -67,11 +112,11 @@ export function calculateDiff(date, other, timeUnit, relative = true) {
67
112
  case 'minute':
68
113
  return compensateDiff(
69
114
  date,
70
- other.setYear(
115
+ other.withYear(
71
116
  date.getYear(),
72
117
  date.getMonth(),
73
118
  date.getDate(),
74
- ).setHours(
119
+ ).withHours(
75
120
  date.getHours(),
76
121
  date.getMinutes(),
77
122
  ),
@@ -81,11 +126,11 @@ export function calculateDiff(date, other, timeUnit, relative = true) {
81
126
  case 'second':
82
127
  return compensateDiff(
83
128
  date,
84
- other.setYear(
129
+ other.withYear(
85
130
  date.getYear(),
86
131
  date.getMonth(),
87
132
  date.getDate(),
88
- ).setHours(
133
+ ).withHours(
89
134
  date.getHours(),
90
135
  date.getMinutes(),
91
136
  date.getSeconds(),
@@ -99,11 +144,24 @@ export function calculateDiff(date, other, timeUnit, relative = true) {
99
144
  };
100
145
 
101
146
  /**
102
- * Compensate the difference between two dates.
147
+ * Gets the RegExp for a list of string values.
148
+ * Longer values are matched first to avoid prefix collisions.
149
+ * @param {string[]} values The values to include in the RegExp.
150
+ * @return {string} The values RegExp.
151
+ */
152
+ export function valuesRegExp(values) {
153
+ return values.slice()
154
+ .sort((a, b) => b.length - a.length)
155
+ .map((value) => escapeRegExp(`${value}`))
156
+ .join('|');
157
+ };
158
+
159
+ /**
160
+ * Compensates the difference between two dates.
103
161
  * @param {DateTime} date The DateTime.
104
162
  * @param {DateTime} other The DateTime to compare to.
105
163
  * @param {number} amount The amount to compensate.
106
- * @param {Boolean} [compensate=true] Whether to compensate the amount.
164
+ * @param {boolean} [compensate=true] Whether to compensate the amount.
107
165
  * @param {number} [compensation=1] The compensation offset.
108
166
  * @return {number} The compensated amount.
109
167
  */
@@ -126,10 +184,10 @@ function compensateDiff(date, other, amount, compensate = true, compensation = 1
126
184
  };
127
185
 
128
186
  /**
129
- * Get the biggest difference between two dates.
187
+ * Gets the biggest difference between two dates.
130
188
  * @param {DateTime} date The DateTime.
131
189
  * @param {DateTime} [other] The DateTime to compare to.
132
- * @return {array} The biggest difference (amount and time unit).
190
+ * @return {[number, string]} The biggest difference (amount and time unit).
133
191
  */
134
192
  export function getBiggestDiff(date, other) {
135
193
  let lastResult;
@@ -159,7 +217,7 @@ export function getBiggestDiff(date, other) {
159
217
  };
160
218
 
161
219
  /**
162
- * Get the offset for a DateTime.
220
+ * Gets the offset for a DateTime.
163
221
  * @param {DateTime} date The DateTime.
164
222
  * @return {number} The offset.
165
223
  */
@@ -170,14 +228,36 @@ export function getOffset(date) {
170
228
  return 0;
171
229
  }
172
230
 
173
- const utcString = getDateFormatter('UTC').format(date);
174
- const localString = getDateFormatter(timeZone).format(date);
231
+ const values = Object.fromEntries(
232
+ getDateFormatter(timeZone)
233
+ .formatToParts(date)
234
+ .filter((part) => part.type !== 'literal')
235
+ .map(({ type, value }) => [type, value]),
236
+ );
237
+
238
+ let year = parseInt(values.year, 10);
239
+ if (values.era === 'BC') {
240
+ year = 1 - year;
241
+ }
175
242
 
176
- return (new Date(utcString) - new Date(localString)) / 60000;
243
+ const localDate = new Date(0);
244
+ localDate.setUTCFullYear(
245
+ year,
246
+ parseInt(values.month, 10) - 1,
247
+ parseInt(values.day, 10),
248
+ );
249
+ const localTime = localDate.setUTCHours(
250
+ parseInt(values.hour, 10),
251
+ parseInt(values.minute, 10),
252
+ parseInt(values.second, 10),
253
+ parseInt(values.fractionalSecond, 10),
254
+ );
255
+
256
+ return (date.getTime() - localTime) / 60000;
177
257
  };
178
258
 
179
259
  /**
180
- * Get the number of milliseconds since the UNIX epoch (offset to timeZone).
260
+ * Gets the number of milliseconds since the UNIX epoch (offset to timeZone).
181
261
  * @param {DateTime} date The DateTime.
182
262
  * @return {number} The number of milliseconds since the UNIX epoch (offset to timeZone).
183
263
  */
@@ -186,7 +266,7 @@ export function getOffsetTime(date) {
186
266
  };
187
267
 
188
268
  /**
189
- * Compare a literal format string with a date string.
269
+ * Compares a literal format string with a date string.
190
270
  * @param {string} formatString The literal format string.
191
271
  * @param {string} dateString The date string.
192
272
  */
@@ -202,8 +282,39 @@ export function parseCompare(formatString, dateString) {
202
282
  };
203
283
 
204
284
  /**
205
- * Generate methods for parsing a date.
206
- * @return {object} An object containing date parsing methods.
285
+ * Parses a supported unzoned ISO string as a neutral wall-clock timestamp.
286
+ * @param {string} dateString The date string to parse.
287
+ * @return {number|null} The timestamp, or null if the shape is not supported.
288
+ */
289
+ export function parseLocalTimestamp(dateString) {
290
+ const match =
291
+ dateString.match(/^(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?$/) ||
292
+ dateString.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d+))?)?$/);
293
+
294
+ if (!match) {
295
+ return null;
296
+ }
297
+
298
+ const [
299
+ , year,
300
+ month = 1,
301
+ day = 1,
302
+ hours = 0,
303
+ minutes = 0,
304
+ seconds = 0,
305
+ fraction = '',
306
+ ] = match;
307
+ const date = new Date(0);
308
+
309
+ date.setUTCFullYear(year, month - 1, day);
310
+ date.setUTCHours(hours, minutes, seconds, fraction.padEnd(3, '0').substring(0, 3));
311
+
312
+ return date.getTime();
313
+ };
314
+
315
+ /**
316
+ * Generates methods for parsing a date.
317
+ * @return {Record<string, {get: Function, set: Function}>} An object containing date parsing methods.
207
318
  */
208
319
  export function parseFactory() {
209
320
  let isPM = false;
@@ -212,7 +323,7 @@ export function parseFactory() {
212
323
  return {
213
324
  date: {
214
325
  get: (datetime) => datetime.getDate(),
215
- set: (datetime, value) => datetime.setDate(value),
326
+ set: (datetime, value) => datetime.withDate(value),
216
327
  },
217
328
  dayPeriod: {
218
329
  get: (datetime) => datetime.getHours() < 12 ? 0 : 1,
@@ -222,18 +333,18 @@ export function parseFactory() {
222
333
  if (lastAM) {
223
334
  hours += datetime.getHours();
224
335
  }
225
- return datetime.setHours(hours);
336
+ return datetime.withHours(hours);
226
337
  },
227
338
  },
228
339
  dayOfYear: {
229
340
  get: (datetime) => datetime.getDayOfYear(),
230
- set: (datetime, value) => datetime.setDayOfYear(value),
341
+ set: (datetime, value) => datetime.withDayOfYear(value),
231
342
  },
232
343
  era: {
233
- get: (datetime) => datetime.getYear() < 1 ? 0 : 1,
344
+ get: (datetime) => datetime.getYear() < 0 ? 0 : 1,
234
345
  set: (datetime, value) => {
235
346
  const offset = value ? 1 : -1;
236
- return datetime.setYear(
347
+ return datetime.withYear(
237
348
  datetime.getYear() * offset,
238
349
  );
239
350
  },
@@ -245,84 +356,99 @@ export function parseFactory() {
245
356
  value += 12;
246
357
  }
247
358
  lastAM = true;
248
- return datetime.setHours(value);
359
+ return datetime.withHours(value);
249
360
  },
250
361
  },
251
362
  hours24: {
252
363
  get: (datetime) => datetime.getHours(),
253
364
  set: (datetime, value) => {
254
365
  lastAM = false;
255
- return datetime.setHours(value);
366
+ return datetime.withHours(value);
256
367
  },
257
368
  },
258
369
  milliseconds: {
259
370
  get: (datetime) => datetime.getMilliseconds(),
260
- set: (datetime, value) => datetime.setMilliseconds(value),
371
+ set: (datetime, value) => datetime.withMilliseconds(value),
261
372
  },
262
373
  minutes: {
263
374
  get: (datetime) => datetime.getMinutes(),
264
- set: (datetime, value) => datetime.setMinutes(value),
375
+ set: (datetime, value) => datetime.withMinutes(value),
265
376
  },
266
377
  month: {
267
378
  get: (datetime) => datetime.getMonth(),
268
- set: (datetime, value) => datetime.setMonth(value),
379
+ set: (datetime, value) => datetime.withMonth(value),
269
380
  },
270
381
  quarter: {
271
382
  get: (datetime) => datetime.getQuarter(),
272
- set: (datetime, value) => datetime.setQuarter(value),
383
+ set: (datetime, value) => datetime.withQuarter(value),
273
384
  },
274
385
  seconds: {
275
386
  get: (datetime) => datetime.getSeconds(),
276
- set: (datetime, value) => datetime.setSeconds(value),
387
+ set: (datetime, value) => datetime.withSeconds(value),
277
388
  },
278
389
  week: {
279
390
  get: (datetime) => datetime.getWeek(),
280
- set: (datetime, value) => datetime.setWeek(value),
391
+ set: (datetime, value) => datetime.withWeek(value),
281
392
  },
282
393
  weekDay: {
283
394
  get: (datetime) => datetime.getWeekDay(),
284
- set: (datetime, value) => datetime.setWeekDay(value),
395
+ set: (datetime, value) => datetime.withWeekDay(value),
285
396
  },
286
397
  weekDayInMonth: {
287
398
  get: (datetime) => datetime.getWeekDayInMonth(),
288
- set: (datetime, value) => datetime.setWeekDayInMonth(value),
399
+ set: (datetime, value) => datetime.withWeekDayInMonth(value),
289
400
  },
290
401
  weekOfMonth: {
291
402
  get: (datetime) => datetime.getWeekOfMonth(),
292
- set: (datetime, value) => datetime.setWeekOfMonth(value),
403
+ set: (datetime, value) => datetime.withWeekOfMonth(value),
293
404
  },
294
405
  weekYear: {
295
406
  get: (datetime) => datetime.getWeekYear(),
296
- set: (datetime, value) => datetime.setWeekYear(value),
407
+ set: (datetime, value) => datetime.withWeekYear(value),
297
408
  },
298
409
  year: {
299
410
  get: (datetime) => {
300
411
  const year = datetime.getYear();
301
412
  return Math.abs(year);
302
413
  },
303
- set: (datetime, value) => datetime.setYear(value),
414
+ set: (datetime, value) => datetime.withYear(value),
304
415
  },
305
416
  };
306
417
  };
307
418
 
308
419
  /**
309
- * Set the number of milliseconds since the UNIX epoch (offset to timeZone).
420
+ * Sets the number of milliseconds since the UNIX epoch (offset to timeZone).
310
421
  * @param {DateTime} date The DateTime.
311
422
  * @param {number} time The number of milliseconds since the UNIX epoch (offset to timeZone).
312
- * @return {DateTime} The DateTime object.
423
+ * @param {number} [direction=1] The direction to resolve a gap.
424
+ * @return {DateTime} A new DateTime instance.
313
425
  */
314
- export function setOffsetTime(date, time) {
315
- const oldOffset = date.getTimeZoneOffset();
426
+ export function setOffsetTime(date, time, direction = 1) {
427
+ const newDate = date.withTime(
428
+ time + (date.getTimeZoneOffset() * 60000),
429
+ );
430
+ const newOffsetTime = getOffsetTime(newDate);
316
431
 
317
- const newTime = time + (oldOffset * 60000);
318
- const newDate = date.setTime(newTime);
432
+ if (newOffsetTime === time) {
433
+ return newDate;
434
+ }
319
435
 
320
- const offset = newDate.getTimeZoneOffset();
436
+ const adjustedDate = date.withTime(
437
+ time + (newDate.getTimeZoneOffset() * 60000),
438
+ );
439
+ const adjustedOffsetTime = getOffsetTime(adjustedDate);
321
440
 
322
- if (oldOffset === offset) {
323
- return newDate;
441
+ if (adjustedOffsetTime === time) {
442
+ return adjustedDate;
443
+ }
444
+
445
+ if (direction < 0) {
446
+ return newOffsetTime < adjustedOffsetTime ?
447
+ newDate :
448
+ adjustedDate;
324
449
  }
325
450
 
326
- // compensate for DST transitions
327
- return newDate.setTime(newTime - ((oldOffset - offset) * 60000));
451
+ return newOffsetTime > adjustedOffsetTime ?
452
+ newDate :
453
+ adjustedDate;
328
454
  };