2025-01-27 15:53:20 +03:00

174 lines
4.6 KiB
TypeScript

// Copyright 2018-2025 the Deno authors. MIT license.
// This module is browser compatible.
/**
* Format milliseconds to time duration.
*
* ```ts
* import { format } from "@std/fmt/duration";
* import { assertEquals } from "@std/assert";
*
* assertEquals(format(99674, { style: "digital" }), "00:00:01:39:674:000:000");
*
* assertEquals(format(99674), "0d 0h 1m 39s 674ms 0µs 0ns");
*
* assertEquals(format(99674, { ignoreZero: true }), "1m 39s 674ms");
*
* assertEquals(format(99674, { style: "full", ignoreZero: true }), "1 minute, 39 seconds, 674 milliseconds");
* ```
* @module
*/
function addZero(num: number, digits: number) {
return String(num).padStart(digits, "0");
}
type DurationPartUnit =
| "days"
| "hours"
| "minutes"
| "seconds"
| "milliseconds"
| "microseconds"
| "nanoseconds";
interface DurationPart {
unit: DurationPartUnit;
value: number;
}
const NARROW_UNIT_NAME_MAP = new Map<DurationPartUnit, string>([
["days", "d"],
["hours", "h"],
["minutes", "m"],
["seconds", "s"],
["milliseconds", "ms"],
["microseconds", "µs"],
["nanoseconds", "ns"],
]);
const FULL_UNIT_NAME_MAP = new Map<DurationPartUnit, string>([
["days", "day"],
["hours", "hour"],
["minutes", "minute"],
["seconds", "second"],
["milliseconds", "millisecond"],
["microseconds", "microsecond"],
["nanoseconds", "nanosecond"],
]);
/** Get key with pluralization */
function getPluralizedKey(unit: DurationPartUnit, value: number) {
return value === 1
? FULL_UNIT_NAME_MAP.get(unit)
: `${FULL_UNIT_NAME_MAP.get(unit)}s`;
}
/** Parse milliseconds into a duration. */
function millisecondsToDurationParts(
ms: number,
): DurationPart[] {
// Duration cannot be negative
const millis = Math.abs(ms);
const millisFraction = millis.toFixed(7).slice(-7, -1);
return [
{ unit: "days", value: Math.trunc(millis / 86400000) },
{ unit: "hours", value: Math.trunc(millis / 3600000) % 24 },
{ unit: "minutes", value: Math.trunc(millis / 60000) % 60 },
{ unit: "seconds", value: Math.trunc(millis / 1000) % 60 },
{ unit: "milliseconds", value: Math.trunc(millis) % 1000 },
{ unit: "microseconds", value: +millisFraction.slice(0, 3) },
{ unit: "nanoseconds", value: +millisFraction.slice(3, 6) },
];
}
/** Options for {@linkcode format}. */
export interface FormatOptions {
/**
* The style for formatting the duration.
*
* "narrow" for "0d 0h 0m 0s 0ms..."
* "digital" for "00:00:00:00:000..."
* "full" for "0 days, 0 hours, 0 minutes,..."
*
* @default {"narrow"}
*/
style?: "narrow" | "digital" | "full";
/**
* Whether to ignore zero values.
* With style="narrow" | "full", all zero values are ignored.
* With style="digital", only values in the ends are ignored.
*
* @default {false}
*/
ignoreZero?: boolean;
}
/**
* Format milliseconds to time duration.
*
* @example Usage
* ```ts
* import { format } from "@std/fmt/duration";
* import { assertEquals } from "@std/assert";
*
* assertEquals(format(99674, { style: "digital" }), "00:00:01:39:674:000:000");
*
* assertEquals(format(99674), "0d 0h 1m 39s 674ms 0µs 0ns");
*
* assertEquals(format(99674, { ignoreZero: true }), "1m 39s 674ms");
*
* assertEquals(format(99674, { style: "full", ignoreZero: true }), "1 minute, 39 seconds, 674 milliseconds");
* ```
*
* @param ms The milliseconds value to format
* @param options The options for formatting
* @returns The formatted string
*/
export function format(
ms: number,
options?: FormatOptions,
): string {
const {
style = "narrow",
ignoreZero = false,
} = options ?? {};
const parts = millisecondsToDurationParts(ms);
switch (style) {
case "narrow": {
let arr = parts;
if (ignoreZero) arr = arr.filter((x) => x.value);
return arr
.map((x) => `${x.value}${NARROW_UNIT_NAME_MAP.get(x.unit)}`)
.join(" ");
}
case "full": {
let arr = parts;
if (ignoreZero) arr = arr.filter((x) => x.value);
return arr
.map((x) => `${x.value} ${getPluralizedKey(x.unit, x.value)}`)
.join(", ");
}
case "digital": {
const arr = parts.map((x) =>
["milliseconds", "microseconds", "nanoseconds"].includes(x.unit)
? addZero(x.value, 3)
: addZero(x.value, 2)
);
if (ignoreZero) {
let cont = true;
while (cont) {
if (!Number(arr[arr.length - 1])) arr.pop();
else cont = false;
}
}
return arr.join(":");
}
default: {
throw new TypeError(`style must be "narrow", "full", or "digital"!`);
}
}
}