/** All money is integer paise. Fractional-rupee floats never enter the domain. */ export type Paise = number export function fromRupees(rupees: number): Paise { return Math.round(rupees * 100) } /** Indian digit grouping: 12,34,567.89 */ export function formatINR(amount: Paise, opts: { symbol?: boolean } = {}): string { const sign = amount < 0 ? '-' : '' const abs = Math.abs(Math.round(amount)) const whole = String(Math.trunc(abs / 100)) const frac = String(abs % 100).padStart(2, '0') const grouped = whole.length <= 3 ? whole : whole.slice(0, -3).replace(/\B(?=(\d{2})+(?!\d))/g, ',') + ',' + whole.slice(-3) return `${sign}${opts.symbol === false ? '' : '₹'}${grouped}.${frac}` } const ONES = [ '', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten', 'Eleven', 'Twelve', 'Thirteen', 'Fourteen', 'Fifteen', 'Sixteen', 'Seventeen', 'Eighteen', 'Nineteen', ] const TENS = ['', '', 'Twenty', 'Thirty', 'Forty', 'Fifty', 'Sixty', 'Seventy', 'Eighty', 'Ninety'] function below100(n: number): string { if (n < 20) return ONES[n]! const t = TENS[Math.floor(n / 10)]! const o = n % 10 return o === 0 ? t : `${t} ${ONES[o]!}` } function below1000(n: number): string { const h = Math.floor(n / 100) const r = n % 100 const parts: string[] = [] if (h > 0) parts.push(`${ONES[h]!} Hundred`) if (r > 0) parts.push(below100(r)) return parts.join(' ') } /** Whole non-negative rupees to Indian-numbering words (crore/lakh/thousand). */ function indianWords(rupees: number): string { if (rupees === 0) return 'Zero' let n = rupees const crore = Math.floor(n / 10_000_000); n %= 10_000_000 const lakh = Math.floor(n / 100_000); n %= 100_000 const thousand = Math.floor(n / 1000); n %= 1000 const parts: string[] = [] if (crore > 0) parts.push(`${below1000(crore)} Crore`) if (lakh > 0) parts.push(`${below100(lakh)} Lakh`) if (thousand > 0) parts.push(`${below100(thousand)} Thousand`) if (n > 0) parts.push(below1000(n)) return parts.join(' ') } /** * Integer paise to an Indian-numbering "amount in words" string for GST invoices * (e.g. 1,00,00,000 paise → "Rupees One Lakh Only"). Rupees and paise are named * separately; "Paisa" is singular. The `formatINR` sibling for the numeric form. */ export function amountInWordsINR(paise: Paise): string { const sign = paise < 0 ? 'Minus ' : '' const abs = Math.abs(Math.round(paise)) const rupees = Math.trunc(abs / 100) const p = abs % 100 const paiseWord = p === 1 ? 'Paisa' : 'Paise' if (rupees > 0 && p > 0) return `${sign}Rupees ${indianWords(rupees)} and ${below100(p)} ${paiseWord} Only` if (rupees > 0) return `${sign}Rupees ${indianWords(rupees)} Only` if (p > 0) return `${sign}${below100(p)} ${paiseWord} Only` return 'Rupees Zero Only' }