datezone
Version:
A lightweight and comprehensive date and timeZone utility library for JavaScript.
237 lines • 6.77 kB
JavaScript
import { getCachedNumberFormat } from "./number-format-cache.js";
/**
* Mapping from Duration keys to Intl unit identifiers
*/
const INTL_UNIT_MAP = {
days: "day",
hours: "hour",
milliseconds: "millisecond",
minutes: "minute",
months: "month",
seconds: "second",
weeks: "week",
years: "year",
};
/**
* Default order of units for formatting
*/
const DEFAULT_UNIT_ORDER = [
"years",
"months",
"weeks",
"days",
"hours",
"minutes",
"seconds",
"milliseconds",
];
/**
* Fast English unit names for direct string building
*/
const EN_UNIT_NAMES = {
days: ["day", "days"],
hours: ["hour", "hours"],
milliseconds: ["millisecond", "milliseconds"],
minutes: ["minute", "minutes"],
months: ["month", "months"],
seconds: ["second", "seconds"],
weeks: ["week", "weeks"],
years: ["year", "years"],
};
/**
* Efficient formatter cache using Map with composite keys
* Format: Map<locale, Map<unit, Intl.NumberFormat>>
*/
const FORMATTER_CACHE = new Map();
/**
* Reusable options object to avoid object allocation on every call
*/
const UNIT_FORMAT_OPTIONS = {
style: "unit",
unit: "year", // Will be overwritten
unitDisplay: "long",
};
/**
* Fast path for English locale formatting
*/
function formatEnglishUnit(value, unit) {
const [singular, plural] = EN_UNIT_NAMES[unit];
return value === 1 ? `1 ${singular}` : `${value} ${plural}`;
}
/**
* Get formatter for a specific locale and unit with optimized caching
* On first use of a locale, cache ALL units for that locale at once
*/
function getFormatter(locale, unit) {
// Get or create locale cache
let localeCache = FORMATTER_CACHE.get(locale);
if (!localeCache) {
// First time using this locale - cache ALL units at once
localeCache = new Map();
FORMATTER_CACHE.set(locale, localeCache);
// Pre-cache all duration units for this locale
for (const intlUnit of Object.values(INTL_UNIT_MAP)) {
UNIT_FORMAT_OPTIONS.unit = intlUnit;
const formatter = getCachedNumberFormat(locale, UNIT_FORMAT_OPTIONS);
localeCache.set(intlUnit, formatter);
}
}
// Return the cached formatter
const intlUnit = INTL_UNIT_MAP[unit];
return localeCache.get(intlUnit);
}
/**
* Pre-allocate arrays for common format scenarios to avoid allocations
*/
const tempParts = new Array(8); // Max 8 duration units
/**
* Performance monitoring and cache management utilities
*/
export const formatDurationPerformance = {
/**
* Clear all caches to free memory (use in long-running applications)
*/
clearCaches() {
FORMATTER_CACHE.clear();
},
/**
* Get cache statistics for monitoring
*/
getCacheStats() {
const formatterCacheSize = Array.from(FORMATTER_CACHE.values()).reduce((total, localeCache) => total + localeCache.size, 0);
return {
formatterCacheSize,
formatterLocales: FORMATTER_CACHE.size,
};
},
/**
* Pre-warm cache for specific locales (optimization for known usage patterns)
*/
warmCacheForLocales(locales) {
for (const locale of locales) {
// Just call getFormatter for any unit to trigger full locale caching
getFormatter(locale, "hours");
}
},
};
/**
* Format a duration object into a human-readable string
*
* @param duration The duration object to format
* @param options Formatting options
* @returns Formatted duration string
*
* @example
* ```ts
* formatDuration({
* years: 2,
* months: 9,
* weeks: 1,
* days: 7,
* hours: 5,
* minutes: 9,
* seconds: 30
* })
* //=> '2 years 9 months 1 week 7 days 5 hours 9 minutes 30 seconds'
* ```
*
* @example
* ```ts
* formatDuration({ months: 9, days: 2 })
* //=> '9 months 2 days'
* ```
*
* @example
* ```ts
* formatDuration(
* {
* years: 2,
* months: 9,
* weeks: 1,
* days: 7,
* hours: 5,
* minutes: 9,
* seconds: 30
* },
* { format: ['months', 'weeks'] }
* )
* //=> '9 months 1 week'
* ```
*
* @example
* ```ts
* formatDuration({ years: 0, months: 9 }, { zero: true })
* //=> '0 years 9 months'
* ```
*
* @example
* ```ts
* formatDuration({ years: 2, months: 9, weeks: 3 }, { delimiter: ', ' })
* //=> '2 years, 9 months, 3 weeks'
* ```
*
* @example
* ```ts
* formatDuration({ years: 2, months: 1 }, { locale: 'fr' })
* //=> '2 années 1 mois'
* ```
*/
export function formatDuration(duration, options) {
const { format = DEFAULT_UNIT_ORDER, zero = false, delimiter = " ", locale = "en", } = options ?? {};
// Ultra-fast path for English locale with default delimiter
if (locale === "en" && delimiter === " ") {
let result = "";
let hasOutput = false;
for (let i = 0; i < format.length; i++) {
const unit = format[i];
const value = duration[unit];
// Skip if value is undefined or zero (unless zero option is enabled)
if (value === undefined || (value === 0 && !zero)) {
continue;
}
// Add space delimiter if we already have output
if (hasOutput) {
result += " ";
}
// Direct string building for English - no Intl overhead
result += formatEnglishUnit(value, unit);
hasOutput = true;
}
return result;
}
// Fallback to Intl.NumberFormat for non-English locales or custom delimiters
let partCount = 0;
// First pass: collect all parts into pre-allocated array
for (let i = 0; i < format.length; i++) {
const unit = format[i];
const value = duration[unit];
// Skip if value is undefined or zero (unless zero option is enabled)
if (value === undefined || (value === 0 && !zero)) {
continue;
}
// Get formatter and format value
const formatter = getFormatter(locale, unit);
tempParts[partCount] = formatter.format(value);
partCount++;
}
// Fast path for empty result
if (partCount === 0) {
return "";
}
// Fast path for single part
if (partCount === 1) {
return tempParts[0];
}
// Fast path for default delimiter (space)
if (delimiter === " ") {
// Use slice to avoid the overhead of creating an array from tempParts
return tempParts.slice(0, partCount).join(" ");
}
// General case with custom delimiter
let result = tempParts[0];
for (let i = 1; i < partCount; i++) {
result += delimiter + tempParts[i];
}
return result;
}
//# sourceMappingURL=format-duration.pub.js.map