UNPKG

@date-vir/duration

Version:

Durations units an utils for date-vir.

121 lines (120 loc) 4.51 kB
import { Duration as LuxonDuration } from 'luxon'; import { flattenUnitsSmallestToLargest, } from './duration-selection.js'; import { DurationUnit } 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, { decimalCount }) { if (decimalCount == undefined) { return value; } const digitFactor = Math.pow(10, decimalCount); const multiplied = value * digitFactor; return Number((Math.round(multiplied) / digitFactor).toFixed(decimalCount)); } /** Round up only if the decimal is >=.9 */ function roundNarrow(value) { return round(Math.max(value - 0.4, 0), { decimalCount: 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 = { decimalCount: rawOptions.decimalCount == undefined ? undefined : Math.round(Math.abs(rawOptions.decimalCount)), }; /** Handle infinite units. */ const hasInfinity = Object.values(duration).includes(Infinity); const hasNegativeInfinity = Object.values(duration).includes(-Infinity); const selectedUnits = flattenUnitsSmallestToLargest(units).reverse(); if (hasInfinity || hasNegativeInfinity) { selectedUnits.forEach((unit) => { finalDuration[unit] = hasInfinity ? Infinity : -Infinity; }); return finalDuration; } let millisecondsRemaining = LuxonDuration.fromObject(duration).as(DurationUnit.Milliseconds); const finalDurationSign = getSign(millisecondsRemaining); selectedUnits.forEach((durationUnit, index) => { const isLastUnit = index === selectedUnits.length - 1; 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.decimalCount == 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; } } }); 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), ]; })); }