admin/helpers/numberToPersianWords.ts
alisaza e1eaf5eff5 feat: initial ghabilee-admin backoffice app
Extract admin dashboard from ghabilee-frontend2 into a dedicated Next.js
app for backoffice.ghabilee.ir (no SEO indexing / Clarity).
2026-09-05 13:12:59 +03:30

107 lines
3.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { coerceToString } from '@/helpers'
const ONES = ['', 'یک', 'دو', 'سه', 'چهار', 'پنج', 'شش', 'هفت', 'هشت', 'نه']
const TEENS = ['ده', 'یازده', 'دوازده', 'سیزده', 'چهارده', 'پانزده', 'شانزده', 'هفده', 'هجده', 'نوزده']
const TENS = ['', '', 'بیست', 'سی', 'چهل', 'پنجاه', 'شصت', 'هفتاد', 'هشتاد', 'نود']
const HUNDREDS = ['', 'صد', 'دویست', 'سیصد', 'چهارصد', 'پانصد', 'ششصد', 'هفتصد', 'هشتصد', 'نهصد']
const SCALES = ['', 'هزار', 'میلیون', 'میلیارد', 'تریلیون']
function joinParts(parts: string[]): string {
return parts.filter(Boolean).join(' و ')
}
/** تبدیل عدد صحیح ۰–۹۹۹ به حروف فارسی */
function underThousandToWords(n: number): string {
if (n === 0) return ''
const hundred = Math.floor(n / 100)
const rest = n % 100
const parts: string[] = []
if (hundred > 0) parts.push(HUNDREDS[hundred])
if (rest > 0) {
if (rest < 10) parts.push(ONES[rest])
else if (rest < 20) parts.push(TEENS[rest - 10])
else {
const ten = Math.floor(rest / 10)
const one = rest % 10
parts.push(one > 0 ? `${TENS[ten]} و ${ONES[one]}` : TENS[ten])
}
}
return joinParts(parts)
}
/**
* تبدیل عدد صحیح غیرمنفی به حروف فارسی.
* اعداد خارج از محدودهٔ امن یا غیرصحیح → رشتهٔ خالی.
*/
export function numberToPersianWords(value: number): string {
if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) return ''
if (value === 0) return 'صفر'
const groups: number[] = []
let remaining = value
while (remaining > 0) {
groups.push(remaining % 1000)
remaining = Math.floor(remaining / 1000)
}
if (groups.length > SCALES.length) return ''
const parts: string[] = []
for (let i = groups.length - 1; i >= 0; i -= 1) {
const group = groups[i]
if (group === 0) continue
const words = underThousandToWords(group)
const scale = SCALES[i]
parts.push(scale ? `${words} ${scale}` : words)
}
return joinParts(parts)
}
/** پارس مقدار اینپوت مبلغ (عدد یا رشتهٔ رقم) به عدد صحیح غیرمنفی */
export function parseAmountInputValue(value: unknown): number | null {
if (value == null || value === '') return null
if (typeof value === 'number') {
if (!Number.isFinite(value) || value < 0) return null
return Math.trunc(value)
}
const digits = coerceToString(value)
.replace(/[۰-۹]/g, (digit) => String(digit.charCodeAt(0) - '۰'.charCodeAt(0)))
.replace(/[٠-٩]/g, (digit) => String(digit.charCodeAt(0) - '٠'.charCodeAt(0)))
.replace(/[^\d]/g, '')
if (!digits) return null
const amount = Number(digits)
if (!Number.isFinite(amount) || amount < 0) return null
return Math.trunc(amount)
}
/**
* نمایش مبلغ به حروف با واحد تومان.
* برای ۰ یا مقدار نامعتبر رشتهٔ خالی برمی‌گرداند (زیر اینپوت چیزی نشان نده).
*/
export function formatTomanInWords(value: unknown): string {
const amount = parseAmountInputValue(value)
if (amount == null || amount <= 0) return ''
const words = numberToPersianWords(amount)
return words ? `${words} تومان` : ''
}