@date-vir/duration
Version:
Durations units an utils for date-vir.
136 lines (135 loc) • 5.19 kB
JavaScript
import { Duration as LuxonDuration } from 'luxon';
import { flattenUnitSelection, } from './duration-selection.js';
import { DurationUnit, orderedDurationUnits } from './units/duration-unit.js';
/**
* This is mostly copied from `@augment-vir/common` so that the `@date-vir/duration` package doesn't
* need to depend on any `@augment-vir/*` packages as that would cause a circular dependency inside
* of the `@augment-vir/*` packages.
*/
function round(value, { roundToDigits }) {
if (roundToDigits == undefined) {
return value;
}
const digitFactor = Math.pow(10, roundToDigits);
const multiplied = value * digitFactor;
return Number((Math.round(multiplied) / digitFactor).toFixed(roundToDigits));
}
/** Round up only if the decimal is >=.9 */
function roundNarrow(value) {
return round(Math.max(value - 0.4, 0), { roundToDigits: 0 });
}
function getSign(value) {
if (value === 0) {
return 0;
}
else {
return Math.sign(value);
}
}
/**
* Convert a duration from any combination of units into the selected units.
*
* @category Duration
* @example
*
* ```ts
* import {convertDuration} from 'date-vir';
*
* convertDuration({seconds: 5, milliseconds: 5}, {minutes: true}); // `{minutes: 0.25}`
* convertDuration({seconds: 5_356_800}, {weeks: true, seconds: true}); // `{weeks: 8, seconds: 518_400}`
* ```
*/
export function convertDuration(duration,
/** Select which duration units to convert to. */
units, rawOptions = {}) {
const finalDuration = {};
const options = {
roundToDigits: rawOptions.roundToDigits == undefined
? undefined
: Math.round(Math.abs(rawOptions.roundToDigits)),
};
const hasInfinity = Object.values(duration).includes(Infinity);
const hasNegativeInfinity = Object.values(duration).includes(-Infinity);
let millisecondsRemaining = LuxonDuration.fromObject(duration).as(DurationUnit.Milliseconds);
const selectedUnits = flattenUnitSelection(units).reverse();
const finalDurationSign = getSign(millisecondsRemaining);
selectedUnits.forEach((durationUnit, index) => {
const isLastUnit = index === selectedUnits.length - 1;
if ((hasInfinity && hasNegativeInfinity) || hasInfinity) {
finalDuration[durationUnit] = Infinity;
}
else if (hasNegativeInfinity) {
finalDuration[durationUnit] = -Infinity;
}
else if (durationUnit === DurationUnit.Milliseconds) {
finalDuration.milliseconds = round(millisecondsRemaining, options);
}
else {
const rawQuantity = LuxonDuration.fromObject({ milliseconds: millisecondsRemaining }).as(durationUnit);
// positive or negative
const signModifier = Math.sign(rawQuantity);
const absoluteQuantity = Math.abs(rawQuantity);
const absoluteRoundedUnitQuantity = isLastUnit
? round(absoluteQuantity, options)
: Math.floor(options.roundToDigits == undefined
? absoluteQuantity
: roundNarrow(absoluteQuantity));
const signedRoundedQuantity = absoluteRoundedUnitQuantity === 0
? /** Use this ternary to prevent `-0` assignment. */
0
: absoluteRoundedUnitQuantity * signModifier;
finalDuration[durationUnit] = signedRoundedQuantity;
millisecondsRemaining -= LuxonDuration.fromObject({
[durationUnit]: signedRoundedQuantity,
}).as(DurationUnit.Milliseconds);
if (finalDurationSign !== getSign(millisecondsRemaining)) {
/**
* If rounding resulted in the amount of milliseconds remaining flipping across `0`,
* just zero it out to prevent crazy values.
*/
millisecondsRemaining = 0;
}
}
});
let foundNonZero = false;
const removedUnits = [];
const usedUnits = orderedDurationUnits.toReversed().filter((unit) => {
if (finalDuration[unit]) {
foundNonZero = true;
return true;
}
else if (foundNonZero) {
removedUnits.push(unit);
return false;
}
return true;
});
if (usedUnits.length < selectedUnits.length) {
const newUnits = {};
usedUnits.forEach((unit) => (newUnits[unit] = true));
const recursiveConversion = convertDuration(duration, newUnits, options);
removedUnits.forEach((unit) => (recursiveConversion[unit] = 0));
return recursiveConversion;
}
return finalDuration;
}
/**
* Round every unit in a given duration to the given number of decimal points.
*
* @category Duration
* @example
*
* ```ts
* import {roundDuration} from 'date-vir';
*
* roundDuration({days: 1.002, minutes: 0.125}, 2); // `{days: 1, minutes: 0.13}`
* ```
*/
export function roundDuration(duration, options) {
return Object.fromEntries(Object.entries(duration).map(([durationUnit, quantity,]) => {
return [
durationUnit,
round(quantity, options),
];
}));
}