Formatting
Format numbers, so'm, phones, cards, dates, relative time, durations and file sizes; spell out numbers and money in Uzbek.
The formats people expect in Uzbekistan work out of the box: 1 250 000 so'm,
+998 90 123 45 67, 21 Oktabr 2026, Chorshanba, 5 daqiqa oldin. The functions don't depend
on Intl or the browser locale, so the output is identical on the server and in every browser.
Bad input never throws — you get an empty string ''.
import {
formatNumber,
formatMoney,
formatPhone,
formatCardNumber,
formatDate,
parseDate,
timeAgo,
formatFileSize,
numberToWords,
moneyToWords,
ordinal,
ordinalWords,
formatDateUz,
formatDateRange,
formatDuration,
} from '@akhatmirzo/core';Numbers and money
- formatNumber(n)
- 1 250 000.5
- formatNumber(n, { decimals: 2 })
- 1 250 000.50
- formatMoney(n, { currency })
- 1 250 001 so'm
'use client';
import { formatMoney, formatNumber, type MoneyCurrency } from '@akhatmirzo/core';
import { Button, Field, Input } from '@akhatmirzo/ui';
import { useState } from 'react';
const CURRENCIES: MoneyCurrency[] = ["so'm", 'UZS', false];
export default function CoreMoney() {
const [raw, setRaw] = useState('1250000.5');
const [currency, setCurrency] = useState<MoneyCurrency>("so'm");
const n = Number(raw.replace(',', '.').replace(/\s/g, ''));
const rows = [
{ call: 'formatNumber(n)', value: formatNumber(n) },
{ call: 'formatNumber(n, { decimals: 2 })', value: formatNumber(n, { decimals: 2 }) },
{ call: 'formatMoney(n, { currency })', value: formatMoney(n, { currency }) },
];
return (
<div className="flex w-full max-w-md flex-col gap-4">
<Field label="Son">
<Input value={raw} onChange={(e) => setRaw(e.target.value)} inputMode="decimal" />
</Field>
<div className="flex flex-wrap items-center gap-2">
{CURRENCIES.map((c) => (
<Button
key={String(c)}
size="sm"
variant={currency === c ? 'primary' : 'outline'}
onClick={() => setCurrency(c)}
>
{c === false ? 'false' : c}
</Button>
))}
</div>
<dl className="divide-y divide-border rounded-xl border border-border bg-surface text-sm">
{rows.map((row) => (
<div key={row.call} className="flex flex-wrap justify-between gap-x-4 gap-y-1 px-3 py-2">
<dt className="font-mono text-xs text-text-muted">{row.call}</dt>
<dd className="font-mono font-medium text-text tabular-nums">{row.value || "''"}</dd>
</div>
))}
</dl>
</div>
);
}
formatNumber
function formatNumber(n: number, options?: FormatNumberOptions): string;Writes a number with a thousands separator. The separator is a regular space (not an NBSP), so the
output is easy to copy and paste. NaN and Infinity return ''.
formatNumber(1250000); // '1 250 000'
formatNumber(1250000.756); // '1 250 000.756'
formatNumber(1250000.5, { decimals: 2 }); // '1 250 000.50'
formatNumber(1234567.891, { separator: ',', decimals: 2 }); // '1,234,567.89'
formatNumber(1234567.5, { decimalMark: ',', decimals: 1 }); // '1 234 567,5'
formatNumber(-4500); // '-4 500'
formatNumber(NaN); // ''| Prop | Type | Default | Description |
|---|---|---|---|
decimals | number | — | Digits after the decimal mark (0–20). If omitted, the number is kept as is |
separator | string | ' ' | Thousands separator |
decimalMark | string | '.' | Decimal mark |
formatMoney
function formatMoney(n: number, options?: FormatMoneyOptions): string;Formats an amount of money. By default it rounds to a whole number and appends so'm.
formatMoney(1250000); // "1 250 000 so'm"
formatMoney(1249999.6); // "1 250 000 so'm"
formatMoney(99.5, { currency: 'UZS', decimals: 2 }); // '99.50 UZS'
formatMoney(1250000, { currency: false }); // '1 250 000'
formatMoney(-99.5, { decimals: 2 }); // "-99.50 so'm"| Prop | Type | Default | Description |
|---|---|---|---|
currency | "so'm" | 'UZS' | false | "so'm" | Currency label; false hides it |
decimals | number | 0 | Digits after the decimal mark |
separator | string | ' ' | Thousands separator |
decimalMark | string | '.' | Decimal mark |
Need an input field?
Where users type an amount, use MoneyInput — it formats as
they type and gives you back a plain number.
formatFileSize
function formatFileSize(bytes: number, decimals?: number): string;Turns a byte count into something readable: B, KB, MB, GB, TB (1 KB = 1024 B). Trailing
zeros are dropped. Negative or invalid values return ''.
formatFileSize(512); // '512 B'
formatFileSize(1024); // '1 KB'
formatFileSize(1536); // '1.5 KB'
formatFileSize(3565158); // '3.4 MB'
formatFileSize(3565158, 0); // '3 MB'
formatFileSize(-1); // ''decimals is the number of digits after the decimal mark; the default is 1.
Phones and cards
Both functions format progressively — incomplete numbers too. That means you can call them
while the user types, right inside onChange:
formatPhone: +998 90 123
Toza qiymat: 86001234
'use client';
import { formatCardNumber, formatPhone } from '@akhatmirzo/core';
import { Field, Input } from '@akhatmirzo/ui';
import { useState } from 'react';
const digitsOf = (s: string) => s.replace(/\D/g, '');
export default function CorePhoneCard() {
const [phone, setPhone] = useState('90123');
const [card, setCard] = useState('8600 1234');
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Field label="Telefon" description={`formatPhone: ${formatPhone(phone) || '—'}`}>
<Input
prefix="+998"
value={formatPhone(phone, 'national')}
onChange={(e) => setPhone(digitsOf(e.target.value).slice(0, 9))}
inputMode="tel"
placeholder="(90) 123-45-67"
/>
</Field>
<Field label="Karta raqami" description={`Toza qiymat: ${digitsOf(card) || '—'}`}>
<Input
value={card}
onChange={(e) => setCard(formatCardNumber(digitsOf(e.target.value).slice(0, 16)))}
inputMode="numeric"
placeholder="0000 0000 0000 0000"
/>
</Field>
</div>
);
}
formatPhone
function formatPhone(input: string | number | null | undefined, style?: PhoneStyle): string;
// PhoneStyle = 'international' | 'national'Formats an Uzbek phone number. Spaces, dashes and parentheses are ignored.
formatPhone('901234567'); // '+998 90 123 45 67'
formatPhone(901234567); // '+998 90 123 45 67'
formatPhone('+998 (90) 123-45-67'); // '+998 90 123 45 67'
formatPhone('+998901234567', 'national'); // '(90) 123-45-67'
formatPhone('90123', 'national'); // '(90) 123'
formatPhone(''); // ''When the 998 prefix is stripped
998 is only treated as the country code when it comes with a + or when there are more than 9
digits. Otherwise it's the start of a national number: formatPhone('998123456') returns '+998 99 812 34 56' (operator 99).
formatCardNumber
function formatCardNumber(input: string | number | null | undefined): string;Groups a card number in blocks of four, up to 19 digits.
formatCardNumber('8600123412341234'); // '8600 1234 1234 1234'
formatCardNumber('8600-1234-12'); // '8600 1234 12'To check whether the number is valid, use
isValidCardNumber.
Dates and time
formatDate(sana, shablon) → 21 Oktabr 2026, Chorshanba
null — bunday sana yo'q
'use client';
import { formatDate, parseDate } from '@akhatmirzo/core';
import { Badge, Field, Input } from '@akhatmirzo/ui';
import { useState } from 'react';
const PATTERNS = ['dd.MM.yyyy', 'd MMMM yyyy, EEEE', 'd MMM, EEE', 'EEEE, d MMMM [soat] HH:mm'];
const DATE = new Date(2026, 9, 21, 14, 30);
export default function CoreDate() {
const [pattern, setPattern] = useState('d MMMM yyyy, EEEE');
const [input, setInput] = useState('31.02.2026');
const parsed = parseDate(input);
return (
<div className="flex w-full max-w-md flex-col gap-4">
<Field
label="Shablon"
description={`formatDate(sana, shablon) → ${formatDate(DATE, pattern)}`}
>
<Input value={pattern} onChange={(e) => setPattern(e.target.value)} />
</Field>
<div className="flex flex-wrap gap-2">
{PATTERNS.map((p) => (
<button key={p} type="button" onClick={() => setPattern(p)} className="cursor-pointer">
<Badge variant="outline">{p}</Badge>
</button>
))}
</div>
<Field
label="parseDate (dd.MM.yyyy)"
description={parsed ? formatDate(parsed, 'EEEE, d MMMM yyyy') : "null — bunday sana yo'q"}
>
<Input value={input} onChange={(e) => setInput(e.target.value)} invalid={!parsed} />
</Field>
</div>
);
}
formatDate
function formatDate(date: DateInput, pattern?: string): string;
// DateInput = Date | number | string | null | undefinedFormats a date with a pattern, in local time. Month and weekday names are in Uzbek. The default
pattern is 'dd.MM.yyyy'. An invalid date returns ''.
const date = new Date(2026, 9, 21, 14, 5);
formatDate(date); // '21.10.2026'
formatDate(date, 'd MMMM yyyy, EEEE'); // '21 Oktabr 2026, Chorshanba'
formatDate(date, 'd MMM, EEE'); // '21 Okt, Ch'
formatDate(date, 'yyyy-MM-dd HH:mm'); // '2026-10-21 14:05'
formatDate(date, 'EEEE, d MMMM [soat] HH:mm'); // 'Chorshanba, 21 Oktabr soat 14:05'
formatDate('sana emas'); // ''The table shows the output for new Date(2026, 2, 5, 9, 5, 7) — March 5, 2026, 09:05:07:
| Token | Output | Token | Output |
|---|---|---|---|
yyyy / yy | 2026 / 26 | HH / H | 09 / 9 (hours) |
MMMM | Mart | mm / m | 05 / 5 (minutes) |
MMM | Mar | ss / s | 07 / 7 (seconds) |
MM / M | 03 / 3 | EEEE | Payshanba |
dd / d | 05 / 5 | EEE | Pa |
Text in square brackets is left as is: '[soat] HH:mm' gives 'soat 14:05'.
parseDate
function parseDate(input: string | null | undefined, pattern?: string): Date | null;Parses text into a date using the same tokens, in local time. Returns null when the text
doesn't match the pattern or the date doesn't exist. Month names are case-insensitive, yy means
20yy, and surrounding spaces are ignored.
parseDate('21.10.2026'); // Date: Oct 21, 2026, 00:00
parseDate('31.02.2026'); // null — there is no February 31
parseDate('5 oktabr 2026', 'd MMMM yyyy'); // Date: Oct 5, 2026
parseDate('2026-10-21 14:30', 'yyyy-MM-dd HH:mm'); // Date: Oct 21, 2026, 14:30
parseDate('21.10.26', 'dd.MM.yy'); // Date: Oct 21, 2026timeAgo
function timeAgo(date: DateInput, now?: DateInput): string;Relative time in Uzbek: "… oldin" (ago) for the past, "…dan keyin" (in …) for the future. Less
than a minute is "hozirgina" (just now); up to a day, you get minutes or hours. Beyond a day, days
are counted by the calendar: 23:00 on Oct 19 seen from 08:00 on Oct 21 (33 hours) is
"2 kun oldin". now is the reference point and defaults to new Date().
timeAgo → 1 soat oldin
- 5 daqiqa5 daqiqa oldin
- 180 daqiqa3 soat oldin
- 1440 daqiqakecha
- 4320 daqiqa3 kun oldin
- 20160 daqiqa2 hafta oldin
- 136800 daqiqa3 oy oldin
- -120 daqiqa2 soatdan keyin
- -1440 daqiqaertaga
'use client';
import { timeAgo } from '@akhatmirzo/core';
import { Field, Input } from '@akhatmirzo/ui';
import { useState } from 'react';
const MINUTE = 60_000;
const NOW = new Date(2026, 9, 21, 12, 0);
const OFFSETS = [5, 180, 1440, 4320, 20160, 136800, -120, -1440];
export default function CoreTimeAgo() {
const [minutes, setMinutes] = useState('90');
const custom = new Date(NOW.getTime() - Number(minutes || 0) * MINUTE);
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<Field label="Necha daqiqa oldin" description={`timeAgo → ${timeAgo(custom, NOW)}`}>
<Input
value={minutes}
onChange={(e) => setMinutes(e.target.value.replace(/[^\d-]/g, ''))}
inputMode="numeric"
suffix="daqiqa"
/>
</Field>
<ul className="divide-y divide-border rounded-xl border border-border bg-surface text-sm">
{OFFSETS.map((m) => (
<li key={m} className="flex justify-between gap-4 px-3 py-2">
<span className="font-mono text-xs text-text-muted">{m} daqiqa</span>
<span className="text-text">{timeAgo(NOW.getTime() - m * MINUTE, NOW)}</span>
</li>
))}
</ul>
</div>
);
}
const now = new Date(2026, 9, 21, 12, 0);
const minutes = (m: number) => new Date(now.getTime() - m * 60_000);
timeAgo(minutes(0.5), now); // 'hozirgina'
timeAgo(minutes(5), now); // '5 daqiqa oldin'
timeAgo(minutes(180), now); // '3 soat oldin'
timeAgo(minutes(1440), now); // 'kecha'
timeAgo(minutes(4320), now); // '3 kun oldin'
timeAgo(minutes(20160), now); // '2 hafta oldin'
timeAgo(minutes(-120), now); // '2 soatdan keyin'
timeAgo(minutes(-1440), now); // 'ertaga'Units: minutes, hours, days, weeks, months and years.
Months and weekdays
const MONTHS_UZ: readonly ['Yanvar', 'Fevral', …, 'Dekabr'];
const MONTHS_UZ_SHORT: readonly ['Yan', 'Fev', 'Mar', 'Apr', 'May', 'Iyn', 'Iyl', …];
const WEEKDAYS_UZ: readonly ['Dushanba', 'Seshanba', …, 'Yakshanba'];
const WEEKDAYS_UZ_SHORT: readonly ['Du', 'Se', 'Ch', 'Pa', 'Ju', 'Sh', 'Ya'];
function weekdayIndex(date: Date): number; // 0 = Monday … 6 = SundayThe week starts on Monday. JavaScript's getDay() counts from Sunday, so get the index with
weekdayIndex:
WEEKDAYS_UZ[weekdayIndex(new Date(2026, 9, 21))]; // 'Chorshanba'
weekdayIndex(new Date(2026, 9, 25)); // 6 — SundayThe short names for June and July are Iyn and Iyl, so they don't collide.
Numbers and money in words
For invoices, contracts and receipts — spell out a number or an amount in Uzbek.
- numberToWords
- bir million ikki yuz ellik ming
- moneyToWords
- Bir million ikki yuz ellik ming soʻm 50 tiyin
- ordinalWords
- bir million ikki yuz ellik minginchi
'use client';
import { moneyToWords, numberToWords, ordinalWords } from '@akhatmirzo/core';
import { Field, Input, SegmentedControl } from '@akhatmirzo/ui';
import { useState } from 'react';
type Script = 'latin' | 'cyrillic';
export default function CoreNumberWords() {
const [raw, setRaw] = useState('1250000.50');
const [script, setScript] = useState<Script>('latin');
const n = Number(raw.replace(',', '.').replace(/\s/g, ''));
return (
<div className="flex w-full max-w-md flex-col gap-4">
<Field label="Summa">
<Input value={raw} onChange={(e) => setRaw(e.target.value)} inputMode="decimal" />
</Field>
<SegmentedControl
aria-label="Alifbo"
value={script}
onValueChange={(v) => setScript(v as Script)}
>
<SegmentedControl.Item value="latin">Lotin</SegmentedControl.Item>
<SegmentedControl.Item value="cyrillic">Кирилл</SegmentedControl.Item>
</SegmentedControl>
<dl className="grid gap-2 text-sm">
<dt className="font-mono text-xs text-text-muted">numberToWords</dt>
<dd>{numberToWords(n, { script }) || '—'}</dd>
<dt className="font-mono text-xs text-text-muted">moneyToWords</dt>
<dd>{moneyToWords(n, { script }) || '—'}</dd>
<dt className="font-mono text-xs text-text-muted">ordinalWords</dt>
<dd>{ordinalWords(n, { script }) || '—'}</dd>
</dl>
</div>
);
}
Script and apostrophe
Every new function that returns text accepts script: 'latin' | 'cyrillic' (default 'latin';
Cyrillic goes through latToCyr). Latin output uses the typographic oʻ gʻ
(U+02BB); pass apostrophe: 'ascii' for a plain '.
numberToWords
function numberToWords(n: number, options?: NumberToWordsOptions): string;Integers up to ±999 999 999 999 999 (trillions). The fractional part is dropped, negatives read
"minus …". NaN, Infinity and out-of-range numbers → ''.
numberToWords(0); // 'nol'
numberToWords(21); // 'yigirma bir'
numberToWords(101); // 'yuz bir'
numberToWords(1001); // 'ming bir'
numberToWords(1_250_000); // 'bir million ikki yuz ellik ming'
numberToWords(2_300_000_000); // 'ikki milliard uch yuz million'
numberToWords(-15); // 'minus oʻn besh'
numberToWords(3.99); // 'uch'
numberToWords(1100, { formal: true }); // 'bir ming bir yuz'
numberToWords(14, { apostrophe: 'ascii' }); // "o'n to'rt"
numberToWords(1_250_000, { script: 'cyrillic' }); // 'бир миллион икки юз эллик минг'By default 100 is "yuz" and 1000 is "ming" (only when the number starts with the thousands:
1 001 000 → "bir million bir ming"). formal: true is the official-document style:
"bir yuz", "bir ming".
| Prop | Type | Default | Description |
|---|---|---|---|
formal | boolean | false | "bir yuz", "bir ming" (official style) |
script | 'latin' | 'cyrillic' | 'latin' | Script |
apostrophe | 'typographic' | 'ascii' | 'typographic' | Character in oʻ gʻ: ʻ or ' |
moneyToWords
function moneyToWords(amount: number, options?: MoneyToWordsOptions): string;Spells out an amount: capitalized, tiyin as digits (invoice style). The fraction is rounded to 2
digits; 0 tiyin is omitted. Also accepts the numberToWords options.
moneyToWords(1_250_000.5); // 'Bir million ikki yuz ellik ming soʻm 50 tiyin'
moneyToWords(1_250_000); // 'Bir million ikki yuz ellik ming soʻm'
moneyToWords(10.25, { tiyin: 'words' }); // 'Oʻn soʻm yigirma besh tiyin'
moneyToWords(10.25, { tiyin: false }); // 'Oʻn soʻm'
moneyToWords(2500, { capitalize: false }); // 'ikki ming besh yuz soʻm'
moneyToWords(2500, { currency: 'UZS' }); // 'Ikki ming besh yuz UZS'
moneyToWords(100_000, { formal: true }); // 'Bir yuz ming soʻm'
moneyToWords(1000, { script: 'cyrillic' }); // 'Минг сўм'| Prop | Type | Default | Description |
|---|---|---|---|
currency | "so'm" | 'UZS' | string | "so'm" | "so'm" — as a word (soʻm / сўм); any other string is appended as is |
tiyin | 'digits' | 'words' | false | 'digits' | "50 tiyin", "ellik tiyin" or omitted |
capitalize | boolean | true | Capitalize first letter |
formal | boolean | false | "bir yuz", "bir ming" |
script | 'latin' | 'cyrillic' | 'latin' | Script |
apostrophe | 'typographic' | 'ascii' | 'typographic' | Character in oʻ gʻ: ʻ or ' |
ordinal and ordinalWords
function ordinal(n: number): string;
function ordinalWords(n: number, options?: UzScriptOptions): string;Ordinal numbers. ordinal — digits plus a hyphen (22-oktabr, 3-sinf). ordinalWords — in
words: -nchi after a final vowel, -inchi otherwise; compound numbers take the suffix on the last
word. Negative or invalid numbers → ''.
ordinal(3); // '3-'
ordinalWords(1); // 'birinchi'
ordinalWords(2); // 'ikkinchi'
ordinalWords(4); // 'toʻrtinchi'
ordinalWords(20); // 'yigirmanchi'
ordinalWords(21); // 'yigirma birinchi'
ordinalWords(1000); // 'minginchi'
ordinalWords(2, { script: 'cyrillic' }); // 'иккинчи'Uzbek dates and durations
Following Uzbek orthography: month names in lowercase, day and year joined with a hyphen —
22-oktabr, 2026-yil. The existing formatDate and MONTHS_UZ are unchanged (capitalized names
are kept).
- long
- 22-oktabr, 2026-yil
- full
- 2026-yil 22-oktabr, payshanba
- short
- 22-okt, 2026
- dayMonth
- 22-oktabr
- monthYear
- oktabr, 2026-yil
- kirill
- 2026-йил 22-октябрь, пайшанба
- +3 kun
- 22–25-oktabr, 2026-yil
- davomiylik
- 2 kun 9 soat 30 daqiqa
'use client';
import { formatDateRange, formatDateUz, formatDuration } from '@akhatmirzo/core';
import { Field, Input } from '@akhatmirzo/ui';
import { useState } from 'react';
const STYLES = ['long', 'full', 'short', 'dayMonth', 'monthYear'] as const;
export default function CoreDateUz() {
const [value, setValue] = useState('2026-10-22');
const [y = 2026, m = 10, d = 22] = value.split('-').map(Number);
const date = new Date(y, m - 1, d, 14, 30);
const end = new Date(y, m - 1, d + 3);
return (
<div className="flex w-full max-w-md flex-col gap-4">
<Field label="Sana">
<Input type="date" value={value} onChange={(e) => setValue(e.target.value)} />
</Field>
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm">
{STYLES.map((style) => (
<div key={style} className="contents">
<dt className="font-mono text-xs text-text-muted">{style}</dt>
<dd>{formatDateUz(date, { style })}</dd>
</div>
))}
<dt className="font-mono text-xs text-text-muted">kirill</dt>
<dd>{formatDateUz(date, { style: 'full', script: 'cyrillic' })}</dd>
<dt className="font-mono text-xs text-text-muted">+3 kun</dt>
<dd>{formatDateRange(date, end)}</dd>
<dt className="font-mono text-xs text-text-muted">davomiylik</dt>
<dd>{formatDuration(end.getTime() - date.getTime(), { units: 3 })}</dd>
</dl>
</div>
);
}
formatDateUz
function formatDateUz(date: DateInput, options?: FormatDateUzOptions): string;Ready-made styles — no pattern to write. Invalid date → ''.
const d = new Date(2026, 9, 22, 14, 30);
formatDateUz(d); // '22-oktabr, 2026-yil'
formatDateUz(d, { style: 'full' }); // '2026-yil 22-oktabr, payshanba'
formatDateUz(d, { style: 'short' }); // '22-okt, 2026'
formatDateUz(d, { style: 'dayMonth' }); // '22-oktabr'
formatDateUz(d, { style: 'monthYear' }); // 'oktabr, 2026-yil'
formatDateUz(d, { time: true }); // '22-oktabr, 2026-yil, 14:30'
formatDateUz(d, { script: 'cyrillic' }); // '22-октябрь, 2026-йил'Cyrillic month names use their standard spelling: январь … июнь, июль … декабрь.
| Prop | Type | Default | Description |
|---|---|---|---|
style | 'long' | 'full' | 'short' | 'dayMonth' | 'monthYear' | 'long' | Output shape |
time | boolean | false | Append ", HH:mm" |
script | 'latin' | 'cyrillic' | 'latin' | Script |
formatDateRange
function formatDateRange(
start: DateInput,
end: DateInput,
options?: FormatDateRangeOptions,
): string;A date range without repetition. The same day gives a single date; if start > end they are
swapped. Options: style: 'long' | 'short', script.
formatDateRange(new Date(2026, 9, 20), new Date(2026, 9, 22)); // '20–22-oktabr, 2026-yil'
formatDateRange(new Date(2026, 8, 30), new Date(2026, 9, 2)); // '30-sentabr – 2-oktabr, 2026-yil'
formatDateRange(new Date(2026, 11, 30), new Date(2027, 0, 2));
// '30-dekabr, 2026-yil – 2-yanvar, 2027-yil'
formatDateRange(new Date(2026, 9, 20), new Date(2026, 9, 22), { style: 'short' });
// '20–22-okt, 2026'formatDuration
function formatDuration(ms: number, options?: FormatDurationOptions): string;A duration in milliseconds. Units: kun (day), soat (hour), daqiqa (minute), soniya (second). Takes
units consecutive units starting from the largest non-zero one, skips zero values and truncates
the remainder (no rounding). < 1 s → '0 soniya'; negative or invalid → ''.
formatDuration(8_100_000); // '2 soat 15 daqiqa'
formatDuration(90_061_000); // '1 kun 1 soat'
formatDuration(90_061_000, { units: 3 }); // '1 kun 1 soat 1 daqiqa'
formatDuration(45_000, { style: 'short' }); // '45 s'
formatDuration(8_100_000, { style: 'short' }); // '2 soat 15 daq'
formatDuration(500); // '0 soniya'
formatDuration(8_100_000, { script: 'cyrillic' }); // '2 соат 15 дақиқа'| Prop | Type | Default | Description |
|---|---|---|---|
units | number | 2 | How many units (1–4) |
style | 'long' | 'short' | 'long' | short: kun, soat, daq, s |
script | 'latin' | 'cyrillic' | 'latin' | Script |