UNPKG

27.9 kBJavaScriptView Raw
1import { InvalidArgumentError, InvalidDurationError, InvalidUnitError } from "./errors.js";
2import Formatter from "./impl/formatter.js";
3import Invalid from "./impl/invalid.js";
4import Locale from "./impl/locale.js";
5import { parseISODuration, parseISOTimeOnly } from "./impl/regexParser.js";
6import {
7 asNumber,
8 hasOwnProperty,
9 isNumber,
10 isUndefined,
11 normalizeObject,
12 roundTo,
13} from "./impl/util.js";
14import Settings from "./settings.js";
15
16const INVALID = "Invalid Duration";
17
18// unit conversion constants
19const lowOrderMatrix = {
20 weeks: {
21 days: 7,
22 hours: 7 * 24,
23 minutes: 7 * 24 * 60,
24 seconds: 7 * 24 * 60 * 60,
25 milliseconds: 7 * 24 * 60 * 60 * 1000,
26 },
27 days: {
28 hours: 24,
29 minutes: 24 * 60,
30 seconds: 24 * 60 * 60,
31 milliseconds: 24 * 60 * 60 * 1000,
32 },
33 hours: { minutes: 60, seconds: 60 * 60, milliseconds: 60 * 60 * 1000 },
34 minutes: { seconds: 60, milliseconds: 60 * 1000 },
35 seconds: { milliseconds: 1000 },
36 },
37 casualMatrix = {
38 years: {
39 quarters: 4,
40 months: 12,
41 weeks: 52,
42 days: 365,
43 hours: 365 * 24,
44 minutes: 365 * 24 * 60,
45 seconds: 365 * 24 * 60 * 60,
46 milliseconds: 365 * 24 * 60 * 60 * 1000,
47 },
48 quarters: {
49 months: 3,
50 weeks: 13,
51 days: 91,
52 hours: 91 * 24,
53 minutes: 91 * 24 * 60,
54 seconds: 91 * 24 * 60 * 60,
55 milliseconds: 91 * 24 * 60 * 60 * 1000,
56 },
57 months: {
58 weeks: 4,
59 days: 30,
60 hours: 30 * 24,
61 minutes: 30 * 24 * 60,
62 seconds: 30 * 24 * 60 * 60,
63 milliseconds: 30 * 24 * 60 * 60 * 1000,
64 },
65
66 ...lowOrderMatrix,
67 },
68 daysInYearAccurate = 146097.0 / 400,
69 daysInMonthAccurate = 146097.0 / 4800,
70 accurateMatrix = {
71 years: {
72 quarters: 4,
73 months: 12,
74 weeks: daysInYearAccurate / 7,
75 days: daysInYearAccurate,
76 hours: daysInYearAccurate * 24,
77 minutes: daysInYearAccurate * 24 * 60,
78 seconds: daysInYearAccurate * 24 * 60 * 60,
79 milliseconds: daysInYearAccurate * 24 * 60 * 60 * 1000,
80 },
81 quarters: {
82 months: 3,
83 weeks: daysInYearAccurate / 28,
84 days: daysInYearAccurate / 4,
85 hours: (daysInYearAccurate * 24) / 4,
86 minutes: (daysInYearAccurate * 24 * 60) / 4,
87 seconds: (daysInYearAccurate * 24 * 60 * 60) / 4,
88 milliseconds: (daysInYearAccurate * 24 * 60 * 60 * 1000) / 4,
89 },
90 months: {
91 weeks: daysInMonthAccurate / 7,
92 days: daysInMonthAccurate,
93 hours: daysInMonthAccurate * 24,
94 minutes: daysInMonthAccurate * 24 * 60,
95 seconds: daysInMonthAccurate * 24 * 60 * 60,
96 milliseconds: daysInMonthAccurate * 24 * 60 * 60 * 1000,
97 },
98 ...lowOrderMatrix,
99 };
100
101// units ordered by size
102const orderedUnits = [
103 "years",
104 "quarters",
105 "months",
106 "weeks",
107 "days",
108 "hours",
109 "minutes",
110 "seconds",
111 "milliseconds",
112];
113
114const reverseUnits = orderedUnits.slice(0).reverse();
115
116// clone really means "create another instance just like this one, but with these changes"
117function clone(dur, alts, clear = false) {
118 // deep merge for vals
119 const conf = {
120 values: clear ? alts.values : { ...dur.values, ...(alts.values || {}) },
121 loc: dur.loc.clone(alts.loc),
122 conversionAccuracy: alts.conversionAccuracy || dur.conversionAccuracy,
123 };
124 return new Duration(conf);
125}
126
127function antiTrunc(n) {
128 return n < 0 ? Math.floor(n) : Math.ceil(n);
129}
130
131// NB: mutates parameters
132function convert(matrix, fromMap, fromUnit, toMap, toUnit) {
133 const conv = matrix[toUnit][fromUnit],
134 raw = fromMap[fromUnit] / conv,
135 sameSign = Math.sign(raw) === Math.sign(toMap[toUnit]),
136 // ok, so this is wild, but see the matrix in the tests
137 added =
138 !sameSign && toMap[toUnit] !== 0 && Math.abs(raw) <= 1 ? antiTrunc(raw) : Math.trunc(raw);
139 toMap[toUnit] += added;
140 fromMap[fromUnit] -= added * conv;
141}
142
143// NB: mutates parameters
144function normalizeValues(matrix, vals) {
145 reverseUnits.reduce((previous, current) => {
146 if (!isUndefined(vals[current])) {
147 if (previous) {
148 convert(matrix, vals, previous, vals, current);
149 }
150 return current;
151 } else {
152 return previous;
153 }
154 }, null);
155}
156
157/**
158 * A Duration object represents a period of time, like "2 months" or "1 day, 1 hour". Conceptually, it's just a map of units to their quantities, accompanied by some additional configuration and methods for creating, parsing, interrogating, transforming, and formatting them. They can be used on their own or in conjunction with other Luxon types; for example, you can use {@link DateTime.plus} to add a Duration object to a DateTime, producing another DateTime.
159 *
160 * Here is a brief overview of commonly used methods and getters in Duration:
161 *
162 * * **Creation** To create a Duration, use {@link Duration.fromMillis}, {@link Duration.fromObject}, or {@link Duration.fromISO}.
163 * * **Unit values** See the {@link Duration#years}, {@link Duration.months}, {@link Duration#weeks}, {@link Duration#days}, {@link Duration#hours}, {@link Duration#minutes}, {@link Duration#seconds}, {@link Duration#milliseconds} accessors.
164 * * **Configuration** See {@link Duration#locale} and {@link Duration#numberingSystem} accessors.
165 * * **Transformation** To create new Durations out of old ones use {@link Duration#plus}, {@link Duration#minus}, {@link Duration#normalize}, {@link Duration#set}, {@link Duration#reconfigure}, {@link Duration#shiftTo}, and {@link Duration#negate}.
166 * * **Output** To convert the Duration into other representations, see {@link Duration#as}, {@link Duration#toISO}, {@link Duration#toFormat}, and {@link Duration#toJSON}
167 *
168 * There's are more methods documented below. In addition, for more information on subtler topics like internationalization and validity, see the external documentation.
169 */
170export default class Duration {
171 /**
172 * @private
173 */
174 constructor(config) {
175 const accurate = config.conversionAccuracy === "longterm" || false;
176 /**
177 * @access private
178 */
179 this.values = config.values;
180 /**
181 * @access private
182 */
183 this.loc = config.loc || Locale.create();
184 /**
185 * @access private
186 */
187 this.conversionAccuracy = accurate ? "longterm" : "casual";
188 /**
189 * @access private
190 */
191 this.invalid = config.invalid || null;
192 /**
193 * @access private
194 */
195 this.matrix = accurate ? accurateMatrix : casualMatrix;
196 /**
197 * @access private
198 */
199 this.isLuxonDuration = true;
200 }
201
202 /**
203 * Create Duration from a number of milliseconds.
204 * @param {number} count of milliseconds
205 * @param {Object} opts - options for parsing
206 * @param {string} [opts.locale='en-US'] - the locale to use
207 * @param {string} opts.numberingSystem - the numbering system to use
208 * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
209 * @return {Duration}
210 */
211 static fromMillis(count, opts) {
212 return Duration.fromObject({ milliseconds: count }, opts);
213 }
214
215 /**
216 * Create a Duration from a JavaScript object with keys like 'years' and 'hours'.
217 * If this object is empty then a zero milliseconds duration is returned.
218 * @param {Object} obj - the object to create the DateTime from
219 * @param {number} obj.years
220 * @param {number} obj.quarters
221 * @param {number} obj.months
222 * @param {number} obj.weeks
223 * @param {number} obj.days
224 * @param {number} obj.hours
225 * @param {number} obj.minutes
226 * @param {number} obj.seconds
227 * @param {number} obj.milliseconds
228 * @param {Object} [opts=[]] - options for creating this Duration
229 * @param {string} [opts.locale='en-US'] - the locale to use
230 * @param {string} opts.numberingSystem - the numbering system to use
231 * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
232 * @return {Duration}
233 */
234 static fromObject(obj, opts = {}) {
235 if (obj == null || typeof obj !== "object") {
236 throw new InvalidArgumentError(
237 `Duration.fromObject: argument expected to be an object, got ${
238 obj === null ? "null" : typeof obj
239 }`
240 );
241 }
242 return new Duration({
243 values: normalizeObject(obj, Duration.normalizeUnit),
244 loc: Locale.fromObject(opts),
245 conversionAccuracy: opts.conversionAccuracy,
246 });
247 }
248
249 /**
250 * Create a Duration from an ISO 8601 duration string.
251 * @param {string} text - text to parse
252 * @param {Object} opts - options for parsing
253 * @param {string} [opts.locale='en-US'] - the locale to use
254 * @param {string} opts.numberingSystem - the numbering system to use
255 * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
256 * @see https://en.wikipedia.org/wiki/ISO_8601#Durations
257 * @example Duration.fromISO('P3Y6M1W4DT12H30M5S').toObject() //=> { years: 3, months: 6, weeks: 1, days: 4, hours: 12, minutes: 30, seconds: 5 }
258 * @example Duration.fromISO('PT23H').toObject() //=> { hours: 23 }
259 * @example Duration.fromISO('P5Y3M').toObject() //=> { years: 5, months: 3 }
260 * @return {Duration}
261 */
262 static fromISO(text, opts) {
263 const [parsed] = parseISODuration(text);
264 if (parsed) {
265 return Duration.fromObject(parsed, opts);
266 } else {
267 return Duration.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`);
268 }
269 }
270
271 /**
272 * Create a Duration from an ISO 8601 time string.
273 * @param {string} text - text to parse
274 * @param {Object} opts - options for parsing
275 * @param {string} [opts.locale='en-US'] - the locale to use
276 * @param {string} opts.numberingSystem - the numbering system to use
277 * @param {string} [opts.conversionAccuracy='casual'] - the conversion system to use
278 * @see https://en.wikipedia.org/wiki/ISO_8601#Times
279 * @example Duration.fromISOTime('11:22:33.444').toObject() //=> { hours: 11, minutes: 22, seconds: 33, milliseconds: 444 }
280 * @example Duration.fromISOTime('11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
281 * @example Duration.fromISOTime('T11:00').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
282 * @example Duration.fromISOTime('1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
283 * @example Duration.fromISOTime('T1100').toObject() //=> { hours: 11, minutes: 0, seconds: 0 }
284 * @return {Duration}
285 */
286 static fromISOTime(text, opts) {
287 const [parsed] = parseISOTimeOnly(text);
288 if (parsed) {
289 return Duration.fromObject(parsed, opts);
290 } else {
291 return Duration.invalid("unparsable", `the input "${text}" can't be parsed as ISO 8601`);
292 }
293 }
294
295 /**
296 * Create an invalid Duration.
297 * @param {string} reason - simple string of why this datetime is invalid. Should not contain parameters or anything else data-dependent
298 * @param {string} [explanation=null] - longer explanation, may include parameters and other useful debugging information
299 * @return {Duration}
300 */
301 static invalid(reason, explanation = null) {
302 if (!reason) {
303 throw new InvalidArgumentError("need to specify a reason the Duration is invalid");
304 }
305
306 const invalid = reason instanceof Invalid ? reason : new Invalid(reason, explanation);
307
308 if (Settings.throwOnInvalid) {
309 throw new InvalidDurationError(invalid);
310 } else {
311 return new Duration({ invalid });
312 }
313 }
314
315 /**
316 * @private
317 */
318 static normalizeUnit(unit) {
319 const normalized = {
320 year: "years",
321 years: "years",
322 quarter: "quarters",
323 quarters: "quarters",
324 month: "months",
325 months: "months",
326 week: "weeks",
327 weeks: "weeks",
328 day: "days",
329 days: "days",
330 hour: "hours",
331 hours: "hours",
332 minute: "minutes",
333 minutes: "minutes",
334 second: "seconds",
335 seconds: "seconds",
336 millisecond: "milliseconds",
337 milliseconds: "milliseconds",
338 }[unit ? unit.toLowerCase() : unit];
339
340 if (!normalized) throw new InvalidUnitError(unit);
341
342 return normalized;
343 }
344
345 /**
346 * Check if an object is a Duration. Works across context boundaries
347 * @param {object} o
348 * @return {boolean}
349 */
350 static isDuration(o) {
351 return (o && o.isLuxonDuration) || false;
352 }
353
354 /**
355 * Get the locale of a Duration, such 'en-GB'
356 * @type {string}
357 */
358 get locale() {
359 return this.isValid ? this.loc.locale : null;
360 }
361
362 /**
363 * Get the numbering system of a Duration, such 'beng'. The numbering system is used when formatting the Duration
364 *
365 * @type {string}
366 */
367 get numberingSystem() {
368 return this.isValid ? this.loc.numberingSystem : null;
369 }
370
371 /**
372 * Returns a string representation of this Duration formatted according to the specified format string. You may use these tokens:
373 * * `S` for milliseconds
374 * * `s` for seconds
375 * * `m` for minutes
376 * * `h` for hours
377 * * `d` for days
378 * * `M` for months
379 * * `y` for years
380 * Notes:
381 * * Add padding by repeating the token, e.g. "yy" pads the years to two digits, "hhhh" pads the hours out to four digits
382 * * The duration will be converted to the set of units in the format string using {@link Duration.shiftTo} and the Durations's conversion accuracy setting.
383 * @param {string} fmt - the format string
384 * @param {Object} opts - options
385 * @param {boolean} [opts.floor=true] - floor numerical values
386 * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("y d s") //=> "1 6 2"
387 * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("yy dd sss") //=> "01 06 002"
388 * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toFormat("M S") //=> "12 518402000"
389 * @return {string}
390 */
391 toFormat(fmt, opts = {}) {
392 // reverse-compat since 1.2; we always round down now, never up, and we do it by default
393 const fmtOpts = {
394 ...opts,
395 floor: opts.round !== false && opts.floor !== false,
396 };
397 return this.isValid
398 ? Formatter.create(this.loc, fmtOpts).formatDurationFromString(this, fmt)
399 : INVALID;
400 }
401
402 /**
403 * Returns a JavaScript object with this Duration's values.
404 * @example Duration.fromObject({ years: 1, days: 6, seconds: 2 }).toObject() //=> { years: 1, days: 6, seconds: 2 }
405 * @return {Object}
406 */
407 toObject() {
408 if (!this.isValid) return {};
409 return { ...this.values };
410 }
411
412 /**
413 * Returns an ISO 8601-compliant string representation of this Duration.
414 * @see https://en.wikipedia.org/wiki/ISO_8601#Durations
415 * @example Duration.fromObject({ years: 3, seconds: 45 }).toISO() //=> 'P3YT45S'
416 * @example Duration.fromObject({ months: 4, seconds: 45 }).toISO() //=> 'P4MT45S'
417 * @example Duration.fromObject({ months: 5 }).toISO() //=> 'P5M'
418 * @example Duration.fromObject({ minutes: 5 }).toISO() //=> 'PT5M'
419 * @example Duration.fromObject({ milliseconds: 6 }).toISO() //=> 'PT0.006S'
420 * @return {string}
421 */
422 toISO() {
423 // we could use the formatter, but this is an easier way to get the minimum string
424 if (!this.isValid) return null;
425
426 let s = "P";
427 if (this.years !== 0) s += this.years + "Y";
428 if (this.months !== 0 || this.quarters !== 0) s += this.months + this.quarters * 3 + "M";
429 if (this.weeks !== 0) s += this.weeks + "W";
430 if (this.days !== 0) s += this.days + "D";
431 if (this.hours !== 0 || this.minutes !== 0 || this.seconds !== 0 || this.milliseconds !== 0)
432 s += "T";
433 if (this.hours !== 0) s += this.hours + "H";
434 if (this.minutes !== 0) s += this.minutes + "M";
435 if (this.seconds !== 0 || this.milliseconds !== 0)
436 // this will handle "floating point madness" by removing extra decimal places
437 // https://stackoverflow.com/questions/588004/is-floating-point-math-broken
438 s += roundTo(this.seconds + this.milliseconds / 1000, 3) + "S";
439 if (s === "P") s += "T0S";
440 return s;
441 }
442
443 /**
444 * Returns an ISO 8601-compliant string representation of this Duration, formatted as a time of day.
445 * Note that this will return null if the duration is invalid, negative, or equal to or greater than 24 hours.
446 * @see https://en.wikipedia.org/wiki/ISO_8601#Times
447 * @param {Object} opts - options
448 * @param {boolean} [opts.suppressMilliseconds=false] - exclude milliseconds from the format if they're 0
449 * @param {boolean} [opts.suppressSeconds=false] - exclude seconds from the format if they're 0
450 * @param {boolean} [opts.includePrefix=false] - include the `T` prefix
451 * @param {string} [opts.format='extended'] - choose between the basic and extended format
452 * @example Duration.fromObject({ hours: 11 }).toISOTime() //=> '11:00:00.000'
453 * @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressMilliseconds: true }) //=> '11:00:00'
454 * @example Duration.fromObject({ hours: 11 }).toISOTime({ suppressSeconds: true }) //=> '11:00'
455 * @example Duration.fromObject({ hours: 11 }).toISOTime({ includePrefix: true }) //=> 'T11:00:00.000'
456 * @example Duration.fromObject({ hours: 11 }).toISOTime({ format: 'basic' }) //=> '110000.000'
457 * @return {string}
458 */
459 toISOTime(opts = {}) {
460 if (!this.isValid) return null;
461
462 const millis = this.toMillis();
463 if (millis < 0 || millis >= 86400000) return null;
464
465 opts = {
466 suppressMilliseconds: false,
467 suppressSeconds: false,
468 includePrefix: false,
469 format: "extended",
470 ...opts,
471 };
472
473 const value = this.shiftTo("hours", "minutes", "seconds", "milliseconds");
474
475 let fmt = opts.format === "basic" ? "hhmm" : "hh:mm";
476
477 if (!opts.suppressSeconds || value.seconds !== 0 || value.milliseconds !== 0) {
478 fmt += opts.format === "basic" ? "ss" : ":ss";
479 if (!opts.suppressMilliseconds || value.milliseconds !== 0) {
480 fmt += ".SSS";
481 }
482 }
483
484 let str = value.toFormat(fmt);
485
486 if (opts.includePrefix) {
487 str = "T" + str;
488 }
489
490 return str;
491 }
492
493 /**
494 * Returns an ISO 8601 representation of this Duration appropriate for use in JSON.
495 * @return {string}
496 */
497 toJSON() {
498 return this.toISO();
499 }
500
501 /**
502 * Returns an ISO 8601 representation of this Duration appropriate for use in debugging.
503 * @return {string}
504 */
505 toString() {
506 return this.toISO();
507 }
508
509 /**
510 * Returns an milliseconds value of this Duration.
511 * @return {number}
512 */
513 toMillis() {
514 return this.as("milliseconds");
515 }
516
517 /**
518 * Returns an milliseconds value of this Duration. Alias of {@link toMillis}
519 * @return {number}
520 */
521 valueOf() {
522 return this.toMillis();
523 }
524
525 /**
526 * Make this Duration longer by the specified amount. Return a newly-constructed Duration.
527 * @param {Duration|Object|number} duration - The amount to add. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()
528 * @return {Duration}
529 */
530 plus(duration) {
531 if (!this.isValid) return this;
532
533 const dur = friendlyDuration(duration),
534 result = {};
535
536 for (const k of orderedUnits) {
537 if (hasOwnProperty(dur.values, k) || hasOwnProperty(this.values, k)) {
538 result[k] = dur.get(k) + this.get(k);
539 }
540 }
541
542 return clone(this, { values: result }, true);
543 }
544
545 /**
546 * Make this Duration shorter by the specified amount. Return a newly-constructed Duration.
547 * @param {Duration|Object|number} duration - The amount to subtract. Either a Luxon Duration, a number of milliseconds, the object argument to Duration.fromObject()
548 * @return {Duration}
549 */
550 minus(duration) {
551 if (!this.isValid) return this;
552
553 const dur = friendlyDuration(duration);
554 return this.plus(dur.negate());
555 }
556
557 /**
558 * Scale this Duration by the specified amount. Return a newly-constructed Duration.
559 * @param {function} fn - The function to apply to each unit. Arity is 1 or 2: the value of the unit and, optionally, the unit name. Must return a number.
560 * @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits(x => x * 2) //=> { hours: 2, minutes: 60 }
561 * @example Duration.fromObject({ hours: 1, minutes: 30 }).mapUnits((x, u) => u === "hour" ? x * 2 : x) //=> { hours: 2, minutes: 30 }
562 * @return {Duration}
563 */
564 mapUnits(fn) {
565 if (!this.isValid) return this;
566 const result = {};
567 for (const k of Object.keys(this.values)) {
568 result[k] = asNumber(fn(this.values[k], k));
569 }
570 return clone(this, { values: result }, true);
571 }
572
573 /**
574 * Get the value of unit.
575 * @param {string} unit - a unit such as 'minute' or 'day'
576 * @example Duration.fromObject({years: 2, days: 3}).get('years') //=> 2
577 * @example Duration.fromObject({years: 2, days: 3}).get('months') //=> 0
578 * @example Duration.fromObject({years: 2, days: 3}).get('days') //=> 3
579 * @return {number}
580 */
581 get(unit) {
582 return this[Duration.normalizeUnit(unit)];
583 }
584
585 /**
586 * "Set" the values of specified units. Return a newly-constructed Duration.
587 * @param {Object} values - a mapping of units to numbers
588 * @example dur.set({ years: 2017 })
589 * @example dur.set({ hours: 8, minutes: 30 })
590 * @return {Duration}
591 */
592 set(values) {
593 if (!this.isValid) return this;
594
595 const mixed = { ...this.values, ...normalizeObject(values, Duration.normalizeUnit) };
596 return clone(this, { values: mixed });
597 }
598
599 /**
600 * "Set" the locale and/or numberingSystem. Returns a newly-constructed Duration.
601 * @example dur.reconfigure({ locale: 'en-GB' })
602 * @return {Duration}
603 */
604 reconfigure({ locale, numberingSystem, conversionAccuracy } = {}) {
605 const loc = this.loc.clone({ locale, numberingSystem }),
606 opts = { loc };
607
608 if (conversionAccuracy) {
609 opts.conversionAccuracy = conversionAccuracy;
610 }
611
612 return clone(this, opts);
613 }
614
615 /**
616 * Return the length of the duration in the specified unit.
617 * @param {string} unit - a unit such as 'minutes' or 'days'
618 * @example Duration.fromObject({years: 1}).as('days') //=> 365
619 * @example Duration.fromObject({years: 1}).as('months') //=> 12
620 * @example Duration.fromObject({hours: 60}).as('days') //=> 2.5
621 * @return {number}
622 */
623 as(unit) {
624 return this.isValid ? this.shiftTo(unit).get(unit) : NaN;
625 }
626
627 /**
628 * Reduce this Duration to its canonical representation in its current units.
629 * @example Duration.fromObject({ years: 2, days: 5000 }).normalize().toObject() //=> { years: 15, days: 255 }
630 * @example Duration.fromObject({ hours: 12, minutes: -45 }).normalize().toObject() //=> { hours: 11, minutes: 15 }
631 * @return {Duration}
632 */
633 normalize() {
634 if (!this.isValid) return this;
635 const vals = this.toObject();
636 normalizeValues(this.matrix, vals);
637 return clone(this, { values: vals }, true);
638 }
639
640 /**
641 * Convert this Duration into its representation in a different set of units.
642 * @example Duration.fromObject({ hours: 1, seconds: 30 }).shiftTo('minutes', 'milliseconds').toObject() //=> { minutes: 60, milliseconds: 30000 }
643 * @return {Duration}
644 */
645 shiftTo(...units) {
646 if (!this.isValid) return this;
647
648 if (units.length === 0) {
649 return this;
650 }
651
652 units = units.map((u) => Duration.normalizeUnit(u));
653
654 const built = {},
655 accumulated = {},
656 vals = this.toObject();
657 let lastUnit;
658
659 for (const k of orderedUnits) {
660 if (units.indexOf(k) >= 0) {
661 lastUnit = k;
662
663 let own = 0;
664
665 // anything we haven't boiled down yet should get boiled to this unit
666 for (const ak in accumulated) {
667 own += this.matrix[ak][k] * accumulated[ak];
668 accumulated[ak] = 0;
669 }
670
671 // plus anything that's already in this unit
672 if (isNumber(vals[k])) {
673 own += vals[k];
674 }
675
676 const i = Math.trunc(own);
677 built[k] = i;
678 accumulated[k] = own - i; // we'd like to absorb these fractions in another unit
679
680 // plus anything further down the chain that should be rolled up in to this
681 for (const down in vals) {
682 if (orderedUnits.indexOf(down) > orderedUnits.indexOf(k)) {
683 convert(this.matrix, vals, down, built, k);
684 }
685 }
686 // otherwise, keep it in the wings to boil it later
687 } else if (isNumber(vals[k])) {
688 accumulated[k] = vals[k];
689 }
690 }
691
692 // anything leftover becomes the decimal for the last unit
693 // lastUnit must be defined since units is not empty
694 for (const key in accumulated) {
695 if (accumulated[key] !== 0) {
696 built[lastUnit] +=
697 key === lastUnit ? accumulated[key] : accumulated[key] / this.matrix[lastUnit][key];
698 }
699 }
700
701 return clone(this, { values: built }, true).normalize();
702 }
703
704 /**
705 * Return the negative of this Duration.
706 * @example Duration.fromObject({ hours: 1, seconds: 30 }).negate().toObject() //=> { hours: -1, seconds: -30 }
707 * @return {Duration}
708 */
709 negate() {
710 if (!this.isValid) return this;
711 const negated = {};
712 for (const k of Object.keys(this.values)) {
713 negated[k] = -this.values[k];
714 }
715 return clone(this, { values: negated }, true);
716 }
717
718 /**
719 * Get the years.
720 * @type {number}
721 */
722 get years() {
723 return this.isValid ? this.values.years || 0 : NaN;
724 }
725
726 /**
727 * Get the quarters.
728 * @type {number}
729 */
730 get quarters() {
731 return this.isValid ? this.values.quarters || 0 : NaN;
732 }
733
734 /**
735 * Get the months.
736 * @type {number}
737 */
738 get months() {
739 return this.isValid ? this.values.months || 0 : NaN;
740 }
741
742 /**
743 * Get the weeks
744 * @type {number}
745 */
746 get weeks() {
747 return this.isValid ? this.values.weeks || 0 : NaN;
748 }
749
750 /**
751 * Get the days.
752 * @type {number}
753 */
754 get days() {
755 return this.isValid ? this.values.days || 0 : NaN;
756 }
757
758 /**
759 * Get the hours.
760 * @type {number}
761 */
762 get hours() {
763 return this.isValid ? this.values.hours || 0 : NaN;
764 }
765
766 /**
767 * Get the minutes.
768 * @type {number}
769 */
770 get minutes() {
771 return this.isValid ? this.values.minutes || 0 : NaN;
772 }
773
774 /**
775 * Get the seconds.
776 * @return {number}
777 */
778 get seconds() {
779 return this.isValid ? this.values.seconds || 0 : NaN;
780 }
781
782 /**
783 * Get the milliseconds.
784 * @return {number}
785 */
786 get milliseconds() {
787 return this.isValid ? this.values.milliseconds || 0 : NaN;
788 }
789
790 /**
791 * Returns whether the Duration is invalid. Invalid durations are returned by diff operations
792 * on invalid DateTimes or Intervals.
793 * @return {boolean}
794 */
795 get isValid() {
796 return this.invalid === null;
797 }
798
799 /**
800 * Returns an error code if this Duration became invalid, or null if the Duration is valid
801 * @return {string}
802 */
803 get invalidReason() {
804 return this.invalid ? this.invalid.reason : null;
805 }
806
807 /**
808 * Returns an explanation of why this Duration became invalid, or null if the Duration is valid
809 * @type {string}
810 */
811 get invalidExplanation() {
812 return this.invalid ? this.invalid.explanation : null;
813 }
814
815 /**
816 * Equality check
817 * Two Durations are equal iff they have the same units and the same values for each unit.
818 * @param {Duration} other
819 * @return {boolean}
820 */
821 equals(other) {
822 if (!this.isValid || !other.isValid) {
823 return false;
824 }
825
826 if (!this.loc.equals(other.loc)) {
827 return false;
828 }
829
830 function eq(v1, v2) {
831 // Consider 0 and undefined as equal
832 if (v1 === undefined || v1 === 0) return v2 === undefined || v2 === 0;
833 return v1 === v2;
834 }
835
836 for (const u of orderedUnits) {
837 if (!eq(this.values[u], other.values[u])) {
838 return false;
839 }
840 }
841 return true;
842 }
843}
844
845/**
846 * @private
847 */
848export function friendlyDuration(durationish) {
849 if (isNumber(durationish)) {
850 return Duration.fromMillis(durationish);
851 } else if (Duration.isDuration(durationish)) {
852 return durationish;
853 } else if (typeof durationish === "object") {
854 return Duration.fromObject(durationish);
855 } else {
856 throw new InvalidArgumentError(
857 `Unknown duration argument ${durationish} of type ${typeof durationish}`
858 );
859 }
860}