UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

169 lines (149 loc) 5.36 kB
import {useMemo} from 'react' import {useCurrentLocale} from '../i18n/hooks/useLocale' import {intlCache} from '../i18n/intlCache' import {useListFormat} from './useListFormat' type Duration = { days: number hours: number minutes: number seconds: number milliseconds: number } type DurationUnit = keyof Duration const PERIODS = ['days', 'hours', 'minutes', 'seconds', 'milliseconds'] as const /** * Options for the duration formatter * * @public */ export interface UseFormattedDurationOptions { /** * The formatting style to use in unit and list formatting. The default is "short". */ style?: 'short' | 'long' | 'narrow' /** * The resolution of the duration. The default is "seconds". */ resolution?: 'seconds' | 'milliseconds' } /** * The result of the duration formatter * * @public */ export interface FormattedDuration { /** The human-readable, formatted duration as a string, eg "2 days, 3 hr, and 20 sec" */ formatted: string /** The machine-readable, formatted ISO-8601 duration string, eg "P2DT3H20S" */ iso8601: string } /** * Formats a duration (in milliseconds) to a more user friendly string eg `1h 30m` or `1t 29m 15s`. * Can be configured to output full units, eg `1 hour 30 minutes` or `1 hour 3 minutes 15 seconds`. * Uses the current locale, which also applies to the division of units. * * @example English (en-US) locale formatting * ```ts * useFormattedDuration(5589000) * // {"formatted": "1 hour, 33 minutes, and 9 seconds", "iso8601": "PT1H33M9S"} * ``` * * @example Norwegian (no-NB) locale formatting * ```ts * useFormattedDuration(5589000) * // {"formatted": "1 time, 33 minutter og 9 sekunder", "iso8601": "PT1H33M9S"} * ``` * * @param options - Optional options for the number formatter * @returns An object with `formatted` and `iso8601` properties * @public */ export function useFormattedDuration( durationMs: number, options?: UseFormattedDurationOptions, ): FormattedDuration { const {style = 'short', resolution = 'seconds'} = options || {} const unitDisplay = style const locale = useCurrentLocale().id const listFormat = useListFormat({type: 'unit', style}) const isNegative = durationMs < 0 const duration = parseMilliseconds(Math.abs(durationMs)) const formatters: Record<DurationUnit, Intl.NumberFormat> = useMemo( () => ({ days: intlCache.numberFormat(locale, {style: 'unit', unit: 'day', unitDisplay}), hours: intlCache.numberFormat(locale, {style: 'unit', unit: 'hour', unitDisplay}), minutes: intlCache.numberFormat(locale, {style: 'unit', unit: 'minute', unitDisplay}), seconds: intlCache.numberFormat(locale, {style: 'unit', unit: 'second', unitDisplay}), milliseconds: intlCache.numberFormat(locale, { style: 'unit', unit: 'millisecond', unitDisplay, }), }), [locale, unitDisplay], ) const parts: string[] = [] for (const period of PERIODS) { const value = duration[period] if (!value || (resolution === 'seconds' && period === 'milliseconds')) { continue } const prefix = isNegative && parts.length === 0 ? '-' : '' parts.push(`${prefix}${formatters[period].format(value)}`) } const formatted = parts.length === 0 ? // If passing duration 0, we still want to show something formatters[resolution].format(0) : // Usually, we want to join the parts with the locales list formatter listFormat.format(parts) const iso8601 = durationToISO8601(duration, isNegative) return {formatted, iso8601} } /** * Parse milliseconds to durations. We stop at days because months are not a fixed unit, * nor are years (365.25 strictly speaking, but people may expact calendar days). If we * wanted to include those, we'd want to use `intervalToDuration` from date-fns, and base * it on two specific dates. * * @param milliseconds - Milliseconds to parse * @returns Object of duration parts * @internal */ function parseMilliseconds(milliseconds: number): Duration { return { days: Math.trunc(milliseconds / 86400000), hours: Math.trunc(milliseconds / 3600000) % 24, minutes: Math.trunc(milliseconds / 60000) % 60, seconds: Math.trunc(milliseconds / 1000) % 60, milliseconds: Math.trunc(milliseconds) % 1000, } } /** * Format the given duration to an {@link https://en.wikipedia.org/wiki/ISO_8601#Durations | ISO-8601 duration} * string, eg `P1DT2H3M4.005S` meaning "1 day, 2 hours, 3 minutes, 4 seconds and 5 milliseconds". * * If days are zero, it will skip straight to the time part, eg `PT2H3M4.005S`. * If milliseconds are zero, it will skip the milliseconds part, eg `PT2H3M4S`. * * @param dur - The duration to format * @returns The formatted duration * @internal */ function durationToISO8601(dur: Duration, isNegative: boolean): string { const date = dur.days ? `${dur.days}D` : '' let time = '' if (dur.hours) time += `${dur.hours}H` if (dur.minutes) time += `${dur.minutes}M` if (dur.milliseconds) { time += `${((dur.seconds * 1000 + dur.milliseconds) / 1000).toFixed(3)}S` } else if (dur.seconds) { time += `${dur.seconds}S` } if (!date && !time) { return 'PT0S' } const parts = time ? [date, time] : [date] const duration = `P${parts.join('T')}` return isNegative ? `-${duration}` : duration }