function unicodeCharToAsciiDigit(ch: string): string { if (ch >= "0" && ch <= "9") return ch; const cp = ch.codePointAt(0)!; if (cp >= 0x06f0 && cp <= 0x06f9) return String(cp - 0x06f0); if (cp >= 0x0660 && cp <= 0x0669) return String(cp - 0x0660); if (cp >= 0x0966 && cp <= 0x096f) return String(cp - 0x0966); if (cp >= 0xff10 && cp <= 0xff19) return String(cp - 0xff10); const folded = ch.normalize("NFKC"); if (folded.length === 1 && folded >= "0" && folded <= "9") return folded; return ch; } /** * Convert decimal digits in any Unicode script (Persian, Arabic-Indic, etc.) * to ASCII `0-9`. Other characters are left unchanged. */ export function normalizeUnicodeDigitsToEnglish(input: string): string { return Array.from(input).map(unicodeCharToAsciiDigit).join(""); } /** Strip grouping separators after digit normalization (for money fields). */ export function normalizeMoneyAmountString(input: string): string { return normalizeUnicodeDigitsToEnglish(input) .trim() .replace(/[,،\u066C\u060C]/g, ""); } /** Parse a money string to a whole **Toman** amount, or `null` if invalid. */ export function parseMoneyAmountToman(value: unknown): number | null { if (value == null || value === "") return null; const n = normalizeMoneyAmountString(String(value)); if (!/^\d+$/.test(n)) return null; const num = Number(n); if (!Number.isFinite(num)) return null; return num; } export function normalizeUnicodeDigitsDeep(value: T): T { if (value == null) return value; if (typeof value === "string") { return normalizeUnicodeDigitsToEnglish(value) as T; } if (Array.isArray(value)) { return value.map((item) => normalizeUnicodeDigitsDeep(item)) as T; } if (typeof value === "object") { const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { out[k] = normalizeUnicodeDigitsDeep(v); } return out as T; } return value; }