@fr0st/datetime 6.0.1 → 7.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.
package/src/factory.js CHANGED
@@ -2,48 +2,57 @@
2
2
  * DateTime Factory
3
3
  */
4
4
 
5
- const data = {};
5
+ const data = new Map();
6
6
 
7
7
  /**
8
- * Get values from cache (or generate if they don't exist).
8
+ * Clears all cached formatter and locale values.
9
+ */
10
+ export function clearDataCache() {
11
+ data.clear();
12
+ };
13
+
14
+ /**
15
+ * Gets a cached value, creating it on first access.
16
+ * @template T
9
17
  * @param {string} key The key for the values.
10
- * @param {function} callback The callback to generate the values.
11
- * @return {array} The cached values.
18
+ * @param {() => T} callback The callback to generate the values.
19
+ * @return {T} The cached value.
12
20
  */
13
21
  export function getData(key, callback) {
14
- if (!(key in data)) {
15
- data[key] = callback();
22
+ if (!data.has(key)) {
23
+ data.set(key, callback());
16
24
  }
17
25
 
18
- return data[key];
26
+ return data.get(key);
19
27
  };
20
28
 
21
29
  /**
22
- * Create a new date formatter for a timeZone.
23
- * @param {string} timeZone The timeZone.
24
- * @param {object} options The options for the formatter.
25
- * @return {Intl.DateTimeFormat} A new DateTimeFormat object.
30
+ * Creates a date formatter for a time zone.
31
+ * @param {string} timeZone The time zone.
32
+ * @return {Intl.DateTimeFormat} The formatter instance.
26
33
  */
27
34
  export function getDateFormatter(timeZone) {
28
35
  return getData(
29
36
  `dateFormatter.${timeZone}`,
30
- (_) => makeFormatter('en', {
37
+ () => makeFormatter('en', {
31
38
  timeZone,
32
39
  hourCycle: 'h23',
40
+ era: 'short',
33
41
  year: 'numeric',
34
42
  month: 'numeric',
35
43
  day: 'numeric',
36
44
  hour: 'numeric',
37
45
  minute: 'numeric',
46
+ second: 'numeric',
47
+ fractionalSecondDigits: 3,
38
48
  }),
39
49
  );
40
50
  };
41
51
 
42
52
  /**
43
- * Create a new relative formatter for a locale.
53
+ * Creates a relative-time formatter for a locale.
44
54
  * @param {string} locale The locale.
45
- * @param {object} options The options for the formatter.
46
- * @return {Intl.RelativeTimeFormat} A new RelativeTimeFormat object.
55
+ * @return {Intl.RelativeTimeFormat|null} The formatter instance, or null when unsupported.
47
56
  */
48
57
  export function getRelativeFormatter(locale) {
49
58
  if (!('RelativeTimeFormat' in Intl)) {
@@ -52,7 +61,7 @@ export function getRelativeFormatter(locale) {
52
61
 
53
62
  return getData(
54
63
  `relativeFormatter.${locale}`,
55
- (_) => new Intl.RelativeTimeFormat(locale, {
64
+ () => new Intl.RelativeTimeFormat(locale, {
56
65
  numeric: 'auto',
57
66
  style: 'long',
58
67
  }),
@@ -60,14 +69,15 @@ export function getRelativeFormatter(locale) {
60
69
  };
61
70
 
62
71
  /**
63
- * Create a new formatter for a locale.
72
+ * Creates a formatter for a locale.
64
73
  * @param {string} locale The locale.
65
- * @param {object} options The options for the formatter.
66
- * @return {Intl.DateTimeFormat} A new DateTimeFormat object.
74
+ * @param {Intl.DateTimeFormatOptions} options The options for the formatter.
75
+ * @return {Intl.DateTimeFormat} The formatter instance.
67
76
  */
68
77
  export function makeFormatter(locale, options) {
69
78
  return new Intl.DateTimeFormat(locale, {
70
79
  timeZone: 'UTC',
71
80
  ...options,
81
+ calendar: 'gregory',
72
82
  });
73
83
  };
@@ -2,11 +2,11 @@ import { getRelativeFormatter, makeFormatter } from './../factory.js';
2
2
  import { getDayPeriods, getDays, getEras, getMonths, getNumbers } from './values.js';
3
3
 
4
4
  /**
5
- * Format a day as a locale string.
5
+ * Formats a day as a locale string.
6
6
  * @param {string} locale The locale.
7
7
  * @param {number} day The day to format (0-6).
8
8
  * @param {string} [type=long] The formatting type.
9
- * @param {Boolean} [standalone=true] Whether the value is standalone.
9
+ * @param {boolean} [standalone=true] Whether the value is standalone.
10
10
  * @return {string} The formatted string.
11
11
  */
12
12
  export function formatDay(locale, day, type = 'long', standalone = true) {
@@ -14,9 +14,9 @@ export function formatDay(locale, day, type = 'long', standalone = true) {
14
14
  };
15
15
 
16
16
  /**
17
- * Format a day period as a locale string.
17
+ * Formats a day period as a locale string.
18
18
  * @param {string} locale The locale.
19
- * @param {number} period The period to format (0-1).
19
+ * @param {number} period The day-period index to format. (0-1)
20
20
  * @param {string} [type=long] The formatting type.
21
21
  * @return {string} The formatted string.
22
22
  */
@@ -25,9 +25,9 @@ export function formatDayPeriod(locale, period, type = 'long') {
25
25
  };
26
26
 
27
27
  /**
28
- * Format an era as a locale string.
28
+ * Formats an era as a locale string.
29
29
  * @param {string} locale The locale.
30
- * @param {number} era The period to format (0-1).
30
+ * @param {number} era The era index to format. (0-1)
31
31
  * @param {string} [type=long] The formatting type.
32
32
  * @return {string} The formatted string.
33
33
  */
@@ -36,11 +36,11 @@ export function formatEra(locale, era, type = 'long') {
36
36
  };
37
37
 
38
38
  /**
39
- * Format a month as a locale string.
39
+ * Formats a month as a locale string.
40
40
  * @param {string} locale The locale.
41
41
  * @param {number} month The month to format (1-12).
42
42
  * @param {string} [type=long] The formatting type.
43
- * @param {Boolean} [standalone=true] Whether the value is standalone.
43
+ * @param {boolean} [standalone=true] Whether the value is standalone.
44
44
  * @return {string} The formatted string.
45
45
  */
46
46
  export function formatMonth(locale, month, type = 'long', standalone = true) {
@@ -48,7 +48,7 @@ export function formatMonth(locale, month, type = 'long', standalone = true) {
48
48
  };
49
49
 
50
50
  /**
51
- * Format a number as a locale number string.
51
+ * Formats a number as a locale number string.
52
52
  * @param {string} locale The locale.
53
53
  * @param {number} number The number to format.
54
54
  * @param {number} [padding=0] The amount of padding to use.
@@ -57,43 +57,52 @@ export function formatMonth(locale, month, type = 'long', standalone = true) {
57
57
  export function formatNumber(locale, number, padding = 0) {
58
58
  const numbers = getNumbers(locale);
59
59
  return `${number}`
60
- .padStart(padding, 0)
60
+ .padStart(padding, '0')
61
61
  .replace(/\d/g, (match) => numbers[match]);
62
62
  };
63
63
 
64
64
  /**
65
- * Format a number to an offset string.
65
+ * Formats a number to an offset string.
66
66
  * @param {number} offset The offset to format.
67
- * @param {Boolean} [useColon=true] Whether to use a colon seperator.
68
- * @param {Boolean} [optionalMinutes=false] Whether minutes are optional.
67
+ * @param {boolean} [useColon=true] Whether to use a colon separator.
68
+ * @param {boolean} [optionalMinutes=false] Whether minutes are optional.
69
+ * @param {boolean} [includeSeconds=true] Whether seconds are included.
69
70
  * @return {string} The formatted offset string.
70
71
  */
71
- export function formatOffset(offset, useColon = true, optionalMinutes = false) {
72
- const hours = Math.abs(
73
- (offset / 60) | 0,
74
- );
75
- const minutes = Math.abs(offset % 60);
72
+ export function formatOffset(offset, useColon = true, optionalMinutes = false, includeSeconds = true) {
73
+ const absoluteSeconds = Math.abs(offset * 60);
74
+ const totalSeconds = Math.round(absoluteSeconds);
75
+ const precision = Number.EPSILON * Math.max(1, absoluteSeconds);
76
+ const roundingError = Math.abs(absoluteSeconds - totalSeconds);
77
+ if (!Number.isFinite(absoluteSeconds) || roundingError > precision || totalSeconds >= 86400) {
78
+ throw new Error('Invalid time zone offset supplied');
79
+ }
76
80
 
81
+ const hours = Math.floor(totalSeconds / 3600);
82
+ const minutes = Math.floor(totalSeconds % 3600 / 60);
83
+ const seconds = totalSeconds % 60;
77
84
  const sign = offset > 0 ?
78
85
  '-' :
79
86
  '+';
80
- const hourString = `${hours}`.padStart(2, 0);
81
- const minuteString = minutes || !optionalMinutes ?
82
- `${minutes}`.padStart(2, 0) :
83
- '';
84
- const colon = useColon && minuteString ?
85
- ':' :
86
- '';
87
+ const parts = [`${hours}`.padStart(2, '0')];
88
+
89
+ if (!optionalMinutes || minutes || seconds) {
90
+ parts.push(`${minutes}`.padStart(2, '0'));
91
+ }
92
+
93
+ if (includeSeconds && seconds) {
94
+ parts.push(`${seconds}`.padStart(2, '0'));
95
+ }
87
96
 
88
- return `${sign}${hourString}${colon}${minuteString}`;
97
+ return sign + parts.join(useColon ? ':' : '');
89
98
  };
90
99
 
91
100
  /**
92
- * Format a relative duration as a locale string.
101
+ * Formats a relative duration as a locale string.
93
102
  * @param {string} locale The locale.
94
103
  * @param {number} amount The amount of duration.
95
104
  * @param {string} unit The time unit.
96
- * @returns {string} The relative duration.
105
+ * @return {string} The relative duration.
97
106
  */
98
107
  export function formatRelative(locale, amount, unit) {
99
108
  const relativeFormatter = getRelativeFormatter(locale);
@@ -106,7 +115,7 @@ export function formatRelative(locale, amount, unit) {
106
115
  };
107
116
 
108
117
  /**
109
- * Format a time zone as a locale string.
118
+ * Formats a time zone as a locale string.
110
119
  * @param {string} locale The locale.
111
120
  * @param {number} timestamp The timestamp to use.
112
121
  * @param {string} timeZone The time zone to format.
@@ -0,0 +1,80 @@
1
+ import { getData } from './../factory.js';
2
+ import { minDaysInFirstWeek, weekStart } from './locales.js';
3
+
4
+ /**
5
+ * Gets a locale value from generated data.
6
+ * @param {object} data The generated locale data.
7
+ * @param {string[]} candidates The locale candidates.
8
+ * @param {number} fallback The fallback value.
9
+ * @return {number} The locale value.
10
+ */
11
+ function generatedValue(data, candidates, fallback) {
12
+ for (const candidate of candidates) {
13
+ for (const [value, valueLocales] of Object.entries(data)) {
14
+ if (valueLocales.includes(candidate)) {
15
+ return parseInt(value, 10);
16
+ }
17
+ }
18
+ }
19
+
20
+ return fallback;
21
+ }
22
+
23
+ /**
24
+ * Gets generated-data candidates for a locale.
25
+ * @param {Intl.Locale} locale The locale.
26
+ * @return {string[]} The locale candidates.
27
+ */
28
+ function localeCandidates(locale) {
29
+ const localeName = locale.toString().split('-x-', 1)[0];
30
+ const regionOverride = localeName.match(
31
+ /-u-(?:[a-z0-9]{2,8}-)*rg-([a-z]{2}|\d{3})zzzz(?:-|$)/i,
32
+ );
33
+ const region = regionOverride?.[1] || locale.region;
34
+
35
+ return [
36
+ [locale.language, locale.script, region],
37
+ [locale.language, region],
38
+ [locale.language, locale.script],
39
+ [locale.language],
40
+ ].map((parts) =>
41
+ parts
42
+ .filter((part) => !!part)
43
+ .join('-')
44
+ .toLowerCase(),
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Gets week information for a locale.
50
+ * @param {string} locale The locale.
51
+ * @return {{firstDay: number, minimalDays: number}} The week information.
52
+ */
53
+ export function getWeekInfo(locale) {
54
+ return getData(
55
+ `weekInfo.${locale}`,
56
+ () => {
57
+ const localeData = new Intl.Locale(locale);
58
+ const runtimeInfo = localeData.getWeekInfo?.() || localeData.weekInfo || {};
59
+
60
+ let { firstDay = null, minimalDays = null } = runtimeInfo;
61
+
62
+ const candidates = firstDay && minimalDays ?
63
+ [] :
64
+ localeCandidates(localeData);
65
+
66
+ if (!firstDay) {
67
+ const phpFirstDay = generatedValue(weekStart, candidates, 2);
68
+
69
+ // IntlCalendar numbers Sunday as 1; Intl.Locale numbers it as 7.
70
+ firstDay = phpFirstDay === 1 ? 7 : phpFirstDay - 1;
71
+ }
72
+
73
+ if (!minimalDays) {
74
+ minimalDays = generatedValue(minDaysInFirstWeek, candidates, 1);
75
+ }
76
+
77
+ return { firstDay, minimalDays };
78
+ },
79
+ );
80
+ };
@@ -1,2 +1,2 @@
1
- export const weekStart = { '1': ['af', 'am', 'ar-il', 'ar-sa', 'ar-ye', 'as', 'bn', 'bo', 'brx', 'ccp', 'ceb', 'chr', 'dav', 'dz', 'ebu', 'en', 'fil', 'gu', 'guz', 'haw', 'he', 'hi', 'id', 'ii', 'ja', 'jv', 'kam', 'ki', 'kln', 'km', 'kn', 'ko', 'kok', 'ks', 'lkt', 'lo', 'luo', 'luy', 'mas', 'mer', 'mgh', 'ml', 'mr', 'mt', 'my', 'nd', 'ne', 'om', 'or', 'pa', 'ps-pk', 'pt', 'qu', 'saq', 'sd', 'seh', 'sn', 'ta', 'te', 'th', 'ti', 'ug', 'ur', 'xh', 'yue', 'zh', 'zu'], '7': ['ar', 'ckb', 'en-ae', 'en-sd', 'fa', 'kab', 'lrc', 'mzn', 'ps'] };
2
- export const minDaysInFirstWeek = { '4': ['ast', 'bg', 'br', 'ca', 'ce', 'cs', 'cy', 'da', 'de', 'dsb', 'el', 'en-at', 'en-be', 'en-ch', 'en-de', 'en-dk', 'en-fi', 'en-fj', 'en-gb', 'en-gg', 'en-gi', 'en-ie', 'en-im', 'en-je', 'en-nl', 'en-se', 'es', 'et', 'eu', 'fi', 'fo', 'fr', 'fur', 'fy', 'ga', 'gd', 'gl', 'gsw', 'gv', 'hsb', 'hu', 'is', 'it', 'ksh', 'kw', 'lb', 'lt', 'nb', 'nds', 'nl', 'nn', 'os-ru', 'pl', 'pt-ch', 'pt-lu', 'pt-pt', 'rm', 'ru', 'sah', 'se', 'sk', 'smn', 'sv', 'tt', 'wae'] };
1
+ export const weekStart = { '1': ['af', 'am', 'ar-il', 'ar-sa', 'ar-ye', 'as', 'bn', 'bo', 'brx', 'ccp', 'ceb', 'chr', 'dav', 'doi', 'dz', 'ebu', 'en', 'es-br', 'es-bz', 'es-co', 'es-do', 'es-gt', 'es-hn', 'es-mx', 'es-ni', 'es-pa', 'es-pe', 'es-ph', 'es-pr', 'es-py', 'es-sv', 'es-us', 'es-ve', 'fil', 'fr-ca', 'gu', 'guz', 'haw', 'he', 'hi', 'id', 'ii', 'ja', 'jv', 'kam', 'ki', 'kln', 'km', 'kn', 'ko', 'kok', 'ks', 'lkt', 'lo', 'luo', 'luy', 'mai', 'mas', 'mer', 'mgh', 'ml', 'mni', 'mr', 'ms-id', 'ms-sg', 'mt', 'my', 'nd', 'ne', 'om', 'or', 'pa', 'ps-pk', 'pt', 'qu', 'sa', 'saq', 'sat', 'sd', 'seh', 'sn', 'so-et', 'so-ke', 'su', 'sw-ke', 'ta', 'te', 'teo-ke', 'th', 'ti', 'ug', 'ur', 'xh', 'yue', 'zh', 'zu'], '2': ['af-na', 'ar-001', 'ar-eh', 'ar-er', 'ar-km', 'ar-lb', 'ar-ma', 'ar-mr', 'ar-ps', 'ar-so', 'ar-ss', 'ar-td', 'ar-tn', 'en-001', 'en-150', 'en-ai', 'en-at', 'en-au', 'en-bb', 'en-be', 'en-bi', 'en-bm', 'en-cc', 'en-ch', 'en-ck', 'en-cm', 'en-cx', 'en-cy', 'en-de', 'en-dg', 'en-dk', 'en-er', 'en-fi', 'en-fj', 'en-fk', 'en-fm', 'en-gb', 'en-gd', 'en-gg', 'en-gh', 'en-gi', 'en-gm', 'en-gy', 'en-ie', 'en-im', 'en-io', 'en-je', 'en-ki', 'en-kn', 'en-ky', 'en-lc', 'en-lr', 'en-ls', 'en-mg', 'en-mp', 'en-ms', 'en-mu', 'en-mw', 'en-my', 'en-na', 'en-nf', 'en-ng', 'en-nl', 'en-nr', 'en-nu', 'en-nz', 'en-pg', 'en-pn', 'en-pw', 'en-rw', 'en-sb', 'en-sc', 'en-se', 'en-sh', 'en-si', 'en-sl', 'en-ss', 'en-sx', 'en-sz', 'en-tc', 'en-tk', 'en-to', 'en-tv', 'en-tz', 'en-ug', 'en-vc', 'en-vg', 'en-vu', 'en-zm', 'ko-kp', 'mas-tz', 'pt-ao', 'pt-ch', 'pt-cv', 'pt-gq', 'pt-gw', 'pt-lu', 'pt-st', 'pt-tl', 'qu-bo', 'qu-ec', 'ta-lk', 'ta-my', 'ti-er'], '7': ['ar', 'ckb', 'en-ae', 'en-sd', 'fa', 'fr-dj', 'fr-dz', 'fr-sy', 'kab', 'lrc', 'mzn', 'ps', 'so-dj', 'uz-arab', 'uz-arab-af'] };
2
+ export const minDaysInFirstWeek = { '4': ['ast', 'bg', 'br', 'ca', 'ce', 'cs', 'cy', 'da', 'de', 'dsb', 'el', 'en-at', 'en-be', 'en-ch', 'en-de', 'en-dk', 'en-fi', 'en-fj', 'en-gb', 'en-gg', 'en-gi', 'en-ie', 'en-im', 'en-je', 'en-nl', 'en-se', 'es', 'et', 'eu', 'fi', 'fo', 'fr', 'fur', 'fy', 'ga', 'gd', 'gl', 'gsw', 'gv', 'hsb', 'hu', 'is', 'it', 'ksh', 'kw', 'lb', 'lt', 'nb', 'nl', 'nn', 'no', 'os-ru', 'pl', 'pt-ch', 'pt-lu', 'pt-pt', 'rm', 'ru', 'sah', 'sc', 'se', 'sk', 'smn', 'sv', 'tt', 'wae'], '1': ['da-gl', 'el-cy', 'es-419', 'es-ar', 'es-bo', 'es-br', 'es-bz', 'es-cl', 'es-co', 'es-cr', 'es-cu', 'es-do', 'es-ea', 'es-ec', 'es-gq', 'es-gt', 'es-hn', 'es-ic', 'es-mx', 'es-ni', 'es-pa', 'es-pe', 'es-ph', 'es-pr', 'es-py', 'es-sv', 'es-us', 'es-uy', 'es-ve', 'fr-bf', 'fr-bi', 'fr-bj', 'fr-bl', 'fr-ca', 'fr-cd', 'fr-cf', 'fr-cg', 'fr-ci', 'fr-cm', 'fr-dj', 'fr-dz', 'fr-ga', 'fr-gn', 'fr-gq', 'fr-ht', 'fr-km', 'fr-ma', 'fr-mf', 'fr-mg', 'fr-ml', 'fr-mr', 'fr-mu', 'fr-nc', 'fr-ne', 'fr-pf', 'fr-pm', 'fr-rw', 'fr-sc', 'fr-sn', 'fr-sy', 'fr-td', 'fr-tg', 'fr-tn', 'fr-vu', 'fr-wf', 'fr-yt', 'nl-aw', 'nl-bq', 'nl-cw', 'nl-sr', 'nl-sx', 'ru-by', 'ru-kg', 'ru-kz', 'ru-md', 'ru-ua'] };
@@ -1,21 +1,24 @@
1
- import { getDayPeriods, getDays, getEras, getMonths, getNumbers } from './values.js';
2
1
  import { weekDay } from './utility.js';
2
+ import { getDayPeriods, getDays, getEras, getMonths, getNumbers } from './values.js';
3
3
 
4
4
  /**
5
- * Parse a day from a locale string.
5
+ * Parses a day from a locale string.
6
6
  * @param {string} locale The locale.
7
7
  * @param {string} value The value to parse.
8
8
  * @param {string} [type=long] The formatting type.
9
- * @param {Boolean} [standalone=true] Whether the value is standalone.
10
- * @return {number} The day number (0-6).
9
+ * @param {boolean} [standalone=true] Whether the value is standalone.
10
+ * @return {number} The local day of the week (1-7).
11
11
  */
12
12
  export function parseDay(locale, value, type = 'long', standalone = true) {
13
- const day = getDays(locale, type, standalone).indexOf(value) || 7;
13
+ const day = getDays(locale, type, standalone).indexOf(value);
14
+ if (day === -1) {
15
+ throw new Error(`Unmatched day string in DateTime string: ${value}`);
16
+ }
14
17
  return weekDay(locale, day);
15
18
  };
16
19
 
17
20
  /**
18
- * Parse a day period from a locale string.
21
+ * Parses a day period from a locale string.
19
22
  * @param {string} locale The locale.
20
23
  * @param {string} value The value to parse.
21
24
  * @param {string} [type=long] The formatting type.
@@ -26,7 +29,7 @@ export function parseDayPeriod(locale, value, type = 'long') {
26
29
  };
27
30
 
28
31
  /**
29
- * Parse an era from a locale string.
32
+ * Parses an era from a locale string.
30
33
  * @param {string} locale The locale.
31
34
  * @param {string} value The value to parse.
32
35
  * @param {string} [type=long] The formatting type.
@@ -37,11 +40,11 @@ export function parseEra(locale, value, type = 'long') {
37
40
  };
38
41
 
39
42
  /**
40
- * Parse a month from a locale string.
43
+ * Parses a month from a locale string.
41
44
  * @param {string} locale The locale.
42
45
  * @param {string} value The value to parse.
43
46
  * @param {string} [type=long] The formatting type.
44
- * @param {Boolean} [standalone=true] Whether the value is standalone.
47
+ * @param {boolean} [standalone=true] Whether the value is standalone.
45
48
  * @return {number} The month number (1-12).
46
49
  */
47
50
  export function parseMonth(locale, value, type = 'long', standalone = true) {
@@ -49,14 +52,25 @@ export function parseMonth(locale, value, type = 'long', standalone = true) {
49
52
  };
50
53
 
51
54
  /**
52
- * Parse a number from a locale number string.
55
+ * Parses locale digits into an ASCII digit string.
56
+ * @param {string} locale The locale.
57
+ * @param {string} value The value to parse.
58
+ * @return {string} The parsed ASCII digit string.
59
+ */
60
+ export function parseNumberString(locale, value) {
61
+ const numbers = getNumbers(locale);
62
+ return Array.from(value, (digit) => numbers.indexOf(digit)).join('');
63
+ };
64
+
65
+ /**
66
+ * Parses a number from a locale number string.
53
67
  * @param {string} locale The locale.
54
68
  * @param {string} value The value to parse.
55
69
  * @return {number} The parsed number.
56
70
  */
57
71
  export function parseNumber(locale, value) {
58
- const numbers = getNumbers(locale);
59
72
  return parseInt(
60
- `${value}`.replace(/./g, (match) => numbers.indexOf(match)),
73
+ parseNumberString(locale, value),
74
+ 10,
61
75
  );
62
76
  };