You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
/** 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'
|
|
}
|