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
JavaScript
// 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 };