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