native-dates 0.0.0-stage → 0.0.6

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.
@@ -0,0 +1,194 @@
1
+ import { guard, refuse, refuseType } from './errors.js';
2
+ import { IS_DURATION } from './identity.js';
3
+ import { isDevelopment, isNumber, isPlainObject, isString } from './types.js';
4
+ import { isDigits, isTemporalDuration, plural, unit } from './units.js';
5
+
6
+ export const fieldNames = [
7
+ 'years',
8
+ 'months',
9
+ 'weeks',
10
+ 'days',
11
+ 'hours',
12
+ 'minutes',
13
+ 'seconds',
14
+ 'milliseconds',
15
+ 'microseconds',
16
+ 'nanoseconds',
17
+ ];
18
+
19
+ // a fractional quantity spills into the next unit down: 1.5 days is a day and 12 hours. a month has no
20
+ // next unit, so a fraction of one is refused rather than guessed
21
+ const spillsInto = {
22
+ year: ['month', 12],
23
+ week: ['day', 7],
24
+ day: ['hour', 24],
25
+ hour: ['minute', 60],
26
+ minute: ['second', 60],
27
+ second: ['millisecond', 1000],
28
+ millisecond: ['microsecond', 1000],
29
+ microsecond: ['nanosecond', 1000],
30
+ };
31
+
32
+ export const spill = (fields, name, value) => {
33
+ if (name === 'quarter') {
34
+ return spill(fields, 'month', value * 3);
35
+ }
36
+ if (!Number.isFinite(value)) {
37
+ refuse('notFinite', `${value} ${name}`);
38
+ }
39
+ const whole = Math.trunc(value);
40
+ const fraction = Number((value - whole).toFixed(9));
41
+ fields[plural(name)] = (fields[plural(name)] ?? 0) + whole;
42
+ if (fraction) {
43
+ const next = spillsInto[name];
44
+ if (!next) {
45
+ refuse('fractionalMonth', `${value} ${name}`, {
46
+ explanation: isDevelopment
47
+ ? 'months and nanoseconds have no smaller unit to spill into. write whole months, or days'
48
+ : 0,
49
+ });
50
+ }
51
+ spill(fields, next[0], Number((fraction * next[1]).toFixed(9)));
52
+ }
53
+ return fields;
54
+ };
55
+
56
+ // the nonzero fields of a Temporal.Duration as a plain object
57
+ export const fieldsOf = (temporal) => {
58
+ const fields = {};
59
+ for (const field of fieldNames) {
60
+ if (temporal[field]) {
61
+ fields[field] = temporal[field];
62
+ }
63
+ }
64
+ return fields;
65
+ };
66
+
67
+ const quantity = /([-+]?)(\d+(?:\.\d+)?)\s*([a-zµ]+)/gi;
68
+
69
+ // '1h 30m', '2 weeks and 3 days', '90 minutes', '1500' (milliseconds).
70
+ // a leading minus with the rest unsigned negates the whole phrase, the way a person means '-1h 30m',
71
+ // and a sign on each quantity is taken as written
72
+ const parseWords = (text) => {
73
+ const phrase = text.trim().replace(/,|\band\b/gi, ' ');
74
+ if (!phrase) {
75
+ refuse('unreadableDuration', text, {
76
+ explanation: isDevelopment ? 'an empty string is not a length. duration(0) is zero' : 0,
77
+ });
78
+ }
79
+ if (/^-?\d+(\.\d+)?$/.test(phrase)) {
80
+ return spill({}, 'millisecond', Number(phrase));
81
+ }
82
+ const found = [];
83
+ const rest = phrase.replace(quantity, (_, sign, amount, name) => {
84
+ found.push([sign, Number(amount), unit(name)]);
85
+ return '';
86
+ });
87
+ if (rest.trim()) {
88
+ refuse('unreadableDuration', text, {
89
+ explanation: isDevelopment
90
+ ? "write quantities with units, '1h 30m' or '2 weeks 3 days', or an ISO duration like PT1H30M"
91
+ : 0,
92
+ });
93
+ }
94
+ const negateAll = found[0]?.[0] === '-' && found.slice(1).every(([sign]) => sign === '');
95
+ const fields = {};
96
+ for (const [sign, amount, name] of found) {
97
+ spill(fields, name, negateAll || sign === '-' ? -amount : amount);
98
+ }
99
+ return fields;
100
+ };
101
+
102
+ // a duration has one sign. '1h -30m' has no meaning until it is subtracted from a point
103
+ const oneSign = (fields, at) => {
104
+ const signs = new Set(Object.values(fields).filter(Boolean).map(Math.sign));
105
+ if (signs.size > 1) {
106
+ refuse('mixedSigns', at, {
107
+ explanation: isDevelopment
108
+ ? 'a duration has one sign. subtract from a point instead: start.plus(months(1)).minus(days(3))'
109
+ : 0,
110
+ });
111
+ }
112
+ return fields;
113
+ };
114
+
115
+ // every shape a duration can be written as, as plural Temporal fields
116
+ export const fieldsFrom = (input, name) => {
117
+ if (input?.[IS_DURATION]) {
118
+ if (name !== undefined) {
119
+ refuseType('notADuration', `${input} ${name}`, {
120
+ explanation: isDevelopment
121
+ ? "a duration already has its units. a unit goes with a number: duration(90, 'minutes')"
122
+ : 0,
123
+ });
124
+ }
125
+ return input.toFields();
126
+ }
127
+ if (isTemporalDuration(input)) {
128
+ return fieldsOf(input);
129
+ }
130
+ if (isNumber(input)) {
131
+ return spill({}, name === undefined ? 'millisecond' : unit(name), input);
132
+ }
133
+ if (isString(input)) {
134
+ // a count from a form or a query string is text, and '30' beside 'days' is thirty days
135
+ if (name !== undefined && isDigits(input)) {
136
+ return spill({}, unit(name), Number(input));
137
+ }
138
+ return /^[-+]?P/i.test(input.trim())
139
+ ? fieldsOf(guard(() => Temporal.Duration.from(input.trim()), 'unreadableDuration', input))
140
+ : oneSign(parseWords(input), input);
141
+ }
142
+ if (isPlainObject(input)) {
143
+ const fields = {};
144
+ for (const [key, value] of Object.entries(input)) {
145
+ spill(fields, unit(key), value);
146
+ }
147
+ return oneSign(fields, JSON.stringify(input));
148
+ }
149
+ return refuseType('notADuration', String(input), {
150
+ explanation: isDevelopment
151
+ ? "a duration is a number with a unit, a phrase like '2h 30m', an ISO string like PT2H30M, or a fields object"
152
+ : 0,
153
+ });
154
+ };
155
+
156
+ export const clockFields = (fields) => {
157
+ const clock = {};
158
+ for (const field of fieldNames.slice(4)) {
159
+ if (fields[field]) {
160
+ clock[field] = fields[field];
161
+ }
162
+ }
163
+ return clock;
164
+ };
165
+
166
+ // years, months, weeks and days stay field-wise: they are calendar units, and folding 25 hours into a
167
+ // day would move a deadline across a daylight saving change. hours and below combine through Temporal
168
+ // so a subtraction balances to one sign: 2h minus 30m is 1h 30m
169
+ export const addFields = (a, b, sign) => {
170
+ const fields = {};
171
+ for (const field of ['years', 'months', 'weeks', 'days']) {
172
+ const sum = (a[field] ?? 0) + sign * (b[field] ?? 0);
173
+ if (sum) {
174
+ fields[field] = sum;
175
+ }
176
+ }
177
+ const clockA = temporalDurationOf(clockFields(a));
178
+ const clockB = temporalDurationOf(clockFields(b));
179
+ Object.assign(fields, fieldsOf(sign > 0 ? clockA.add(clockB) : clockA.subtract(clockB)));
180
+ return oneSign(fields, JSON.stringify(fields));
181
+ };
182
+
183
+ // Temporal refuses an empty fields object, and a zero duration has no nonzero field to offer it
184
+ export const temporalDurationOf = (fields) =>
185
+ Object.keys(fields).length
186
+ ? guard(() => Temporal.Duration.from(fields), 'unreadableDuration', JSON.stringify(fields))
187
+ : new Temporal.Duration();
188
+
189
+ export const temporalDurationFrom = (input, name) => temporalDurationOf(fieldsFrom(input, name));
190
+
191
+ // plural keys are how Temporal writes a length, singular keys how it writes a point, so { hours: 2 }
192
+ // is two hours where { hour: 2 } is two o'clock
193
+ export const isDurationFields = (value) =>
194
+ isPlainObject(value) && Object.keys(value).length > 0 && Object.keys(value).every((key) => fieldNames.includes(key));
@@ -0,0 +1,93 @@
1
+ import { refuse } from './errors.js';
2
+ import { plainDateTimeFormat, zonedDateTimeFormat } from './intl.js';
3
+ import { isDevelopment, isPlainObject, isString } from './types.js';
4
+ import { locale as pickLocale } from './zones.js';
5
+
6
+ const presets = {
7
+ datetime: {
8
+ short: { dateStyle: 'short', timeStyle: 'short' },
9
+ medium: { dateStyle: 'medium', timeStyle: 'short' },
10
+ long: { dateStyle: 'long', timeStyle: 'short' },
11
+ full: { dateStyle: 'full', timeStyle: 'short' },
12
+ date: { dateStyle: 'medium' },
13
+ time: { timeStyle: 'short' },
14
+ month: { month: 'long', year: 'numeric' },
15
+ text: {
16
+ year: 'numeric',
17
+ month: 'short',
18
+ day: 'numeric',
19
+ hour: 'numeric',
20
+ minute: '2-digit',
21
+ timeZoneName: 'short',
22
+ },
23
+ },
24
+ date: {
25
+ short: { dateStyle: 'short' },
26
+ medium: { dateStyle: 'medium' },
27
+ long: { dateStyle: 'long' },
28
+ full: { dateStyle: 'full' },
29
+ month: { month: 'long', year: 'numeric' },
30
+ },
31
+ time: {
32
+ default: { timeStyle: 'short' },
33
+ short: { timeStyle: 'short' },
34
+ medium: { timeStyle: 'medium' },
35
+ long: { timeStyle: 'long' },
36
+ full: { timeStyle: 'full' },
37
+ },
38
+ };
39
+
40
+ // frozen, so a preset keys its formatter by identity and a format call serializes nothing
41
+ for (const kind of Object.values(presets)) {
42
+ for (const options of Object.values(kind)) {
43
+ Object.freeze(options);
44
+ }
45
+ }
46
+
47
+ // a preset name or an Intl options bag resolves to Intl options, anything else resolves to undefined
48
+ export const intlOptions = (kind, spec) => {
49
+ if (spec === undefined) {
50
+ return presets[kind].default ?? presets[kind].medium;
51
+ }
52
+ if (isPlainObject(spec)) {
53
+ return spec;
54
+ }
55
+ if (!isString(spec)) {
56
+ refuse('unknownFormat', String(spec), {
57
+ explanation: isDevelopment
58
+ ? "a format is a preset like 'medium', an Intl options object, or a token pattern with plugins/tokens"
59
+ : 0,
60
+ });
61
+ }
62
+ return Object.hasOwn(presets[kind], spec) ? presets[kind][spec] : undefined;
63
+ };
64
+
65
+ export const unknownFormat = (spec) =>
66
+ refuse('unknownFormat', String(spec), {
67
+ explanation: isDevelopment
68
+ ? "a format is a preset like 'medium' or an Intl options object. import '@native-dates/plugins/tokens' for patterns like 'YYYY-MM-DD'"
69
+ : 0,
70
+ });
71
+
72
+ // Intl gets epoch milliseconds and a zone on every engine. a date is its UTC midnight read in UTC, a
73
+ // time is that clock on the epoch day, so the fields print unshifted whether Temporal is native or not
74
+ export const epochOf = (kind, subject) => {
75
+ if (kind === 'date') {
76
+ // Date.UTC reads a year under 100 as the 1900s, setUTCFullYear reads it as written
77
+ const stamp = new Date(0);
78
+ stamp.setUTCFullYear(subject.year, subject.month - 1, subject.day);
79
+ return stamp.getTime();
80
+ }
81
+ if (kind === 'time') {
82
+ return Date.UTC(1970, 0, 1, subject.hour, subject.minute, subject.second, subject.millisecond);
83
+ }
84
+ return subject;
85
+ };
86
+
87
+ export const formatter = (kind, options, locale, zone) =>
88
+ kind === 'datetime'
89
+ ? zonedDateTimeFormat(pickLocale(locale), options, zone)
90
+ : plainDateTimeFormat(pickLocale(locale), options);
91
+
92
+ export const formatIntl = (kind, subject, options, locale, zone) =>
93
+ formatter(kind, options, locale, zone).format(epochOf(kind, subject));
@@ -0,0 +1,10 @@
1
+ // Symbol.for keys survive cross-realm boundaries (bundles, iframes), so
2
+ // `instanceof DateTime` works against any realm's class definition
3
+ export const IS_DATE_TIME = Symbol.for('native-dates/DateTime');
4
+ export const IS_CALENDAR_DATE = Symbol.for('native-dates/CalendarDate');
5
+ export const IS_TIME = Symbol.for('native-dates/Time');
6
+ export const IS_DURATION = Symbol.for('native-dates/Duration');
7
+ export const IS_RANGE = Symbol.for('native-dates/Range');
8
+ export const IS_DATE_RANGE = Symbol.for('native-dates/DateRange');
9
+ export const IS_DATE_TIME_RANGE = Symbol.for('native-dates/DateTimeRange');
10
+ export const IS_TIME_RANGE = Symbol.for('native-dates/TimeRange');
@@ -0,0 +1,45 @@
1
+ import { guard } from './errors.js';
2
+ import { isPlainObject } from './types.js';
3
+
4
+ // an Intl constructor costs about a hundred format calls, so each formatter is built once per argument
5
+ // list and reused. the arguments key nested maps, a string by value and a frozen options object by
6
+ // identity, so a preset costs no serialization on a format call. a caller's own object may change
7
+ // between calls, so it keys by its fields. every call of one formatter passes the same number of arguments
8
+ export const cached = (make) => {
9
+ const root = new Map();
10
+ return (...args) => {
11
+ let level = root;
12
+ const last = args.length - 1;
13
+ for (let index = 0; index <= last; index++) {
14
+ const arg = args[index];
15
+ const key = isPlainObject(arg) && !Object.isFrozen(arg) ? JSON.stringify(arg) : arg;
16
+ let next = level.get(key);
17
+ if (next === undefined) {
18
+ next = index === last ? make(...args) : new Map();
19
+ level.set(key, next);
20
+ }
21
+ level = next;
22
+ }
23
+ return level;
24
+ };
25
+ };
26
+
27
+ // a datetime prints in its zone unless the options name one. a date or a time has no instant, so its
28
+ // fields print through UTC whatever the options say. an unknown option throws as the formatter is built,
29
+ // so the refusal wraps the build alone and a call that finds its formatter pays nothing for it
30
+ export const zonedDateTimeFormat = cached((locale, options, zone) =>
31
+ guard(() => new Intl.DateTimeFormat(locale, { timeZone: zone, ...options }), 'unknownFormat', JSON.stringify(options))
32
+ );
33
+ export const plainDateTimeFormat = cached((locale, options) =>
34
+ guard(
35
+ () => new Intl.DateTimeFormat(locale, { ...options, timeZone: 'UTC' }),
36
+ 'unknownFormat',
37
+ JSON.stringify(options),
38
+ )
39
+ );
40
+ export const durationFormat = cached((locale, style, hoursDisplay) =>
41
+ new Intl.DurationFormat(locale, { style, hoursDisplay })
42
+ );
43
+ export const unitFormat = cached((locale, unit, unitDisplay) =>
44
+ new Intl.NumberFormat(locale, { style: 'unit', unit, unitDisplay })
45
+ );
@@ -0,0 +1,20 @@
1
+ // leads the file without optional chaining so bundlers fold process.env.NODE_ENV across modules
2
+ export const isDevelopment = typeof process !== 'undefined' ? process.env.NODE_ENV !== 'production' : false;
3
+
4
+ const toString = Object.prototype.toString;
5
+
6
+ export const isObject = (value) => value !== null && typeof value === 'object';
7
+
8
+ export const isPlainObject = (value) => {
9
+ if (!isObject(value)) {
10
+ return false;
11
+ }
12
+ const proto = Object.getPrototypeOf(value);
13
+ return proto === null || proto === Object.prototype;
14
+ };
15
+
16
+ export const isString = (value) => typeof value === 'string';
17
+ export const isNumber = (value) => typeof value === 'number';
18
+ export const isFunction = (value) => typeof value === 'function';
19
+ export const isArray = Array.isArray;
20
+ export const isDate = (value) => toString.call(value) === '[object Date]';
@@ -0,0 +1,147 @@
1
+ import { refuse } from './errors.js';
2
+ import { isDevelopment, isNumber, isPlainObject, isString } from './types.js';
3
+
4
+ if (!globalThis.Temporal) {
5
+ refuse('noTemporal', 'globalThis.Temporal', {
6
+ explanation: isDevelopment
7
+ ? 'this library wraps the Temporal API. import @native-dates/polyfill first on a runtime without one'
8
+ : 0,
9
+ });
10
+ }
11
+
12
+ export const inspect = Symbol.for('nodejs.util.inspect.custom');
13
+
14
+ // every common unit spelling plus the calendar units duration arithmetic refuses to guess at
15
+ const spellings = {
16
+ y: 'year',
17
+ yr: 'year',
18
+ yrs: 'year',
19
+ year: 'year',
20
+ years: 'year',
21
+ q: 'quarter',
22
+ qtr: 'quarter',
23
+ quarter: 'quarter',
24
+ quarters: 'quarter',
25
+ mo: 'month',
26
+ mos: 'month',
27
+ month: 'month',
28
+ months: 'month',
29
+ w: 'week',
30
+ wk: 'week',
31
+ wks: 'week',
32
+ week: 'week',
33
+ weeks: 'week',
34
+ d: 'day',
35
+ day: 'day',
36
+ days: 'day',
37
+ h: 'hour',
38
+ hr: 'hour',
39
+ hrs: 'hour',
40
+ hour: 'hour',
41
+ hours: 'hour',
42
+ m: 'minute',
43
+ min: 'minute',
44
+ mins: 'minute',
45
+ minute: 'minute',
46
+ minutes: 'minute',
47
+ s: 'second',
48
+ sec: 'second',
49
+ secs: 'second',
50
+ second: 'second',
51
+ seconds: 'second',
52
+ ms: 'millisecond',
53
+ msec: 'millisecond',
54
+ msecs: 'millisecond',
55
+ millisecond: 'millisecond',
56
+ milliseconds: 'millisecond',
57
+ us: 'microsecond',
58
+ 'µs': 'microsecond',
59
+ microsecond: 'microsecond',
60
+ microseconds: 'microsecond',
61
+ ns: 'nanosecond',
62
+ nanosecond: 'nanosecond',
63
+ nanoseconds: 'nanosecond',
64
+ };
65
+
66
+ export const units = [
67
+ 'year',
68
+ 'month',
69
+ 'week',
70
+ 'day',
71
+ 'hour',
72
+ 'minute',
73
+ 'second',
74
+ 'millisecond',
75
+ 'microsecond',
76
+ 'nanosecond',
77
+ ];
78
+ export const timeUnits = ['hour', 'minute', 'second', 'millisecond', 'microsecond', 'nanosecond'];
79
+
80
+ // a number written as text, the way a form or a query string carries one
81
+ export const isDigits = (value) => isString(value) && /^\s*[-+]?\d+(?:\.\d+)?\s*$/.test(value);
82
+
83
+ // day.js keeps M for month and m for minute, and the tokens plugin does too, so the one capital is kept
84
+ export const unit = (input) => {
85
+ const key = input === 'M' ? 'month' : isString(input) ? input.trim().toLowerCase() : '';
86
+ const found = Object.hasOwn(spellings, key) && spellings[key];
87
+ return found || refuse('unknownUnit', String(input), {
88
+ explanation: isDevelopment
89
+ ? `units are ${units.join(', ')} or quarter, singular or plural, or an abbreviation like h, min, d`
90
+ : 0,
91
+ });
92
+ };
93
+
94
+ export const plural = (name) => `${name}s`;
95
+
96
+ const weekdays = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
97
+
98
+ // ISO numbering, monday is 1 and sunday is 7
99
+ export const weekdayNumber = (input) => {
100
+ if (isNumber(input) && Number.isInteger(input) && input >= 1 && input <= 7) {
101
+ return input;
102
+ }
103
+ const key = isString(input) ? input.trim().toLowerCase().slice(0, 3) : '';
104
+ const index = key.length === 3 ? weekdays.findIndex((name) => name.startsWith(key)) : -1;
105
+ return index === -1
106
+ ? refuse('unknownWeekday', String(input), {
107
+ explanation: isDevelopment ? `weekdays are ${weekdays.join(', ')} or 1 through 7 from monday` : 0,
108
+ })
109
+ : index + 1;
110
+ };
111
+
112
+ export const isZonedDateTime = (value) => value instanceof Temporal.ZonedDateTime;
113
+ export const isInstant = (value) => value instanceof Temporal.Instant;
114
+ export const isPlainDate = (value) => value instanceof Temporal.PlainDate;
115
+ export const isPlainDateTime = (value) => value instanceof Temporal.PlainDateTime;
116
+ export const isPlainTime = (value) => value instanceof Temporal.PlainTime;
117
+ export const isTemporalDuration = (value) => value instanceof Temporal.Duration;
118
+
119
+ export const pad = (value, width = 2) => String(Math.abs(value)).padStart(width, '0');
120
+
121
+ // Temporal truncates 9.5 to 9 before it checks a range, so a fraction is refused before it gets there
122
+ export const wholeNumbers = (values) => values.every((value) => !isNumber(value) || Number.isInteger(value));
123
+
124
+ // set() replaces parts as strictly as a factory reads them: whole numbers, in range
125
+ export const withParts = (temporal, changes) => {
126
+ if (!wholeNumbers(Object.values(changes))) {
127
+ throw new RangeError('a part is a whole number');
128
+ }
129
+ return temporal.with(changes, { overflow: 'reject' });
130
+ };
131
+
132
+ // singular keys, the shape Temporal's with() takes, from any spelling a caller wrote
133
+ export const singularKeys = (fields) => {
134
+ const out = {};
135
+ for (const [key, value] of Object.entries(fields)) {
136
+ out[unit(key)] = value;
137
+ }
138
+ return out;
139
+ };
140
+
141
+ export const quarterStart = (month) => Math.floor((month - 1) / 3) * 3 + 1;
142
+
143
+ // one whole unit as duration fields
144
+ export const stepOf = (name) => (name === 'quarter' ? { months: 3 } : { [plural(name)]: 1 });
145
+
146
+ // the second argument of a factory is a zone, or an options bag { zone, loose }
147
+ export const zoneOptions = (value) => (isPlainObject(value) ? value : { zone: value });
@@ -0,0 +1,113 @@
1
+ import { guard, refuse, requireLoose } from './errors.js';
2
+ import { isDevelopment, isPlainObject, isString } from './types.js';
3
+ import { weekdayNumber } from './units.js';
4
+
5
+ // process-wide defaults, zoneAliases being the names a zone answers to when plugins/zones is loaded
6
+ const settings = { zone: undefined, locale: undefined, weekStart: 1, dayFirst: false, zoneAliases: {} };
7
+ const resolved = new Map([['UTC', 'UTC']]);
8
+
9
+ export const canonicalZone = (id) => {
10
+ try {
11
+ return Temporal.Instant.fromEpochMilliseconds(0).toZonedDateTimeISO(id).timeZoneId;
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ };
17
+
18
+ let zoneExtension = null;
19
+
20
+ export const setZoneExtension = (resolver) => {
21
+ zoneExtension = resolver;
22
+ resolved.clear();
23
+ resolved.set('UTC', 'UTC');
24
+ };
25
+
26
+ // an IANA name, a fixed offset, or UTC in core, plus cities, abbreviations and zoneAliases when
27
+ // plugins/zones is loaded. resolves once per spelling and hands back the canonical id
28
+ export const zoneId = (id) => {
29
+ if (id === undefined || id === null) {
30
+ return settings.zone ?? Temporal.Now.timeZoneId();
31
+ }
32
+ if (!isString(id)) {
33
+ refuse('unknownZone', String(id), {
34
+ explanation: isDevelopment
35
+ ? 'a zone is an IANA name like America/New_York, a fixed offset like +05:30, or UTC'
36
+ : 0,
37
+ });
38
+ }
39
+ const known = resolved.get(id);
40
+ if (known) {
41
+ return known;
42
+ }
43
+ const found = zoneExtension ? zoneExtension(id, settings.zoneAliases) : canonicalZone(id.trim());
44
+ if (!found) {
45
+ refuse('unknownZone', id, {
46
+ explanation: isDevelopment
47
+ ? zoneExtension
48
+ ? "a zone is an IANA name like America/New_York, a city like Berlin or los angeles, an abbreviation like PT or CET, a fixed offset like +05:30, or UTC. name your own once: configure({ zoneAliases: { hq: 'Europe/Berlin' } })"
49
+ : "a zone is an IANA name like America/New_York, a fixed offset like +05:30, or UTC. import '@native-dates/plugins/zones' for cities like Berlin, abbreviations like PT, and zoneAliases"
50
+ : 0,
51
+ });
52
+ }
53
+ resolved.set(id, found);
54
+ return found;
55
+ };
56
+
57
+ export const configure = ({ zone, locale, weekStart, dayFirst, zoneAliases } = {}) => {
58
+ if (zoneAliases !== undefined) {
59
+ if (!zoneExtension) {
60
+ refuse('missingPlugin', 'zoneAliases', {
61
+ explanation: isDevelopment
62
+ ? "import '@native-dates/plugins/zones' for zoneAliases"
63
+ : 0,
64
+ });
65
+ }
66
+ if (!isPlainObject(zoneAliases)) {
67
+ refuse('unknownZone', String(zoneAliases), {
68
+ explanation: isDevelopment ? "zoneAliases is an object of names to zones: { hq: 'Europe/Berlin' }" : 0,
69
+ });
70
+ }
71
+ for (const [name, target] of Object.entries(zoneAliases)) {
72
+ settings.zoneAliases[name] = zoneId(target);
73
+ }
74
+ resolved.clear();
75
+ resolved.set('UTC', 'UTC');
76
+ }
77
+ if (zone !== undefined) {
78
+ settings.zone = zone === null ? undefined : zoneId(zone);
79
+ }
80
+ if (locale !== undefined) {
81
+ settings.locale = locale === null ? undefined : checkedLocale(locale);
82
+ }
83
+ if (weekStart !== undefined) {
84
+ settings.weekStart = weekdayNumber(weekStart);
85
+ }
86
+ if (dayFirst !== undefined) {
87
+ requireLoose('dayFirst');
88
+ settings.dayFirst = !!dayFirst;
89
+ }
90
+ return { ...settings, zoneAliases: { ...settings.zoneAliases } };
91
+ };
92
+
93
+ // an Accept-Language header is the classic first attempt, and Intl would refuse it far from the cause
94
+ const knownLocales = new Set();
95
+ const checkedLocale = (tag) => {
96
+ const key = String(tag);
97
+ if (!knownLocales.has(key)) {
98
+ guard(
99
+ () => Intl.getCanonicalLocales(tag),
100
+ 'unknownLocale',
101
+ key,
102
+ isDevelopment
103
+ ? "a locale is a BCP 47 tag like 'en-US' or 'de', or a list of them. pick one out of an Accept-Language header"
104
+ : 0,
105
+ );
106
+ knownLocales.add(key);
107
+ }
108
+ return tag;
109
+ };
110
+
111
+ export const locale = (override) => (override === undefined ? settings.locale : checkedLocale(override));
112
+ export const dayFirst = (override) => (override === undefined ? settings.dayFirst : !!override);
113
+ export const weekStart = (override) => (override === undefined ? settings.weekStart : weekdayNumber(override));
package/src/index.js ADDED
@@ -0,0 +1,30 @@
1
+ // nouns
2
+ export { CalendarDate } from './calendar-date.js';
3
+ export { DateTime } from './date-time.js';
4
+ export { Duration } from './duration.js';
5
+ export { Time } from './time.js';
6
+
7
+ // factories
8
+ export { date, today, tomorrow, yesterday } from './calendar-date.js';
9
+ export { datetime, endOfToday, now, startOfToday } from './date-time.js';
10
+ export { days, duration, hours, milliseconds, minutes, months, seconds, weeks, years } from './duration.js';
11
+ export { time } from './time.js';
12
+
13
+ // guards, comparisons, brands and setup
14
+ export { isCalendarDate } from './calendar-date.js';
15
+ export { isDateTime } from './date-time.js';
16
+ export { isDuration } from './duration.js';
17
+ export { compare, earliest, kindOf, latest } from './helpers/compare.js';
18
+ export {
19
+ IS_CALENDAR_DATE,
20
+ IS_DATE_RANGE,
21
+ IS_DATE_TIME,
22
+ IS_DATE_TIME_RANGE,
23
+ IS_DURATION,
24
+ IS_RANGE,
25
+ IS_TIME,
26
+ IS_TIME_RANGE,
27
+ } from './helpers/identity.js';
28
+ export { weekdayNumber as weekday } from './helpers/units.js';
29
+ export { configure } from './helpers/zones.js';
30
+ export { isTime } from './time.js';