n2words
Version:
Convert numbers to words in 70+ languages with zero dependencies. Supports BigInt, decimals, and browser/Node.js environments.
295 lines (251 loc) • 8.54 kB
JavaScript
/**
* Swahili (Kenya) language converter
*
* CLDR: sw-KE | Swahili as used in Kenya
*
* Key features:
* - "na" connector for compound numbers
* - Reversed hundreds: "mia moja" (one hundred)
* - Scale words: elfu, milioni, bilioni
*/
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 { western } from './utils/scale.js'
// ============================================================================
// Vocabulary
// ============================================================================
const ONES = ['sifuri', 'moja', 'mbili', 'tatu', 'nne', 'tano', 'sita', 'saba', 'nane', 'tisa']
/** @type {Record<number, string>} */
const TENS = { 10: 'kumi', 20: 'ishirini', 30: 'thelathini', 40: 'arobaini', 50: 'hamsini', 60: 'sitini', 70: 'sabini', 80: 'themanini', 90: 'tisini' }
const SCALE_WORDS = ['', 'elfu', 'milioni', 'bilioni', 'trilioni', 'kwadrilioni', 'kwintilioni']
// Supported magnitude ceiling (checked at the public entry points), derived from the scale table.
export const cardinalMax = western(SCALE_WORDS.length - 1)
export const ordinalMax = western(SCALE_WORDS.length - 1)
export const currencyMax = western(SCALE_WORDS.length - 1)
const ZERO = 'sifuri'
const NEGATIVE = 'minus'
const DECIMAL_SEP = 'nukta'
// ============================================================================
// Ordinal Vocabulary
// ============================================================================
// Swahili ordinals use "wa" + cardinal: wa kwanza (1st), wa pili (2nd)
// First few have special forms
/** @type {Record<number, string>} */
const ORDINAL_ONES = {
1: 'wa kwanza',
2: 'wa pili',
3: 'wa tatu',
4: 'wa nne',
5: 'wa tano',
6: 'wa sita',
7: 'wa saba',
8: 'wa nane',
9: 'wa tisa',
}
const ORDINAL_PREFIX = 'wa'
// ============================================================================
// Currency Vocabulary (Kenyan Shilling)
// ============================================================================
const SHILLING = 'shilingi'
const CENT = 'senti'
// ============================================================================
// Conversion Functions
// ============================================================================
/**
* @param {number} n The integer (0-99) to convert
* @returns {string} The number in Swahili words
*/
function wordsUnder100(n) {
if (n < 10) return ONES[n]
if (n === 10) return TENS[10]
if (n < 20) {
// 11-19: 'kumi na <digit>'
return TENS[10] + ' na ' + ONES[n - 10]
}
const tens = Math.trunc(n / 10) * 10
const ones = n % 10
if (ones === 0) return TENS[tens]
return TENS[tens] + ' na ' + ONES[ones]
}
/**
* @param {number} n The integer (0-999) to convert
* @returns {string} The number in Swahili words
*/
function wordsUnder1000(n) {
if (n < 100) return wordsUnder100(n)
if (n === 100) return 'mia moja'
const hundreds = Math.trunc(n / 100)
const rest = n % 100
const parts = []
// Hundreds: 'mia <digit>'
parts.push('mia ' + ONES[hundreds])
if (rest > 0) {
if (rest < 10) {
parts.push('na ' + ONES[rest])
}
else {
parts.push(wordsUnder100(rest))
}
}
return parts.join(' ')
}
/**
* @param {bigint} n The integer to split into 3-digit segments
* @returns {number[]} The segments, least-significant first
*/
function extractSegments(n) {
const segments = []
let temp = n
while (temp > 0n) {
segments.push(Number(temp % 1000n))
temp = temp / 1000n
}
return segments
}
/**
* @param {bigint} n The integer to convert
* @returns {string} The number in Swahili words
*/
function integerToWords(n) {
if (n === 0n) return ZERO
// segments stored least-significant first: [ones, thousands, millions, ...]
const segments = extractSegments(n)
const parts = []
// Iterate from highest scale to lowest
for (let scaleIndex = segments.length - 1; scaleIndex >= 0; scaleIndex--) {
const val = segments[scaleIndex]
if (val === 0) continue
if (scaleIndex === 0) {
// Units segment
if (val < 10 && parts.length > 0) {
parts.push('na ' + ONES[val])
}
else if (val === 100 && parts.length > 0) {
// In compound numbers (e.g., 1100 -> 'elfu moja mia'), use 'mia' not 'mia moja'
parts.push('mia')
}
else {
parts.push(wordsUnder1000(val))
}
}
else {
// Scale segments: 'elfu moja', 'milioni mbili'
const unit = (val === 1) ? 'moja' : wordsUnder1000(val)
parts.push(SCALE_WORDS[scaleIndex] + ' ' + unit)
}
}
return parts.join(' ').trim()
}
/**
* @param {string} decimalPart The digits after the decimal point
* @returns {string} The decimal digits in Swahili words
*/
function decimalPartToWords(decimalPart) {
let result = ''
let i = 0
while (i < decimalPart.length && decimalPart[i] === '0') {
if (result) result += ' '
result += ZERO
i++
}
const remainder = decimalPart.slice(i)
if (remainder) {
if (result) result += ' '
result += integerToWords(BigInt(remainder))
}
return result
}
/**
* Converts a numeric value to Swahili words.
* @param {number | string | bigint} value - The numeric value to convert
* @returns {string} The number in Swahili words
*/
function toCardinal(value) {
const { isNegative, integerPart, decimalPart } = parseCardinalValue(value)
checkMax(integerPart, cardinalMax, decimalPart)
let result = ''
if (isNegative) {
result = NEGATIVE + ' '
}
result += integerToWords(integerPart)
if (decimalPart) {
result += ' ' + DECIMAL_SEP + ' ' + decimalPartToWords(decimalPart)
}
return result
}
// ============================================================================
// ORDINAL: toOrdinal(value)
// ============================================================================
/**
* Converts a non-negative integer to Swahili ordinal words.
*
* Swahili ordinals: wa kwanza (1st), wa pili (2nd), wa tatu (3rd), etc.
* @param {bigint} n - Positive integer to convert
* @returns {string} Swahili ordinal words
*/
function integerToOrdinal(n) {
// Special forms for 1-9
if (n >= 1n && n <= 9n) {
return ORDINAL_ONES[Number(n)]
}
// For 10+, use "wa" prefix + cardinal
return ORDINAL_PREFIX + ' ' + integerToWords(n)
}
/**
* Converts a numeric value to Swahili 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) // 'wa kwanza'
* toOrdinal(2) // 'wa pili'
* toOrdinal(10) // 'wa kumi'
*/
function toOrdinal(value) {
const integerPart = parseOrdinalValue(value)
checkMax(integerPart, ordinalMax)
return integerToOrdinal(integerPart)
}
// ============================================================================
// CURRENCY: toCurrency(value)
// ============================================================================
/**
* Converts a numeric value to Swahili currency words (Kenyan Shilling).
*
* Uses shilingi and senti (100 senti = 1 shilingi).
* @param {number | string | bigint} value - The currency amount to convert
* @returns {string} The amount in Swahili 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) // 'shilingi arobaini na mbili'
* toCurrency(1.50) // 'shilingi moja na senti hamsini'
* toCurrency(-5) // 'minus shilingi tano'
*/
function toCurrency(value) {
const { isNegative, dollars: shillings, cents: senti } = parseCurrencyValue(value)
checkMax(shillings, currencyMax)
let result = ''
if (isNegative) {
result = NEGATIVE + ' '
}
// Shillings part
if (shillings > 0n || senti === 0n) {
result += SHILLING + ' ' + integerToWords(shillings)
}
// Senti part
if (senti > 0n) {
if (shillings > 0n) {
result += ' na '
}
result += CENT + ' ' + integerToWords(senti)
}
return result
}
// ============================================================================
// Exports
// ============================================================================
export { toCardinal, toOrdinal, toCurrency }