UNPKG

arabicfmt

Version:

Arabic-first formatting for numbers, currency, dates and bidirectional text across all 22 Arab League countries — with correct handling of the 2025–2026 Unicode currency-symbol transition (Saudi Riyal U+20C1, UAE Dirham U+20C3, Omani Rial U+20C4).

312 lines (303 loc) 12.5 kB
import { arabicToWords, countedNoun } from './chunk-EVN4RFPU.js'; import { stripBidi } from './chunk-WAZEILBO.js'; import { DEFAULT_LOCALE, withNumberingSystem } from './chunk-5IFTL7BQ.js'; import { toLatinDigits, toArabicDigits } from './chunk-OA4S5ZUV.js'; // src/number/format.ts function formatNumber(value, options = {}) { const locale = options.locale ?? DEFAULT_LOCALE; const numerals = options.numerals ?? "latn"; const intlOptions = { style: options.style ?? "decimal", useGrouping: options.grouping ?? true }; if (options.fractionDigits != null) { intlOptions.minimumFractionDigits = options.fractionDigits; intlOptions.maximumFractionDigits = options.fractionDigits; } if (options.minimumFractionDigits != null) { intlOptions.minimumFractionDigits = options.minimumFractionDigits; } if (options.maximumFractionDigits != null) { intlOptions.maximumFractionDigits = options.maximumFractionDigits; } if (options.signDisplay) intlOptions.signDisplay = options.signDisplay; if (options.notation) { intlOptions.notation = options.notation; if (options.notation === "compact") { intlOptions.compactDisplay = options.compactDisplay ?? "short"; } } return new Intl.NumberFormat( withNumberingSystem(locale, numerals), intlOptions ).format(value); } function formatPercent(value, options = {}) { return formatNumber(value, { ...options, style: "percent" }); } function formatCompact(value, options = {}) { return formatNumber(value, { ...options, notation: "compact", compactDisplay: options.compactDisplay ?? "short" }); } // src/number/parse.ts var cc = String.fromCharCode; var ARABIC_THOUSANDS = cc(1644); var ARABIC_DECIMAL = cc(1643); function parseNumber(input) { const clean = toLatinDigits(stripBidi(input)).replace(new RegExp(`[,${ARABIC_THOUSANDS}]`, "g"), "").replace(new RegExp(`[${ARABIC_DECIMAL}]`, "g"), ".").trim(); return parseFloat(clean); } function parseCurrency(input) { const isNegative = /^\(.*\)$/.test(input.trim()); const normalized = toLatinDigits(stripBidi(input)).replace(/^\(|\)$/g, "").replace(new RegExp(`[,${ARABIC_THOUSANDS}]`, "g"), "").replace(new RegExp(`[${ARABIC_DECIMAL}]`, "g"), ".").replace(/[^\d.+\-]/g, " ").trim(); const value = parseFloat(normalized); return isNegative ? -value : value; } // src/number/fraction.ts var DENOMINATORS = { 2: { singular: "\u0646\u0635\u0641", dual: "\u0646\u0635\u0641\u0627\u0646", plural: "\u0623\u0646\u0635\u0627\u0641" }, 3: { singular: "\u062B\u0644\u062B", dual: "\u062B\u0644\u062B\u0627\u0646", plural: "\u0623\u062B\u0644\u0627\u062B" }, 4: { singular: "\u0631\u0628\u0639", dual: "\u0631\u0628\u0639\u0627\u0646", plural: "\u0623\u0631\u0628\u0627\u0639" }, 5: { singular: "\u062E\u0645\u0633", dual: "\u062E\u0645\u0633\u0627\u0646", plural: "\u0623\u062E\u0645\u0627\u0633" }, 6: { singular: "\u0633\u062F\u0633", dual: "\u0633\u062F\u0633\u0627\u0646", plural: "\u0623\u0633\u062F\u0627\u0633" }, 7: { singular: "\u0633\u0628\u0639", dual: "\u0633\u0628\u0639\u0627\u0646", plural: "\u0623\u0633\u0628\u0627\u0639" }, 8: { singular: "\u062B\u0645\u0646", dual: "\u062B\u0645\u0646\u0627\u0646", plural: "\u0623\u062B\u0645\u0627\u0646" }, 9: { singular: "\u062A\u0633\u0639", dual: "\u062A\u0633\u0639\u0627\u0646", plural: "\u0623\u062A\u0633\u0627\u0639" }, 10: { singular: "\u0639\u0634\u0631", dual: "\u0639\u0634\u0631\u0627\u0646", plural: "\u0623\u0639\u0634\u0627\u0631" } }; function arabicFraction(numerator, denominator) { const num = Math.trunc(numerator); const forms = DENOMINATORS[Math.trunc(denominator)]; if (!forms || num < 1) return ""; if (num === 1) return forms.singular; if (num === 2) return forms.dual; return `${arabicToWords(num, { gender: "male" })} ${forms.plural}`; } // src/number/ordinal.ts var ORD_M = [ "", "\u0627\u0644\u0623\u0648\u0644", "\u0627\u0644\u062B\u0627\u0646\u064A", "\u0627\u0644\u062B\u0627\u0644\u062B", "\u0627\u0644\u0631\u0627\u0628\u0639", "\u0627\u0644\u062E\u0627\u0645\u0633", "\u0627\u0644\u0633\u0627\u062F\u0633", "\u0627\u0644\u0633\u0627\u0628\u0639", "\u0627\u0644\u062B\u0627\u0645\u0646", "\u0627\u0644\u062A\u0627\u0633\u0639", "\u0627\u0644\u0639\u0627\u0634\u0631", "\u0627\u0644\u062D\u0627\u062F\u064A \u0639\u0634\u0631", "\u0627\u0644\u062B\u0627\u0646\u064A \u0639\u0634\u0631", "\u0627\u0644\u062B\u0627\u0644\u062B \u0639\u0634\u0631", "\u0627\u0644\u0631\u0627\u0628\u0639 \u0639\u0634\u0631", "\u0627\u0644\u062E\u0627\u0645\u0633 \u0639\u0634\u0631", "\u0627\u0644\u0633\u0627\u062F\u0633 \u0639\u0634\u0631", "\u0627\u0644\u0633\u0627\u0628\u0639 \u0639\u0634\u0631", "\u0627\u0644\u062B\u0627\u0645\u0646 \u0639\u0634\u0631", "\u0627\u0644\u062A\u0627\u0633\u0639 \u0639\u0634\u0631" ]; var ORD_F = [ "", "\u0627\u0644\u0623\u0648\u0644\u0649", "\u0627\u0644\u062B\u0627\u0646\u064A\u0629", "\u0627\u0644\u062B\u0627\u0644\u062B\u0629", "\u0627\u0644\u0631\u0627\u0628\u0639\u0629", "\u0627\u0644\u062E\u0627\u0645\u0633\u0629", "\u0627\u0644\u0633\u0627\u062F\u0633\u0629", "\u0627\u0644\u0633\u0627\u0628\u0639\u0629", "\u0627\u0644\u062B\u0627\u0645\u0646\u0629", "\u0627\u0644\u062A\u0627\u0633\u0639\u0629", "\u0627\u0644\u0639\u0627\u0634\u0631\u0629", "\u0627\u0644\u062D\u0627\u062F\u064A\u0629 \u0639\u0634\u0631\u0629", "\u0627\u0644\u062B\u0627\u0646\u064A\u0629 \u0639\u0634\u0631\u0629", "\u0627\u0644\u062B\u0627\u0644\u062B\u0629 \u0639\u0634\u0631\u0629", "\u0627\u0644\u0631\u0627\u0628\u0639\u0629 \u0639\u0634\u0631\u0629", "\u0627\u0644\u062E\u0627\u0645\u0633\u0629 \u0639\u0634\u0631\u0629", "\u0627\u0644\u0633\u0627\u062F\u0633\u0629 \u0639\u0634\u0631\u0629", "\u0627\u0644\u0633\u0627\u0628\u0639\u0629 \u0639\u0634\u0631\u0629", "\u0627\u0644\u062B\u0627\u0645\u0646\u0629 \u0639\u0634\u0631\u0629", "\u0627\u0644\u062A\u0627\u0633\u0639\u0629 \u0639\u0634\u0631\u0629" ]; var COMPOUND_M = [ "", "\u0627\u0644\u062D\u0627\u062F\u064A", "\u0627\u0644\u062B\u0627\u0646\u064A", "\u0627\u0644\u062B\u0627\u0644\u062B", "\u0627\u0644\u0631\u0627\u0628\u0639", "\u0627\u0644\u062E\u0627\u0645\u0633", "\u0627\u0644\u0633\u0627\u062F\u0633", "\u0627\u0644\u0633\u0627\u0628\u0639", "\u0627\u0644\u062B\u0627\u0645\u0646", "\u0627\u0644\u062A\u0627\u0633\u0639" ]; var COMPOUND_F = [ "", "\u0627\u0644\u062D\u0627\u062F\u064A\u0629", "\u0627\u0644\u062B\u0627\u0646\u064A\u0629", "\u0627\u0644\u062B\u0627\u0644\u062B\u0629", "\u0627\u0644\u0631\u0627\u0628\u0639\u0629", "\u0627\u0644\u062E\u0627\u0645\u0633\u0629", "\u0627\u0644\u0633\u0627\u062F\u0633\u0629", "\u0627\u0644\u0633\u0627\u0628\u0639\u0629", "\u0627\u0644\u062B\u0627\u0645\u0646\u0629", "\u0627\u0644\u062A\u0627\u0633\u0639\u0629" ]; var TENS_ORD = [ "", "", "\u0627\u0644\u0639\u0634\u0631\u0648\u0646", "\u0627\u0644\u062B\u0644\u0627\u062B\u0648\u0646", "\u0627\u0644\u0623\u0631\u0628\u0639\u0648\u0646", "\u0627\u0644\u062E\u0645\u0633\u0648\u0646", "\u0627\u0644\u0633\u062A\u0648\u0646", "\u0627\u0644\u0633\u0628\u0639\u0648\u0646", "\u0627\u0644\u062B\u0645\u0627\u0646\u0648\u0646", "\u0627\u0644\u062A\u0633\u0639\u0648\u0646" ]; function indefinite(s) { return s.replace(/(^|و)ال/g, "$1"); } function arabicOrdinal(n, options = {}) { if (!Number.isFinite(n)) return ""; const gender = options.gender ?? "male"; const definite = options.definite ?? true; const value = Math.trunc(n); if (value < 1) return ""; let base; if (value <= 19) { base = (gender === "male" ? ORD_M : ORD_F)[value] ?? ""; } else if (value <= 99) { const tens = Math.floor(value / 10); const unit = value % 10; const tensWord = TENS_ORD[tens] ?? ""; if (unit === 0) { base = tensWord; } else { const unitWord = (gender === "male" ? COMPOUND_M : COMPOUND_F)[unit] ?? ""; base = `${unitWord} \u0648${tensWord}`; } } else { base = `\u0627\u0644${arabicToWords(value, { gender })}`; } return definite ? base : indefinite(base); } // src/number/duration.ts var SECOND = { gender: "female", singular: "\u062B\u0627\u0646\u064A\u0629", dual: "\u062B\u0627\u0646\u064A\u062A\u0627\u0646", plural: "\u062B\u0648\u0627\u0646\u064D", accusative: "\u062B\u0627\u0646\u064A\u0629\u064B" }; var MINUTE = { gender: "female", singular: "\u062F\u0642\u064A\u0642\u0629", dual: "\u062F\u0642\u064A\u0642\u062A\u0627\u0646", plural: "\u062F\u0642\u0627\u0626\u0642", accusative: "\u062F\u0642\u064A\u0642\u0629\u064B" }; var HOUR = { gender: "female", singular: "\u0633\u0627\u0639\u0629", dual: "\u0633\u0627\u0639\u062A\u0627\u0646", plural: "\u0633\u0627\u0639\u0627\u062A", accusative: "\u0633\u0627\u0639\u0629\u064B" }; var DAY = { gender: "male", singular: "\u064A\u0648\u0645", dual: "\u064A\u0648\u0645\u0627\u0646", plural: "\u0623\u064A\u0627\u0645", accusative: "\u064A\u0648\u0645\u0627\u064B" }; var UNIT_SECONDS = { day: 86400, hour: 3600, minute: 60, second: 1 }; var UNIT_NOUN = { day: DAY, hour: HOUR, minute: MINUTE, second: SECOND }; var ORDER = ["day", "hour", "minute", "second"]; function formatDuration(value, options = {}) { if (!Number.isFinite(value)) return ""; const seconds = Math.floor( Math.abs(value) / (options.input === "s" ? 1 : 1e3) ); const largest = options.largest ?? 2; const allowed = options.units ?? ORDER; const units = ORDER.filter((u) => allowed.includes(u)); if (seconds === 0) return "\u0623\u0642\u0644 \u0645\u0646 \u062B\u0627\u0646\u064A\u0629"; const parts = []; let remaining = seconds; for (const unit of units) { const size = UNIT_SECONDS[unit]; const count = Math.floor(remaining / size); if (count > 0) { parts.push(countedNoun(count, UNIT_NOUN[unit])); remaining -= count * size; } if (parts.length >= largest) break; } return parts.join(" \u0648"); } // src/number/filesize.ts var UNITS_AR = [ "\u0628\u0627\u064A\u062A", "\u0643\u064A\u0644\u0648\u0628\u0627\u064A\u062A", "\u0645\u064A\u062C\u0627\u0628\u0627\u064A\u062A", "\u062C\u064A\u062C\u0627\u0628\u0627\u064A\u062A", "\u062A\u064A\u0631\u0627\u0628\u0627\u064A\u062A", "\u0628\u064A\u062A\u0627\u0628\u0627\u064A\u062A", "\u0625\u0643\u0633\u0627\u0628\u0627\u064A\u062A" ]; var UNITS_LATIN = ["B", "KB", "MB", "GB", "TB", "PB", "EB"]; function formatFileSize(bytes, options = {}) { if (!Number.isFinite(bytes)) return ""; const locale = options.locale ?? DEFAULT_LOCALE; const numerals = options.numerals ?? "latn"; const base = options.base ?? 1024; const precision = options.precision ?? 1; const units = options.unitStyle === "latin" ? UNITS_LATIN : UNITS_AR; const negative = bytes < 0; const abs = Math.abs(bytes); let exponent = abs < 1 ? 0 : Math.floor(Math.log(abs) / Math.log(base)); exponent = Math.min(exponent, units.length - 1); const value = abs / base ** exponent; const maximumFractionDigits = exponent === 0 ? 0 : precision; const number = formatNumber(value, { locale, numerals, maximumFractionDigits }); const unit = units[exponent] ?? units[units.length - 1]; return `${negative ? "-" : ""}${number} ${unit}`; } // src/number/relative.ts var UNITS = [ "year", "month", "week", "day", "hour", "minute", "second" ]; var THRESHOLDS = { year: 365 * 24 * 3600, month: 30 * 24 * 3600, week: 7 * 24 * 3600, day: 24 * 3600, hour: 3600, minute: 60, second: 1 }; function formatRelativeTime(date, base = /* @__PURE__ */ new Date(), options = {}) { const locale = options.locale ?? DEFAULT_LOCALE; const numeric = options.numeric ?? "auto"; const style = options.style ?? "long"; const diffSeconds = (date.getTime() - base.getTime()) / 1e3; const absDiff = Math.abs(diffSeconds); let unit = "second"; let value = Math.round(diffSeconds); for (const u of UNITS) { const threshold = THRESHOLDS[u] ?? 1; if (absDiff >= threshold) { unit = u; value = Math.round(diffSeconds / threshold); break; } } const formatted = new Intl.RelativeTimeFormat(locale, { numeric, style }).format(value, unit); if (options.numerals === "arab") { return toArabicDigits(formatted); } return formatted; } export { arabicFraction, arabicOrdinal, formatCompact, formatDuration, formatFileSize, formatNumber, formatPercent, formatRelativeTime, parseCurrency, parseNumber };