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).

171 lines (168 loc) 6.41 kB
// src/number/words.ts var ONES_M = [ "", "\u0648\u0627\u062D\u062F", "\u0627\u062B\u0646\u0627\u0646", "\u062B\u0644\u0627\u062B\u0629", "\u0623\u0631\u0628\u0639\u0629", "\u062E\u0645\u0633\u0629", "\u0633\u062A\u0629", "\u0633\u0628\u0639\u0629", "\u062B\u0645\u0627\u0646\u064A\u0629", "\u062A\u0633\u0639\u0629" ]; var ONES_F = [ "", "\u0648\u0627\u062D\u062F\u0629", "\u0627\u062B\u0646\u062A\u0627\u0646", "\u062B\u0644\u0627\u062B", "\u0623\u0631\u0628\u0639", "\u062E\u0645\u0633", "\u0633\u062A", "\u0633\u0628\u0639", "\u062B\u0645\u0627\u0646\u064A", "\u062A\u0633\u0639" ]; var TEENS_M = [ "\u0639\u0634\u0631\u0629", "\u0623\u062D\u062F \u0639\u0634\u0631", "\u0627\u062B\u0646\u0627 \u0639\u0634\u0631", "\u062B\u0644\u0627\u062B\u0629 \u0639\u0634\u0631", "\u0623\u0631\u0628\u0639\u0629 \u0639\u0634\u0631", "\u062E\u0645\u0633\u0629 \u0639\u0634\u0631", "\u0633\u062A\u0629 \u0639\u0634\u0631", "\u0633\u0628\u0639\u0629 \u0639\u0634\u0631", "\u062B\u0645\u0627\u0646\u064A\u0629 \u0639\u0634\u0631", "\u062A\u0633\u0639\u0629 \u0639\u0634\u0631" ]; var TEENS_F = [ "\u0639\u0634\u0631", "\u0625\u062D\u062F\u0649 \u0639\u0634\u0631\u0629", "\u0627\u062B\u0646\u062A\u0627 \u0639\u0634\u0631\u0629", "\u062B\u0644\u0627\u062B \u0639\u0634\u0631\u0629", "\u0623\u0631\u0628\u0639 \u0639\u0634\u0631\u0629", "\u062E\u0645\u0633 \u0639\u0634\u0631\u0629", "\u0633\u062A \u0639\u0634\u0631\u0629", "\u0633\u0628\u0639 \u0639\u0634\u0631\u0629", "\u062B\u0645\u0627\u0646\u064A \u0639\u0634\u0631\u0629", "\u062A\u0633\u0639 \u0639\u0634\u0631\u0629" ]; var TENS = [ "", "\u0639\u0634\u0631\u0629", "\u0639\u0634\u0631\u0648\u0646", "\u062B\u0644\u0627\u062B\u0648\u0646", "\u0623\u0631\u0628\u0639\u0648\u0646", "\u062E\u0645\u0633\u0648\u0646", "\u0633\u062A\u0648\u0646", "\u0633\u0628\u0639\u0648\u0646", "\u062B\u0645\u0627\u0646\u0648\u0646", "\u062A\u0633\u0639\u0648\u0646" ]; var HUNDREDS = [ "", "\u0645\u0626\u0629", "\u0645\u0626\u062A\u0627\u0646", "\u062B\u0644\u0627\u062B\u0645\u0626\u0629", "\u0623\u0631\u0628\u0639\u0645\u0626\u0629", "\u062E\u0645\u0633\u0645\u0626\u0629", "\u0633\u062A\u0645\u0626\u0629", "\u0633\u0628\u0639\u0645\u0626\u0629", "\u062B\u0645\u0627\u0646\u0645\u0626\u0629", "\u062A\u0633\u0639\u0645\u0626\u0629" ]; var SCALES = [ { one: "", two: "", plural: "", tanwin: "" }, // placeholder for index 0 { one: "\u0623\u0644\u0641", two: "\u0623\u0644\u0641\u0627\u0646", plural: "\u0622\u0644\u0627\u0641", tanwin: "\u0623\u0644\u0641\u0627\u064B" }, { one: "\u0645\u0644\u064A\u0648\u0646", two: "\u0645\u0644\u064A\u0648\u0646\u0627\u0646", plural: "\u0645\u0644\u0627\u064A\u064A\u0646", tanwin: "\u0645\u0644\u064A\u0648\u0646\u0627\u064B" }, { one: "\u0645\u0644\u064A\u0627\u0631", two: "\u0645\u0644\u064A\u0627\u0631\u0627\u0646", plural: "\u0645\u0644\u064A\u0627\u0631\u0627\u062A", tanwin: "\u0645\u0644\u064A\u0627\u0631\u0627\u064B" }, { one: "\u062A\u0631\u064A\u0644\u064A\u0648\u0646", two: "\u062A\u0631\u064A\u0644\u064A\u0648\u0646\u0627\u0646", plural: "\u062A\u0631\u064A\u0644\u064A\u0648\u0646\u0627\u062A", tanwin: "\u062A\u0631\u064A\u0644\u064A\u0648\u0646\u0627\u064B" } ]; function under1000(n, gender) { if (n <= 0) return ""; const h = Math.floor(n / 100); const rem = n % 100; const parts = []; if (h > 0) parts.push(HUNDREDS[h]); if (rem === 0) ; else if (rem < 10) { parts.push((gender === "male" ? ONES_M : ONES_F)[rem]); } else if (rem < 20) { parts.push((gender === "male" ? TEENS_M : TEENS_F)[rem - 10]); } else { const t = Math.floor(rem / 10); const u = rem % 10; if (u === 0) { parts.push(TENS[t]); } else { const unit = gender === "female" && u === 1 ? "\u0625\u062D\u062F\u0649" : (gender === "male" ? ONES_M : ONES_F)[u]; parts.push(`${unit} \u0648${TENS[t]}`); } } return parts.join(" \u0648"); } function scaleGroup(count, scale) { if (count === 1) return scale.one; if (count === 2) return scale.two; const countWord = under1000(count, "male"); const mod100 = count % 100; if (mod100 >= 3 && mod100 <= 10) return `${countWord} ${scale.plural}`; if (mod100 >= 11 && mod100 <= 99) return `${countWord} ${scale.tanwin}`; return `${countWord} ${scale.one}`; } function digitToWord(d) { return d === 0 ? "\u0635\u0641\u0631" : ONES_M[d]; } function fractionDigits(n) { const s = Math.abs(n).toString(); if (s.includes("e") || s.includes("E")) return ""; const dot = s.indexOf("."); return dot === -1 ? "" : s.slice(dot + 1); } function integerToWords(abs, gender) { if (abs === 0) return "\u0635\u0641\u0631"; const groups = []; let remaining = abs; for (let scale = SCALES.length - 1; scale >= 0; scale--) { const divisor = Math.pow(1e3, scale); const count = Math.floor(remaining / divisor); if (count > 0) { groups.push({ count, scale }); remaining %= divisor; } } const parts = groups.map( ({ count, scale }) => scale === 0 ? under1000(count, gender) : scaleGroup(count, SCALES[scale]) ); return parts.join(" \u0648"); } function arabicToWords(n, options = {}) { if (!Number.isFinite(n)) return ""; const gender = options.gender ?? "male"; const addNegative = options.negative ?? true; const fractionMode = options.fraction ?? "off"; let result = integerToWords(Math.abs(Math.trunc(n)), gender); if (fractionMode !== "off") { const frac = fractionDigits(n); if (frac) { const fracWords = fractionMode === "digits" ? frac.split("").map((c) => digitToWord(Number(c))).join(" ") : integerToWords(Number(frac), gender); result = `${result} \u0641\u0627\u0635\u0644\u0629 ${fracWords}`; } } return n < 0 && addNegative ? `\u0633\u0627\u0644\u0628 ${result}` : result; } // src/number/count.ts function countedNoun(n, noun) { if (n === 1) { return `${noun.singular} ${noun.gender === "female" ? "\u0648\u0627\u062D\u062F\u0629" : "\u0648\u0627\u062D\u062F"}`; } if (n === 2) return noun.dual; const words = arabicToWords(n, { gender: noun.gender }); const mod = n % 100; let unit; if (mod >= 3 && mod <= 10) unit = noun.plural; else if (mod >= 11 && mod <= 99) unit = noun.accusative; else unit = noun.singular; return `${words} ${unit}`; } export { arabicToWords, countedNoun };