mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
141 lines
5.1 KiB
JavaScript
141 lines
5.1 KiB
JavaScript
import { isNumber } from "../predicate/predicate.js";
|
|
//#region src/time/format.ts
|
|
const DurationFormat = Intl.DurationFormat;
|
|
const durationFormatters = /* @__PURE__ */ new Map();
|
|
/**
|
|
* `Intl.DurationFormat` is unavailable on Node < 23 (SSR/prerender) and pre-2024 evergreen
|
|
* browsers, so degrade gracefully per the documented browser-support fallback policy.
|
|
* Digital output stays exact; localized phrase styles fall back to English.
|
|
*/
|
|
function createFallbackFormatter(style, hoursDisplay) {
|
|
if (style === "digital") {
|
|
const pad = (value) => String(value).padStart(2, "0");
|
|
return { format: (duration) => {
|
|
const body = `${pad(duration.minutes ?? 0)}:${pad(duration.seconds ?? 0)}`;
|
|
return hoursDisplay === "always" || duration.hours !== void 0 ? `${duration.hours ?? 0}:${body}` : body;
|
|
} };
|
|
}
|
|
const units = [
|
|
["hours", "hour"],
|
|
["minutes", "minute"],
|
|
["seconds", "second"]
|
|
];
|
|
return { format: (duration) => units.filter(([unit]) => duration[unit] !== void 0).map(([unit, label]) => {
|
|
const value = duration[unit] ?? 0;
|
|
return `${value} ${label}${value === 1 ? "" : "s"}`;
|
|
}).join(", ") };
|
|
}
|
|
function localeCacheKey(locale) {
|
|
if (locale === void 0) return "";
|
|
return Array.isArray(locale) ? locale.join(":") : locale;
|
|
}
|
|
function isEnglishLocale(locale) {
|
|
const tag = Array.isArray(locale) ? locale[0] : locale;
|
|
if (!tag) return true;
|
|
return tag === "en" || tag.startsWith("en-");
|
|
}
|
|
function getDurationFormatter(locale, style = "long", hoursDisplay) {
|
|
const key = `${localeCacheKey(locale)}:${style}:${hoursDisplay ?? ""}`;
|
|
let formatter = durationFormatters.get(key);
|
|
if (!formatter) {
|
|
if (DurationFormat) formatter = new DurationFormat(locale, hoursDisplay === void 0 ? { style } : {
|
|
style,
|
|
hoursDisplay
|
|
});
|
|
else formatter = createFallbackFormatter(style, hoursDisplay);
|
|
durationFormatters.set(key, formatter);
|
|
}
|
|
return formatter;
|
|
}
|
|
function isValidTime(value) {
|
|
return isNumber(value) && Number.isFinite(value);
|
|
}
|
|
/**
|
|
* Format seconds to digital display string.
|
|
*
|
|
* @param seconds - Time in seconds (can be negative)
|
|
* @param guide - Guide time (typically duration) to determine display format
|
|
* @returns Formatted string like "1:30" or "1:05:30"
|
|
*
|
|
* @example
|
|
* formatTime(90) // "1:30"
|
|
* formatTime(3661) // "1:01:01"
|
|
* formatTime(35, 3600) // "0:00:35" (guided by 1-hour duration)
|
|
* formatTime(35, 600) // "00:35" (guided by 10-minute duration)
|
|
*/
|
|
function formatTime(seconds, guide) {
|
|
if (!isValidTime(seconds)) return "0:00";
|
|
const negative = seconds < 0;
|
|
const totalSeconds = Math.floor(Math.abs(seconds));
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
const secondsPart = totalSeconds % 60;
|
|
const guideSeconds = isValidTime(guide ?? 0) ? Math.abs(guide ?? 0) : 0;
|
|
const guideHours = Math.floor(guideSeconds / 3600);
|
|
const guideMinutes = Math.floor(guideSeconds / 60 % 60);
|
|
const showHours = hours > 0 || guideHours > 0;
|
|
const padMinutes = showHours || guideMinutes >= 10;
|
|
const duration = showHours ? {
|
|
hours,
|
|
minutes,
|
|
seconds: secondsPart
|
|
} : {
|
|
minutes,
|
|
seconds: secondsPart
|
|
};
|
|
let body = getDurationFormatter("en", "digital", showHours ? "always" : "auto").format(duration);
|
|
if (!padMinutes) body = body.replace(/^0(?=\d:)/, "");
|
|
return `${negative ? "-" : ""}${body}`;
|
|
}
|
|
/**
|
|
* Convert seconds to ISO 8601 duration for datetime attribute.
|
|
*
|
|
* @param seconds - Time in seconds
|
|
* @returns ISO 8601 duration string like "PT1M30S"
|
|
*
|
|
* @example
|
|
* secondsToIsoDuration(90) // "PT1M30S"
|
|
* secondsToIsoDuration(3661) // "PT1H1M1S"
|
|
*/
|
|
function secondsToIsoDuration(seconds) {
|
|
if (!isValidTime(seconds)) return "PT0S";
|
|
const positiveSeconds = Math.abs(seconds);
|
|
const h = Math.floor(positiveSeconds / 3600);
|
|
const m = Math.floor(positiveSeconds / 60 % 60);
|
|
const s = Math.floor(positiveSeconds % 60);
|
|
let duration = "PT";
|
|
if (h > 0) duration += `${h}H`;
|
|
if (m > 0) duration += `${m}M`;
|
|
if (s > 0 || duration === "PT") duration += `${s}S`;
|
|
return duration;
|
|
}
|
|
/**
|
|
* Human-readable duration using {@link Intl.DurationFormat}.
|
|
*
|
|
* Negative `seconds` denote remaining time: the absolute value is formatted, then wrapped in a
|
|
* localized phrase via {@link TimeFormatOptions.formatRemaining}; otherwise `{duration} remaining`.
|
|
*/
|
|
function formatTimeAsPhrase(seconds, options) {
|
|
if (!isValidTime(seconds)) return "";
|
|
const negative = seconds < 0;
|
|
const totalSeconds = Math.floor(Math.abs(seconds));
|
|
const hours = Math.floor(totalSeconds / 3600);
|
|
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
const secondsPart = totalSeconds % 60;
|
|
const record = {};
|
|
if (hours > 0) record.hours = hours;
|
|
if (minutes > 0) record.minutes = minutes;
|
|
if (secondsPart > 0 || hours === 0 && minutes === 0) record.seconds = secondsPart;
|
|
const body = getDurationFormatter(options?.locale, options?.style ?? "long").format(record);
|
|
if (negative) {
|
|
const formatRemaining = options?.formatRemaining;
|
|
if (formatRemaining) return formatRemaining(body);
|
|
if (isEnglishLocale(options?.locale)) return `${body} remaining`;
|
|
return body;
|
|
}
|
|
return body;
|
|
}
|
|
//#endregion
|
|
export { formatTime, formatTimeAsPhrase, secondsToIsoDuration };
|
|
|
|
//# sourceMappingURL=format.js.map
|