/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { UnexpectedDataError } from "#MatterError.js"; import { Branded } from "#util/Type.js"; import type { Millis, Seconds, TimeUnit } from "./TimeUnit.js"; /** * A time interval. * * You can create an interval using a {@link TimeUnit} factory such as {@link Seconds}. * * Regardless of the input unit, intervals are stored as milliseconds. You can use {@link TimeUnit#of} to convert an * interval to the correct unit. * * Math operators always result in millisecond values and can thus be converted back to an interval using * {@link Millis}. For example, `Millisecs(Hours(1) + Minutes(30))` would produce a 90 minute {@link Duration}. */ export type Duration = Branded | 0; /** * Create an interval from a number or string. */ export function Duration(source: T): Duration { if (typeof source === "string") { return Duration.parse(source); } if (typeof source === "number") { if (!Number.isFinite(source)) { throw new DurationFormatError(`A duration must be a finite number`); } return source; } throw new DurationFormatError(`Interval value is not a number (received ${typeof source})`); } /** * Thrown when a textual duration cannot be parsed. */ export class DurationFormatError extends UnexpectedDataError {} export namespace Duration { /** * Determine the greater of two intervals. */ export function max(a: Duration, b: Duration) { if (b > a) { return b; } return a; } /** * Determine the lesser of two intervals. */ export function min(a: Duration, b: Duration) { if (b < a) { return b; } return a; } /** * Convert an interval to a compact human-readable string. */ export function format( duration: T, ): T extends undefined ? string | undefined : string { if (duration === undefined) { return undefined as T extends undefined ? string | undefined : string; } const ms = duration as number; if (typeof ms !== "number" || Number.isNaN(ms)) { return "invalid"; } switch (ms) { case 0: return "0"; case Infinity: return "forever"; case -Infinity: return "until now"; // Umm... I guess? } const negative = ms < 0 ? "-" : ""; let absMs = Math.abs(ms); if (absMs < 1) { return `${negative}${toPrecision(absMs * 1000, 3)}μs`; } else if (absMs < 1000) { return `${negative}${toPrecision(absMs, 3)}ms`; } else if (absMs < 60000) { return `${negative}${toPrecision(absMs / 1000, 3)}s`; } const parts = Array(); if (absMs > 86_400_000) { parts.push(`${Math.floor(absMs / 86_400_000)}d`); absMs %= 86_400_000; } const hours = Math.floor(absMs / 3_600_000); if (hours) { parts.push(`${hours}h`); } absMs %= 3_600_000; const minutes = Math.floor(absMs / 60_000); if (minutes) { parts.push(`${minutes}m`); } absMs %= 60_000; const seconds = Math.floor(absMs / 1_000); if (seconds) { parts.push(`${seconds}s`); } return `${negative}${parts.join(" ")}`; } /** * Parse a string into an interval. */ export function parse(text: string) { const parts = text.split(/\s+/).filter(part => part !== ""); let interval = 0; for (const part of parts) { const suffix = part.match(/[a-zμ]+$/i)?.[0]; if (suffix === undefined) { throw new DurationFormatError(`Interval component "${part}" is missing a time suffix`); } const numericPart = part.slice(0, part.length - suffix.length); if (numericPart === "") { throw new DurationFormatError(`Interval component "${part}" contains no numeric component`); } const value = Number(numericPart); if (!Number.isFinite(value)) { throw new DurationFormatError(`Interval component "${part}" contains an invalid numeric value`); } switch (suffix.toLowerCase()) { case "μs": case "us": interval += value / 1000; break; case "ms": interval += value; break; case "s": interval += value * 1000; break; case "m": interval += value * 60_000; break; case "h": interval += value * 3_600_000; break; case "d": interval += value * 86_400_000; break; default: throw new DurationFormatError(`Interval component ${part} contains an unsupported unit suffix`); } } return interval as Duration; } } function toPrecision(number: number, precision: number) { // Remove trailing zeros after a non-zero decimal digit, then remove any trailing '.0' return number .toPrecision(precision) .replace(/(\.\d*?[1-9])0+$/, "$1") .replace(/\.0+$/, ""); }