n2words
Version:
Convert numbers to words in 70+ languages with zero dependencies. Supports BigInt, decimals, and browser/Node.js environments.
276 lines (231 loc) • 10.9 kB
JavaScript
/**
* Marathi (India) language converter
*
* CLDR: mr-IN | Marathi as used in India
*
* Key features:
* - Indian numbering system (हजार, लाख, कोटी)
* - Devanagari script
* - 3-2-2 grouping pattern (last 3 digits, then groups of 2)
* - Complete word forms for 0-99
* - Per-digit decimal reading
*/
import { parseCardinalValue } from './utils/parse-cardinal.js'
import { parseCurrencyValue } from './utils/parse-currency.js'
import { parseOrdinalValue } from './utils/parse-ordinal.js'
import { checkMax } from './utils/check-max.js'
import { indian } from './utils/scale.js'
// ============================================================================
// Vocabulary
// ============================================================================
const ZERO = 'शून्य'
const NEGATIVE = 'उणे'
const DECIMAL_SEP = 'दशांश'
const HUNDRED = 'शंभर'
// ============================================================================
// Ordinal Vocabulary
// ============================================================================
// Ordinal suffix (adds to cardinal for numbers >= 7)
const ORDINAL_SUFFIX = 'वा'
// Special ordinals for first few numbers (1-6 have irregular forms)
const ORDINAL_SPECIAL = ['', 'पहिला', 'दुसरा', 'तिसरा', 'चौथा', 'पाचवा', 'सहावा']
// ============================================================================
// Currency Vocabulary (Indian Rupee)
// ============================================================================
// Rupee: singular/plural
const RUPEE = 'रुपया'
const RUPEES = 'रुपये'
// Paisa: singular/plural
const PAISA = 'पैसा'
const PAISE = 'पैसे'
const BELOW_HUNDRED = [
'शून्य', 'एक', 'दोन', 'तीन', 'चार', 'पाच', 'सहा', 'सात', 'आठ', 'नऊ',
'दहा', 'अकरा', 'बारा', 'तेरा', 'चौदा', 'पंधरा', 'सोळा', 'सतरा', 'अठरा', 'एकोणीस',
'वीस', 'एकवीस', 'बावीस', 'तेवीस', 'चोवीस', 'पंचवीस', 'सव्वीस', 'सत्तावीस', 'अठ्ठावीस', 'एकोणतीस',
'तीस', 'एकतीस', 'बत्तीस', 'तेहेतीस', 'चौतीस', 'पस्तीस', 'छत्तीस', 'सदतीस', 'अडतीस', 'एकोणचाळीस',
'चाळीस', 'एकेचाळीस', 'बेचाळीस', 'त्रेचाळीस', 'चव्वेचाळीस', 'पंचेचाळीस', 'सेहेचाळीस', 'सत्तेचाळीस', 'अठ्ठेचाळीस', 'एकोणपन्नास',
'पन्नास', 'एक्काव्वन', 'बावन्न', 'त्रेपन्न', 'चोपन्न', 'पंचाव्वन', 'छप्पन्न', 'सत्तावन्न', 'अठ्ठावन्न', 'एकोणसाठ',
'साठ', 'एकसष्ठ', 'बासष्ठ', 'त्रेसष्ठ', 'चौसष्ठ', 'पासष्ठ', 'सहासष्ठ', 'सदुसष्ठ', 'अडुसष्ठ', 'एकोणसत्तर',
'सत्तर', 'एकाहत्तर', 'बाहत्तर', 'त्र्याहत्तर', 'चौऱ्याहत्तर', 'पंच्याहत्तर', 'शहात्तर', 'सत्याहत्तर', 'अठ्ठ्याहत्तर', 'एकोणऐंशी',
'ऐंशी', 'एक्याऐंशी', 'ब्याऐंशी', 'त्र्याऐंशी', 'चौऱ्याऐंशी', 'पंच्याऐंशी', 'शहाऐंशी', 'सत्याऐंशी', 'अठ्ठ्याऐंशी', 'एकोणनव्वद',
'नव्वद', 'एक्याण्णव', 'ब्याण्णव', 'त्र्याण्णव', 'चौऱ्याण्णव', 'पंच्याण्णव', 'शहाण्णव', 'सत्याण्णव', 'अठ्ठ्याण्णव', 'नव्याण्णव',
]
// Scale words: index 0 = units (empty), 1 = thousand, 2 = lakh, 3 = crore, etc.
const SCALE_WORDS = ['', 'हजार', 'लाख', 'कोटी', 'अब्ज', 'खर्व', 'निखर्व', 'महापद्म', 'शंकू']
// 3-2-2 Indian grouping: a 3-digit base segment, then 2 digits per scale word
// (SCALE_WORDS[0] = '' is the units slot). Past the table the scale word is
// dropped, which collapses the magnitude — so cap there.
export const cardinalMax = indian(SCALE_WORDS.length)
export const ordinalMax = indian(SCALE_WORDS.length)
export const currencyMax = indian(SCALE_WORDS.length)
// ============================================================================
// Segment Building
// ============================================================================
/**
* Builds words for a 0-999 segment.
* @param {number} n - Segment value (0-999)
* @returns {string} Marathi words for the segment
*/
function buildSegment(n) {
if (n === 0) return ''
if (n < 100) return BELOW_HUNDRED[n]
const hundreds = Math.trunc(n / 100)
const remainder = n % 100
if (remainder === 0) {
return BELOW_HUNDRED[hundreds] + ' ' + HUNDRED
}
return BELOW_HUNDRED[hundreds] + ' ' + HUNDRED + ' ' + BELOW_HUNDRED[remainder]
}
// ============================================================================
// Conversion Functions
// ============================================================================
/**
* Converts a non-negative integer to Marathi words.
*
* Uses BigInt modulo for segment extraction (faster than string slicing).
* South Asian 3-2-2 grouping: first 3 digits, then groups of 2.
* @param {bigint} n - Non-negative integer to convert
* @returns {string} Marathi words
*/
function integerToWords(n) {
if (n === 0n) return ZERO
// Fast path: numbers < 1000 (direct lookup)
if (n < 1000n) {
return buildSegment(Number(n))
}
// Extract segments using BigInt modulo
const segments = []
segments.push(Number(n % 1000n))
let temp = n / 1000n
while (temp > 0n) {
segments.push(Number(temp % 100n))
temp = temp / 100n
}
// Build result string (process from most-significant to least)
const words = []
for (let i = segments.length - 1; i >= 0; i--) {
const segment = segments[i]
if (segment === 0) continue
if (i === 0) {
words.push(buildSegment(segment))
}
else {
words.push(BELOW_HUNDRED[segment])
}
if (i > 0 && SCALE_WORDS[i]) {
words.push(SCALE_WORDS[i])
}
}
return words.join(' ')
}
/**
* Reads the fractional digits per-digit in Marathi.
* @param {string} decimalPart - Decimal digit string
* @returns {string} Marathi words for each digit
*/
function decimalPartToWords(decimalPart) {
// Per-digit decimal reading
const digits = []
for (const char of decimalPart) {
const d = parseInt(char, 10)
digits.push(d === 0 ? ZERO : BELOW_HUNDRED[d])
}
return digits.join(' ')
}
/**
* Converts a numeric value to Marathi words.
* @param {number | string | bigint} value - The numeric value to convert
* @returns {string} The number in Marathi words
*/
function toCardinal(value) {
const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
// The fraction is spelled digit by digit, so only the integer part has a ceiling.
checkMax(integerPart, cardinalMax)
let result = ''
if (isNegative) {
result = NEGATIVE + ' '
}
result += integerToWords(integerPart)
if (decimalPart) {
result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
}
return result
}
// ============================================================================
// ORDINAL: toOrdinal(value)
// ============================================================================
/**
* Converts a positive integer to Marathi ordinal words.
*
* Marathi ordinals: First 6 are irregular, then add -वा suffix.
* @param {bigint} n - Positive integer to convert
* @returns {string} Marathi ordinal words
*/
function integerToOrdinal(n) {
// Special ordinals for 1-6
if (n >= 1n && n <= 6n) {
return ORDINAL_SPECIAL[Number(n)]
}
// For 7 and above, add suffix to cardinal
const cardinal = integerToWords(n)
return cardinal + ORDINAL_SUFFIX
}
/**
* Converts a numeric value to Marathi ordinal words.
* @param {number | string | bigint} value - The numeric value to convert (positive integer)
* @returns {string} The number as ordinal words
* @throws {TypeError} If value is not a valid numeric type
* @throws {RangeError} If value is negative, zero, or has a decimal part
* @example
* toOrdinal(1) // 'पहिला'
* toOrdinal(2) // 'दुसरा'
* toOrdinal(3) // 'तिसरा'
* toOrdinal(10) // 'दहावा'
*/
function toOrdinal(value) {
const integerPart = parseOrdinalValue(value)
// Ordinals build on the cardinal speller, so they share its ceiling.
checkMax(integerPart, ordinalMax)
return integerToOrdinal(integerPart)
}
// ============================================================================
// CURRENCY: toCurrency(value, options?)
// ============================================================================
/**
* Converts a numeric value to Marathi currency words (Indian Rupee).
* @param {number | string | bigint} value - The currency amount to convert
* @returns {string} The amount in Marathi currency words
* @throws {TypeError} If value is not a valid numeric type
* @throws {Error} If value is not a valid number format
* @example
* toCurrency(42.50) // 'बेचाळीस रुपये पन्नास पैसे'
* toCurrency(1) // 'एक रुपया'
* toCurrency(0.01) // 'एक पैसा'
*/
function toCurrency(value) {
const { isNegative, dollars: rupees, cents: paise } = parseCurrencyValue(value)
checkMax(rupees, currencyMax)
// Build result
let result = ''
if (isNegative) result = NEGATIVE + ' '
// Rupees part - show if non-zero, or if no paise
if (rupees > 0n || paise === 0n) {
result += integerToWords(rupees)
// Singular for 1 rupee, plural otherwise
result += ' ' + (rupees === 1n ? RUPEE : RUPEES)
}
// Paise part
if (paise > 0n) {
if (rupees > 0n) {
result += ' '
}
result += integerToWords(paise)
// Singular for 1 paisa, plural otherwise
result += ' ' + (paise === 1n ? PAISA : PAISE)
}
return result
}
// ============================================================================
// Exports
// ============================================================================
export { toCardinal, toOrdinal, toCurrency }