UNPKG

@storm-stack/date-time

Version:

This package includes a DateTime class, various utility functions for working with dates and times, and a number of formatting options.

135 lines (134 loc) 5.4 kB
import { StormError } from "@storm-stack/errors"; import { MessageType } from "@storm-stack/types"; import { DateTimeErrorCode } from "../errors.mjs"; import { StormDateTime } from "../storm-date-time.mjs"; import { isDateTime } from "./is-date-time.mjs"; const pluralize = (word, count) => count === 1 ? word : `${word}s`; const SECOND_ROUNDING_EPSILON = 1e-7; const parseMilliseconds = (milliseconds) => { if (!Number.isFinite(milliseconds)) { throw StormError.createValidation( { code: DateTimeErrorCode.ms_format, type: MessageType.ERROR }, { message: "Method `parseMilliseconds` expected a finite number" } ); } return { days: Math.trunc(milliseconds / 864e5), hours: Math.trunc(milliseconds / 36e5) % 24, minutes: Math.trunc(milliseconds / 6e4) % 60, seconds: Math.trunc(milliseconds / 1e3) % 60, milliseconds: Math.trunc(milliseconds) % 1e3, microseconds: Math.trunc(milliseconds * 1e3) % 1e3, nanoseconds: Math.trunc(milliseconds * 1e6) % 1e3 }; }; export const formatSince = (dateTimeOrDuration, dateTimeTo = StormDateTime.current(), options) => { const colonNotation = options?.colonNotation ?? false; let compact = options?.compact ?? false; let formatSubMilliseconds = options?.formatSubMilliseconds ?? false; const keepDecimalsOnWholeSeconds = options?.keepDecimalsOnWholeSeconds ?? false; let millisecondsDecimalDigits = options?.millisecondsDecimalDigits ?? 0; let secondsDecimalDigits = options?.secondsDecimalDigits ?? 1; let separateMilliseconds = options?.separateMilliseconds ?? false; const unitCount = options?.unitCount ?? 0; let verbose = options?.verbose ?? false; let milliseconds; milliseconds = isDateTime(dateTimeOrDuration) ? dateTimeTo.since(dateTimeOrDuration).milliseconds : dateTimeOrDuration.milliseconds; if (!Number.isFinite(milliseconds)) { throw StormError.createValidation( { code: DateTimeErrorCode.ms_format, type: MessageType.ERROR }, { message: "Method `formatSince` expected a finite number" } ); } if (milliseconds < 0) { milliseconds *= -1; } if (colonNotation) { compact = false; formatSubMilliseconds = false; separateMilliseconds = false; verbose = false; } if (compact) { secondsDecimalDigits = 0; millisecondsDecimalDigits = 0; } const result = []; const floorDecimals = (value, decimalDigits) => { const flooredInterimValue = Math.floor( value * 10 ** decimalDigits + SECOND_ROUNDING_EPSILON ); const flooredValue = Math.round(flooredInterimValue) / 10 ** decimalDigits; return flooredValue.toFixed(decimalDigits); }; const add = (value, long, short, valueString) => { if ((result.length === 0 || !colonNotation) && value === 0 && !(colonNotation && short === "m")) { return; } let _valueString = (valueString || value || "0").toString(); let prefix; let suffix; if (colonNotation) { prefix = result.length > 0 ? ":" : ""; suffix = ""; const wholeDigits = _valueString.includes(".") ? _valueString.split(".")[0]?.length ?? 0 : _valueString.length; const minLength = result.length > 0 ? 2 : 1; _valueString = "0".repeat(Math.max(0, minLength - wholeDigits)) + _valueString; } else { prefix = ""; suffix = verbose ? ` ${pluralize(long, value)}` : short; } result.push(prefix + _valueString + suffix); }; const parsed = parseMilliseconds(milliseconds); add(Math.trunc(parsed.days / 365), "year", "y"); add(parsed.days % 365, "day", "d"); add(parsed.hours, "hour", "h"); add(parsed.minutes, "minute", "m"); if (separateMilliseconds || formatSubMilliseconds || !colonNotation && milliseconds < 1e3) { add(parsed.seconds, "second", "s"); if (formatSubMilliseconds) { add(parsed.milliseconds, "millisecond", "ms"); add(parsed.microseconds, "microsecond", "\xB5s"); add(parsed.nanoseconds, "nanosecond", "ns"); } else { const millisecondsAndBelow = parsed.milliseconds + parsed.microseconds / 1e3 + parsed.nanoseconds / 1e6; const roundedMilliseconds = millisecondsAndBelow >= 1 ? Math.round(millisecondsAndBelow) : Math.ceil(millisecondsAndBelow); const millisecondsString = millisecondsDecimalDigits ? millisecondsAndBelow.toFixed(millisecondsDecimalDigits) : String(roundedMilliseconds); add( Number.parseFloat(millisecondsString), "millisecond", "ms", millisecondsString ); } } else { const seconds = milliseconds / 1e3 % 60; const secondsFixed = floorDecimals(seconds, secondsDecimalDigits); const secondsString = keepDecimalsOnWholeSeconds ? secondsFixed : secondsFixed.replace(/\.0+$/, ""); add(Number.parseFloat(secondsString), "second", "s", secondsString); } if (result.length === 0) { return `0${verbose ? " milliseconds" : "ms"}`; } if (compact) { if (!result[0]) { throw StormError.createValidation( { code: DateTimeErrorCode.formatting_failure, type: MessageType.ERROR }, { message: "Unexpected empty result" } ); } return result[0]; } if (typeof unitCount === "number") { const separator = colonNotation ? "" : " "; return result.slice(0, Math.max(unitCount, 1)).join(separator); } return colonNotation ? result.join("") : result.join(" "); };