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.
- package/LICENSE +7 -0
- package/README.md +41 -2
- package/package.json +46 -4
- package/src/calendar-date.js +393 -0
- package/src/date-time.js +433 -0
- package/src/duration.js +341 -0
- package/src/helpers/compare.js +126 -0
- package/src/helpers/errors.js +89 -0
- package/src/helpers/fields.js +194 -0
- package/src/helpers/format.js +93 -0
- package/src/helpers/identity.js +10 -0
- package/src/helpers/intl.js +45 -0
- package/src/helpers/types.js +20 -0
- package/src/helpers/units.js +147 -0
- package/src/helpers/zones.js +113 -0
- package/src/index.js +30 -0
- package/src/time.js +357 -0
- package/types/index.d.ts +538 -0
package/src/time.js
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
import { CalendarDate } from './calendar-date.js';
|
|
2
|
+
import { DateTime } from './date-time.js';
|
|
3
|
+
import { anchored } from './duration.js';
|
|
4
|
+
import { guard, loosely, parseLoose, refuse, refuseType, requireLoose } from './helpers/errors.js';
|
|
5
|
+
import { fieldsFrom, temporalDurationOf } from './helpers/fields.js';
|
|
6
|
+
import { formatIntl, intlOptions, unknownFormat } from './helpers/format.js';
|
|
7
|
+
import { IS_TIME } from './helpers/identity.js';
|
|
8
|
+
import { isDate, isDevelopment, isNumber, isPlainObject, isString } from './helpers/types.js';
|
|
9
|
+
import {
|
|
10
|
+
inspect,
|
|
11
|
+
isDigits,
|
|
12
|
+
isPlainDateTime,
|
|
13
|
+
isPlainTime,
|
|
14
|
+
isZonedDateTime,
|
|
15
|
+
singularKeys,
|
|
16
|
+
stepOf,
|
|
17
|
+
unit,
|
|
18
|
+
wholeNumbers,
|
|
19
|
+
withParts,
|
|
20
|
+
} from './helpers/units.js';
|
|
21
|
+
import { zoneId } from './helpers/zones.js';
|
|
22
|
+
|
|
23
|
+
const clock = /^(\d{1,2})(?::(\d{2}))?(?::(\d{2})(?:\.(\d{1,9}))?)?\s*([ap]\.?m\.?)?$/i;
|
|
24
|
+
|
|
25
|
+
export class Time {
|
|
26
|
+
// brand time
|
|
27
|
+
get [IS_TIME]() {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
static [Symbol.hasInstance](value) {
|
|
31
|
+
return !!value?.[IS_TIME];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#plain;
|
|
35
|
+
|
|
36
|
+
// time(9, 30), time(input, zone), or time(input, { zone, loose })
|
|
37
|
+
constructor(input, minuteOrOptions, second) {
|
|
38
|
+
if (isDigits(input) && (isNumber(minuteOrOptions) || isDigits(minuteOrOptions))) {
|
|
39
|
+
[input, minuteOrOptions, second] = [
|
|
40
|
+
Number(input),
|
|
41
|
+
Number(minuteOrOptions),
|
|
42
|
+
second === undefined ? second : Number(second),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
const settings = isPlainObject(minuteOrOptions)
|
|
46
|
+
? minuteOrOptions
|
|
47
|
+
: isString(minuteOrOptions)
|
|
48
|
+
? { zone: minuteOrOptions }
|
|
49
|
+
: {};
|
|
50
|
+
this.#plain =
|
|
51
|
+
isPlainTime(input) && settings.zone === undefined && !settings.loose && settings.dayFirst === undefined
|
|
52
|
+
? input
|
|
53
|
+
: Time.#read(input, isNumber(minuteOrOptions) ? minuteOrOptions : undefined, second, settings);
|
|
54
|
+
// the parts are own properties, so a value prints them in a console without a click. the text is
|
|
55
|
+
// keyed first so a preview leads with it, and filled once the parts it reads are in
|
|
56
|
+
this.text = undefined;
|
|
57
|
+
const plain = this.#plain;
|
|
58
|
+
this.hour = plain.hour;
|
|
59
|
+
this.minute = plain.minute;
|
|
60
|
+
this.second = plain.second;
|
|
61
|
+
this.millisecond = plain.millisecond;
|
|
62
|
+
this.text = this.format();
|
|
63
|
+
Object.freeze(this);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
static #read(input, minute, second, settings) {
|
|
67
|
+
if (settings.zone !== undefined) {
|
|
68
|
+
zoneId(settings.zone);
|
|
69
|
+
}
|
|
70
|
+
if (settings.loose || settings.dayFirst !== undefined) {
|
|
71
|
+
requireLoose(settings.loose ? 'loose' : 'dayFirst');
|
|
72
|
+
}
|
|
73
|
+
if (input === undefined || input === null) {
|
|
74
|
+
return refuseType('notATime', String(input), {
|
|
75
|
+
explanation: isDevelopment
|
|
76
|
+
? 'now().time is the current clock. a missing value is refused so an absent field never silently becomes now'
|
|
77
|
+
: 0,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
if (isNumber(input)) {
|
|
81
|
+
return guard(
|
|
82
|
+
() => {
|
|
83
|
+
if (!wholeNumbers([input, minute, second])) {
|
|
84
|
+
throw new RangeError('a clock reads whole numbers');
|
|
85
|
+
}
|
|
86
|
+
return Temporal.PlainTime.from({ hour: input, minute: minute ?? 0, second: second ?? 0 }, {
|
|
87
|
+
overflow: 'reject',
|
|
88
|
+
});
|
|
89
|
+
},
|
|
90
|
+
'unreadableTime',
|
|
91
|
+
`${input}:${minute ?? 0}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (input instanceof Time) {
|
|
95
|
+
return input.toTemporal();
|
|
96
|
+
}
|
|
97
|
+
if (input instanceof DateTime) {
|
|
98
|
+
return input.toTemporal().toPlainTime();
|
|
99
|
+
}
|
|
100
|
+
if (isDate(input)) {
|
|
101
|
+
return new DateTime(input, settings).toTemporal().toPlainTime();
|
|
102
|
+
}
|
|
103
|
+
if (isString(input)) {
|
|
104
|
+
try {
|
|
105
|
+
return Time.#parse(input);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (!settings.loose || error.code !== 'unreadableTime') {
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
const zoned = parseLoose(input, settings.zone, settings.dayFirst);
|
|
112
|
+
if (!zoned) {
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
return zoned.toPlainTime();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (isZonedDateTime(input) || isPlainDateTime(input)) {
|
|
119
|
+
return input.toPlainTime();
|
|
120
|
+
}
|
|
121
|
+
if (isPlainObject(input)) {
|
|
122
|
+
return guard(
|
|
123
|
+
() => Temporal.PlainTime.from(singularKeys(input), { overflow: 'reject' }),
|
|
124
|
+
'unreadableTime',
|
|
125
|
+
JSON.stringify(input),
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return refuseType('notATime', String(input), {
|
|
129
|
+
explanation: isDevelopment
|
|
130
|
+
? "a time is a clock string like '09:00' or '5pm', numbers, a fields object, a datetime, or a Temporal value"
|
|
131
|
+
: 0,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// '9am', '5:30 pm', '09:00', '17:30:15'. anything else goes to Temporal's ISO reader
|
|
136
|
+
static #parse(text) {
|
|
137
|
+
const match = clock.exec(text.trim());
|
|
138
|
+
if (match) {
|
|
139
|
+
const [, hourText, minute = '0', second = '0', fraction = '', meridiem] = match;
|
|
140
|
+
let hour = Number(hourText);
|
|
141
|
+
if (meridiem) {
|
|
142
|
+
const afternoon = meridiem[0].toLowerCase() === 'p';
|
|
143
|
+
if (hour < 1 || hour > 12) {
|
|
144
|
+
refuse('unreadableTime', text, {
|
|
145
|
+
explanation: isDevelopment ? 'clock hours with am or pm run 1 through 12' : 0,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
hour = (hour % 12) + (afternoon ? 12 : 0);
|
|
149
|
+
}
|
|
150
|
+
const nanos = fraction.padEnd(9, '0');
|
|
151
|
+
return guard(
|
|
152
|
+
() =>
|
|
153
|
+
Temporal.PlainTime.from({
|
|
154
|
+
hour,
|
|
155
|
+
minute: Number(minute),
|
|
156
|
+
second: Number(second),
|
|
157
|
+
millisecond: Number(nanos.slice(0, 3)),
|
|
158
|
+
microsecond: Number(nanos.slice(3, 6)),
|
|
159
|
+
nanosecond: Number(nanos.slice(6)),
|
|
160
|
+
}, { overflow: 'reject' }),
|
|
161
|
+
'unreadableTime',
|
|
162
|
+
text,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return guard(
|
|
166
|
+
() => Temporal.PlainTime.from(text.trim()),
|
|
167
|
+
'unreadableTime',
|
|
168
|
+
text,
|
|
169
|
+
isDevelopment
|
|
170
|
+
? "write a clock time: '09:00', '5:30pm', '17:30:15.250'. plugins/loose adds { loose: true } to read a datetime and keep the clock"
|
|
171
|
+
: 0,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/*******************************
|
|
176
|
+
Reads
|
|
177
|
+
*******************************/
|
|
178
|
+
|
|
179
|
+
get microsecond() {
|
|
180
|
+
return this.#plain.microsecond;
|
|
181
|
+
}
|
|
182
|
+
get nanosecond() {
|
|
183
|
+
return this.#plain.nanosecond;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/*******************************
|
|
187
|
+
Arithmetic
|
|
188
|
+
*******************************/
|
|
189
|
+
|
|
190
|
+
// arithmetic wraps like a clock face: 23:00 plus two hours is 01:00
|
|
191
|
+
plus(amount, name) {
|
|
192
|
+
return new Time(
|
|
193
|
+
guard(() => this.#plain.add(Time.#clockFields(amount, name)), 'cannotAdd', `${amount} ${name ?? ''}`),
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// a clock has no calendar, so a day added to a time is refused rather than wrapped away
|
|
198
|
+
static #clockFields(amount, name) {
|
|
199
|
+
const fields = fieldsFrom(amount, name);
|
|
200
|
+
const calendar = ['years', 'months', 'weeks', 'days'].find((field) => fields[field]);
|
|
201
|
+
if (calendar) {
|
|
202
|
+
refuse('notATimeUnit', calendar, {
|
|
203
|
+
explanation: isDevelopment
|
|
204
|
+
? 'a time has no calendar. add hours or minutes, or place it on a date first: time.on(date, zone).plus(days)'
|
|
205
|
+
: 0,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
return temporalDurationOf(fields);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
minus(amount, name) {
|
|
212
|
+
return new Time(
|
|
213
|
+
guard(
|
|
214
|
+
() => this.#plain.subtract(Time.#clockFields(amount, name)),
|
|
215
|
+
'cannotSubtract',
|
|
216
|
+
`${amount} ${name ?? ''}`,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
set(fields, value) {
|
|
222
|
+
const changes = isString(fields) ? { [unit(fields)]: value } : singularKeys(fields);
|
|
223
|
+
return new Time(
|
|
224
|
+
guard(() => withParts(this.#plain, changes), 'cannotSet', JSON.stringify(changes)),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
startOf(name) {
|
|
229
|
+
return new Time(
|
|
230
|
+
guard(() => this.#plain.round({ smallestUnit: unit(name), roundingMode: 'floor' }), 'cannotRound', String(name)),
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
endOf(name) {
|
|
235
|
+
return this.startOf(name).plus(stepOf(unit(name))).minus({ milliseconds: 1 });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
round(increment, name) {
|
|
239
|
+
return this.#round(increment, name, 'halfExpand');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
floor(increment, name) {
|
|
243
|
+
return this.#round(increment, name, 'floor');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
ceil(increment, name) {
|
|
247
|
+
return this.#round(increment, name, 'ceil');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#round(increment, name, mode) {
|
|
251
|
+
const [count, target] = isString(increment) && !isDigits(increment) ? [1, increment] : [Number(increment), name];
|
|
252
|
+
return new Time(
|
|
253
|
+
guard(
|
|
254
|
+
() => this.#plain.round({ smallestUnit: unit(target), roundingIncrement: count, roundingMode: mode }),
|
|
255
|
+
'cannotRound',
|
|
256
|
+
`${count} ${target}`,
|
|
257
|
+
),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/*******************************
|
|
262
|
+
Comparison
|
|
263
|
+
*******************************/
|
|
264
|
+
|
|
265
|
+
equals(other) {
|
|
266
|
+
return this.#compare(other) === 0;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
#compare(other) {
|
|
270
|
+
return Temporal.PlainTime.compare(this.#plain, new Time(other).toTemporal());
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
isBefore(other) {
|
|
274
|
+
return this.#compare(other) < 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
isAfter(other) {
|
|
278
|
+
return this.#compare(other) > 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
isSame(other, name) {
|
|
282
|
+
if (name === undefined) {
|
|
283
|
+
return this.equals(other);
|
|
284
|
+
}
|
|
285
|
+
return this.startOf(name).equals(new Time(other).startOf(name));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/*******************************
|
|
289
|
+
Measure
|
|
290
|
+
*******************************/
|
|
291
|
+
|
|
292
|
+
// within the day, signed: 22:00 until 02:00 is minus twenty hours, not four
|
|
293
|
+
until(other, name) {
|
|
294
|
+
const span = anchored(this.#plain.until(new Time(other).toTemporal(), { largestUnit: 'hour' }), undefined);
|
|
295
|
+
return name === undefined ? span : span.total(name);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
since(other, name) {
|
|
299
|
+
return new Time(other).until(this, name);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
on(day, zone) {
|
|
303
|
+
return new CalendarDate(day).at(this, zone);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/*******************************
|
|
307
|
+
Output
|
|
308
|
+
*******************************/
|
|
309
|
+
|
|
310
|
+
format(spec, locale) {
|
|
311
|
+
const options = intlOptions('time', spec);
|
|
312
|
+
return options ? formatIntl('time', this.#plain, options, locale) : unknownFormat(spec);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// a fraction prints in groups of three, the width its precision needs, so one precision is one width
|
|
316
|
+
toString() {
|
|
317
|
+
const digits = this.nanosecond ? 9 : this.microsecond ? 6 : this.millisecond ? 3 : 0;
|
|
318
|
+
return this.#plain.toString({ fractionalSecondDigits: digits });
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
toJSON() {
|
|
322
|
+
return this.toString();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
toTemporal() {
|
|
326
|
+
return this.#plain;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
valueOf() {
|
|
330
|
+
return refuse('notANumber', String(this), {
|
|
331
|
+
explanation: isDevelopment
|
|
332
|
+
? 'a time of day is not a point on the number line. compare with isBefore, isAfter or equals, or measure with until'
|
|
333
|
+
: 0,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// + gives the string, while < and - still refuse through valueOf
|
|
338
|
+
[Symbol.toPrimitive](hint) {
|
|
339
|
+
return hint === 'number' ? this.valueOf() : this.toString();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
[inspect]() {
|
|
343
|
+
return `Time(${this.#plain})`;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
Time.prototype.add = Time.prototype.plus;
|
|
348
|
+
Time.prototype.subtract = Time.prototype.minus;
|
|
349
|
+
Time.prototype.remove = Time.prototype.minus;
|
|
350
|
+
|
|
351
|
+
export const time = (input, minuteOrOptions, second) => {
|
|
352
|
+
const build = () =>
|
|
353
|
+
input instanceof Time && minuteOrOptions === undefined ? input : new Time(input, minuteOrOptions, second);
|
|
354
|
+
return isPlainObject(minuteOrOptions) && minuteOrOptions.loose ? loosely(build) : build();
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
export const isTime = (value) => value instanceof Time;
|