From 603107396b332a17ed4171895bc9370fdec1fec5 Mon Sep 17 00:00:00 2001 From: Brian Funk Date: Sun, 8 Feb 2026 09:20:50 -0500 Subject: [PATCH] feat: add 13 new languages, bringing total to 22 New languages: Japanese, Korean, Arabic, Italian, Dutch, Turkish, Polish, Swedish, Indonesian, Thai, Norwegian, Finnish, Icelandic. Full Scandinavian coverage (Danish, Swedish, Norwegian, Finnish, Icelandic). All languages support BigInt up to 10^36. 265 tests passing. Version bumped to 1.0.1. Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 10 ++ README.md | 58 ++++++-- index.js | 47 ++++++- languages/ar.js | 128 +++++++++++++++++ languages/fi.js | 103 ++++++++++++++ languages/id.js | 105 ++++++++++++++ languages/index.js | 67 ++++++++- languages/is.js | 103 ++++++++++++++ languages/it.js | 124 +++++++++++++++++ languages/ja.js | 122 ++++++++++++++++ languages/ko.js | 122 ++++++++++++++++ languages/nl.js | 112 +++++++++++++++ languages/no.js | 103 ++++++++++++++ languages/pl.js | 146 ++++++++++++++++++++ languages/sv.js | 103 ++++++++++++++ languages/th.js | 123 +++++++++++++++++ languages/tr.js | 102 ++++++++++++++ package.json | 2 +- test/index.test.js | 337 ++++++++++++++++++++++++++++++++++++++++++++- 19 files changed, 1997 insertions(+), 20 deletions(-) create mode 100644 languages/ar.js create mode 100644 languages/fi.js create mode 100644 languages/id.js create mode 100644 languages/is.js create mode 100644 languages/it.js create mode 100644 languages/ja.js create mode 100644 languages/ko.js create mode 100644 languages/nl.js create mode 100644 languages/no.js create mode 100644 languages/pl.js create mode 100644 languages/sv.js create mode 100644 languages/th.js create mode 100644 languages/tr.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 55f6ec2..808bf9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.1] - 2026-02-08 + +### Added + +- **13 New Languages** - Japanese (`ja`), Korean (`ko`), Arabic (`ar`), Italian (`it`), Dutch (`nl`), Turkish (`tr`), Polish (`pl`), Swedish (`sv`), Indonesian (`id`), Thai (`th`), Norwegian (`no`), Finnish (`fi`), Icelandic (`is`) + - Total language support now at 22 languages + - Full Scandinavian coverage: Danish, Swedish, Norwegian, Finnish, Icelandic + - All new languages support BigInt up to 10^36 +- 49 new tests for all new languages (265 total) + ## [1.0.0] - 2026-02-01 ### Breaking Changes diff --git a/README.md b/README.md index c7d1ae4..63b6dac 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,16 @@ > Number One Way to Makes Words from Numbers -Transform any number into beautiful words. From `42` to `"forty-two"`, from `1000000` to `"one million"`. Supports **8 languages**, ordinals, currency, Roman numerals, and more! +Transform any number into beautiful words. From `42` to `"forty-two"`, from `1000000` to `"one million"`. Supports **22 languages**, ordinals, currency, Roman numerals, and more! ## Why numberstring? - **Zero dependencies** - Lightweight and fast -- **9 languages** - English, Spanish, French, German, Danish, Chinese, Hindi, Russian, Portuguese +- **22 languages** - English, Spanish, French, German, Danish, Chinese, Hindi, Russian, Portuguese, Japanese, Korean, Arabic, Italian, Dutch, Turkish, Polish, Swedish, Indonesian, Thai, Norwegian, Finnish, Icelandic - **Huge range** - Supports 0 to decillions (10^36) with BigInt - **Feature-rich** - Ordinals, decimals, currency, fractions, years, phone numbers - **Roman numerals** - Convert to and from Roman numerals -- **Well tested** - 216 tests with 90%+ coverage +- **Well tested** - 265 tests with 90%+ coverage - **Modern ES modules** - Tree-shakeable, TypeScript-friendly ## Installation @@ -191,10 +191,10 @@ comma(1234567); // '1,234,567' ## Multi-Language Support -numberstring supports 9 languages! Each language is in a separate file for easy tree-shaking. +numberstring supports 22 languages! Each language is in a separate file for easy tree-shaking. ```javascript -import { toWords, spanish, french, german, danish, chinese, hindi, russian, portuguese } from 'numberstring'; +import { toWords } from 'numberstring'; // Using toWords with lang option toWords(42, { lang: 'es' }); // 'cuarenta y dos' @@ -205,16 +205,48 @@ toWords(42, { lang: 'zh' }); // '四十二' toWords(42, { lang: 'hi' }); // 'बयालीस' toWords(42, { lang: 'ru' }); // 'сорок два' toWords(42, { lang: 'pt' }); // 'quarenta e dois' - -// Or use language functions directly -spanish(1000); // 'mil' -french(80); // 'quatre-vingts' -german(21); // 'einundzwanzig' -chinese(10000); // '一万' -russian(2000); // 'две тысячи' -portuguese(100); // 'cem' +toWords(42, { lang: 'ja' }); // '四十二' +toWords(42, { lang: 'ko' }); // '사십이' +toWords(42, { lang: 'ar' }); // 'اثنان وأربعون' +toWords(42, { lang: 'it' }); // 'quarantadue' +toWords(42, { lang: 'nl' }); // 'tweeënveertig' +toWords(42, { lang: 'tr' }); // 'kırk iki' +toWords(42, { lang: 'pl' }); // 'czterdzieści dwa' +toWords(42, { lang: 'sv' }); // 'fyrtiotvå' +toWords(42, { lang: 'id' }); // 'empat puluh dua' +toWords(42, { lang: 'th' }); // 'สี่สิบสอง' +toWords(42, { lang: 'no' }); // 'førtito' +toWords(42, { lang: 'fi' }); // 'neljäkymmentäkaksi' +toWords(42, { lang: 'is' }); // 'fjörutíu og tveir' ``` +### Supported Languages + +| Code | Language | Example (42) | +|------|----------|-------------| +| `en` | English | forty-two | +| `es` | Spanish | cuarenta y dos | +| `fr` | French | quarante-deux | +| `de` | German | zweiundvierzig | +| `da` | Danish | toogfyrre | +| `zh` | Chinese | 四十二 | +| `hi` | Hindi | बयालीस | +| `ru` | Russian | сорок два | +| `pt` | Portuguese | quarenta e dois | +| `ja` | Japanese | 四十二 | +| `ko` | Korean | 사십이 | +| `ar` | Arabic | اثنان وأربعون | +| `it` | Italian | quarantadue | +| `nl` | Dutch | tweeënveertig | +| `tr` | Turkish | kırk iki | +| `pl` | Polish | czterdzieści dwa | +| `sv` | Swedish | fyrtiotvå | +| `id` | Indonesian | empat puluh dua | +| `th` | Thai | สี่สิบสอง | +| `no` | Norwegian | førtito | +| `fi` | Finnish | neljäkymmentäkaksi | +| `is` | Icelandic | fjörutíu og tveir | + ### Adding a New Language Languages are modular! To add a new language: diff --git a/index.js b/index.js index 9ffb679..e07ac47 100644 --- a/index.js +++ b/index.js @@ -14,15 +14,15 @@ /** * numberstring - Convert numbers to their word representation - * Supports English, Spanish, French, German, Danish, Mandarin Chinese, Hindi, Russian, and Portuguese + * Supports 22 languages including English, Spanish, French, German, Danish, Chinese, Hindi, Russian, Portuguese, Japanese, Korean, Arabic, Italian, Dutch, Turkish, Polish, Swedish, Indonesian, Thai, Norwegian, Finnish, and Icelandic * @module numberstring */ // Import language functions -import { english, spanish, french, german, danish, chinese, hindi, russian, portuguese, LANGUAGES } from './languages/index.js'; +import { english, spanish, french, german, danish, chinese, hindi, russian, portuguese, japanese, korean, arabic, italian, dutch, turkish, polish, swedish, indonesian, thai, norwegian, finnish, icelandic, LANGUAGES } from './languages/index.js'; // Re-export language functions -export { spanish, french, german, danish, chinese, hindi, russian, portuguese }; +export { spanish, french, german, danish, chinese, hindi, russian, portuguese, japanese, korean, arabic, italian, dutch, turkish, polish, swedish, indonesian, thai, norwegian, finnish, icelandic }; // ============================================================================ // CONSTANTS @@ -597,7 +597,7 @@ const percent = (pct, opt) => { * Convert a number to words in a specified language * @param {number|bigint} n - The number to convert * @param {Object} [opt] - Options object - * @param {string} [opt.lang] - Language code: 'en', 'es', 'fr', 'de', 'da', 'zh', 'hi', 'ru' + * @param {string} [opt.lang] - Language code: 'en', 'es', 'fr', 'de', 'da', 'zh', 'hi', 'ru', 'pt', 'ja', 'ko', 'ar', 'it', 'nl', 'tr', 'pl', 'sv', 'id', 'th', 'no', 'fi', 'is' * @returns {string|false} The word representation */ const toWords = (n, opt) => { @@ -633,6 +633,45 @@ const toWords = (n, opt) => { case 'portuguese': result = portuguese(n); break; + case 'japanese': + result = japanese(n); + break; + case 'korean': + result = korean(n); + break; + case 'arabic': + result = arabic(n); + break; + case 'italian': + result = italian(n); + break; + case 'dutch': + result = dutch(n); + break; + case 'turkish': + result = turkish(n); + break; + case 'polish': + result = polish(n); + break; + case 'swedish': + result = swedish(n); + break; + case 'indonesian': + result = indonesian(n); + break; + case 'thai': + result = thai(n); + break; + case 'norwegian': + result = norwegian(n); + break; + case 'finnish': + result = finnish(n); + break; + case 'icelandic': + result = icelandic(n); + break; default: result = english(n); } diff --git a/languages/ar.js b/languages/ar.js new file mode 100644 index 0000000..0995676 --- /dev/null +++ b/languages/ar.js @@ -0,0 +1,128 @@ +/** + * Arabic number-to-words converter + * Uses masculine default forms with standard group-of-3 pattern + * @module languages/ar + */ + +const AR_ONES = Object.freeze(['', 'واحد', 'اثنان', 'ثلاثة', 'أربعة', 'خمسة', 'ستة', 'سبعة', 'ثمانية', 'تسعة']); +const AR_TEENS = Object.freeze(['عشرة', 'أحد عشر', 'اثنا عشر', 'ثلاثة عشر', 'أربعة عشر', 'خمسة عشر', 'ستة عشر', 'سبعة عشر', 'ثمانية عشر', 'تسعة عشر']); +const AR_TENS = Object.freeze(['', '', 'عشرون', 'ثلاثون', 'أربعون', 'خمسون', 'ستون', 'سبعون', 'ثمانون', 'تسعون']); +const AR_HUNDREDS = Object.freeze(['', 'مائة', 'مائتان', 'ثلاثمائة', 'أربعمائة', 'خمسمائة', 'ستمائة', 'سبعمائة', 'ثمانمائة', 'تسعمائة']); +const AR_ILLIONS = Object.freeze([ + ['', '', ''], // ones + ['ألف', 'ألفان', 'آلاف'], // thousands + ['مليون', 'مليونان', 'ملايين'], // millions + ['مليار', 'ملياران', 'مليارات'], // billions + ['تريليون', 'تريليونان', 'تريليونات'], // trillions + ['كوادريليون', 'كوادريليونان', 'كوادريليونات'], // quadrillions + ['كوينتيليون', 'كوينتيليونان', 'كوينتيليونات'], // quintillions + ['سكستيليون', 'سكستيليونان', 'سكستيليونات'], // sextillions + ['سبتيليون', 'سبتيليونان', 'سبتيليونات'], // septillions + ['أوكتيليون', 'أوكتيليونان', 'أوكتيليونات'], // octillions + ['نونيليون', 'نونيليونان', 'نونيليونات'], // nonillions + ['ديسيليون', 'ديسيليونان', 'ديسيليونات'] // decillions +]); + +/** Maximum supported value (10^36 - 1, up to decillions) */ +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +/** + * Get the correct Arabic scale word form + * Arabic has 3 forms: singular (1), dual (2), plural (3-10) + * For 11+, use the singular form + */ +const getArScale = (n, forms) => { + if (n === 1) return forms[0]; + if (n === 2) return forms[1]; + if (n >= 3 && n <= 10) return forms[2]; + return forms[0]; // 11+ uses singular +}; + +const hundredAr = (n) => { + if (n < 100 || n >= 1000) return ''; + return AR_HUNDREDS[Math.floor(n / 100)]; +}; + +const tenAr = (n) => { + if (n === 0) return ''; + if (n < 10) return AR_ONES[n]; + if (n < 20) return AR_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return AR_TENS[tensDigit]; + // In Arabic, ones come before tens: واحد وعشرون (one and twenty) + return `${AR_ONES[onesDigit]} و${AR_TENS[tensDigit]}`; +}; + +/** + * Convert a number to Arabic words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Arabic word representation + * + * @example + * arabic(42) // 'اثنان وأربعون' + * arabic(1000) // 'ألف' + */ +const arabic = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'صفر'; + + const parts = []; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h === 0) continue; + + let chunk = ''; + const hund = hundredAr(h); + const t = tenment(num, i); + const tenWord = tenAr(t); + + if (hund && tenWord) { + chunk = `${hund} و${tenWord}`; + } else if (hund) { + chunk = hund; + } else if (tenWord) { + chunk = tenWord; + } + + if (i > 0) { + const forms = AR_ILLIONS[i]; + if (h === 1) { + // Just the scale word alone (e.g., ألف for 1000) + chunk = forms[0]; + } else if (h === 2) { + // Dual form (e.g., ألفان for 2000) + chunk = forms[1]; + } else { + // 3+: chunk + scale word + const scaleWord = getArScale(h, forms); + chunk = `${chunk} ${scaleWord}`; + } + } + + parts.push(chunk); + } + + return parts.join(' و'); +}; + +export default arabic; +export { arabic, AR_ONES, AR_TENS, AR_TEENS, AR_HUNDREDS, AR_ILLIONS, MAX_VALUE }; diff --git a/languages/fi.js b/languages/fi.js new file mode 100644 index 0000000..99e2b9e --- /dev/null +++ b/languages/fi.js @@ -0,0 +1,103 @@ +/** + * Finnish number-to-words converter + * Finnish is agglutinative with partitive forms for plurals + * @module languages/fi + */ + +const FI_ONES = Object.freeze(['', 'yksi', 'kaksi', 'kolme', 'neljä', 'viisi', 'kuusi', 'seitsemän', 'kahdeksan', 'yhdeksän']); +const FI_TEENS = Object.freeze(['kymmenen', 'yksitoista', 'kaksitoista', 'kolmetoista', 'neljätoista', 'viisitoista', 'kuusitoista', 'seitsemäntoista', 'kahdeksantoista', 'yhdeksäntoista']); +const FI_TENS = Object.freeze(['', '', 'kaksikymmentä', 'kolmekymmentä', 'neljäkymmentä', 'viisikymmentä', 'kuusikymmentä', 'seitsemänkymmentä', 'kahdeksankymmentä', 'yhdeksänkymmentä']); +const FI_ILLIONS = Object.freeze(['', 'tuhat', 'miljoona', 'miljardi', 'biljoona', 'biljardi', 'triljoona', 'triljardi', 'kvadriljoona', 'kvadriljardi', 'kvintiljoona', 'kvintiljardi']); +const FI_ILLIONS_PLURAL = Object.freeze(['', 'tuhatta', 'miljoonaa', 'miljardia', 'biljoonaa', 'biljardia', 'triljoonaa', 'triljardia', 'kvadriljoonaa', 'kvadriljardia', 'kvintiljoonaa', 'kvintiljardia']); + +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +const tenFi = (n) => { + if (n === 0) return ''; + if (n < 10) return FI_ONES[n]; + if (n < 20) return FI_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return FI_TENS[tensDigit]; + return `${FI_TENS[tensDigit]}${FI_ONES[onesDigit]}`; +}; + +const hundredFi = (n) => { + if (n < 100 || n >= 1000) return ''; + const h = Math.floor(n / 100); + if (h === 1) return 'sata'; + return `${FI_ONES[h]}sataa`; +}; + +/** + * Convert a number to Finnish words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Finnish word representation + * + * @example + * finnish(42) // 'neljäkymmentäkaksi' + * finnish(1000) // 'tuhat' + */ +const finnish = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'nolla'; + if (num === 1n) return 'yksi'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h > 0) { + if (i === 0) { + if (h >= 100) s += hundredFi(h); + const t = tenment(num, i); + if (t > 0) s += tenFi(t); + } else if (i === 1) { + if (h === 1) { + s += 'tuhat'; + } else { + if (h >= 100) s += hundredFi(h); + const t = tenment(num, i); + if (t > 0) s += tenFi(t); + s += 'tuhatta'; + } + } else { + if (h >= 100) s += hundredFi(h); + const t = tenment(num, i); + if (t > 0) { + if (t === 1) { + s += 'yksi '; + } else { + s += tenFi(t) + ' '; + } + } else if (h < 100 && h >= 1 && h === 1) { + s += 'yksi '; + } + const illionWord = h === 1 ? FI_ILLIONS[i] : FI_ILLIONS_PLURAL[i]; + s += `${illionWord} `; + } + } + } + + return s.trim(); +}; + +export default finnish; +export { finnish }; diff --git a/languages/id.js b/languages/id.js new file mode 100644 index 0000000..9feede8 --- /dev/null +++ b/languages/id.js @@ -0,0 +1,105 @@ +/** + * Indonesian number-to-words converter + * Very regular with "se-" prefix replacing "satu" for puluh, ratus, ribu + * @module languages/id + */ + +const ID_ONES = Object.freeze(['', 'satu', 'dua', 'tiga', 'empat', 'lima', 'enam', 'tujuh', 'delapan', 'sembilan']); +const ID_TENS = Object.freeze(['', 'sepuluh', 'dua puluh', 'tiga puluh', 'empat puluh', 'lima puluh', 'enam puluh', 'tujuh puluh', 'delapan puluh', 'sembilan puluh']); +const ID_TEENS = Object.freeze(['sepuluh', 'sebelas', 'dua belas', 'tiga belas', 'empat belas', 'lima belas', 'enam belas', 'tujuh belas', 'delapan belas', 'sembilan belas']); +const ID_ILLIONS = Object.freeze(['', 'ribu', 'juta', 'miliar', 'triliun', 'kuadriliun', 'kuintiliun', 'sekstiliun', 'septiliun', 'oktiliun', 'noniliun', 'desiliun']); + +/** Maximum supported value (10^36 - 1, up to decillions) */ +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +const hundredId = (n) => { + if (n < 100 || n >= 1000) return ''; + const h = Math.floor(n / 100); + // 1 before ratus uses "se-" prefix: seratus + if (h === 1) return 'seratus'; + return `${ID_ONES[h]} ratus`; +}; + +const tenId = (n) => { + if (n === 0) return ''; + if (n < 10) return ID_ONES[n]; + // 10-19 are special: sebelas for 11, [ones] belas for 12-19, sepuluh for 10 + if (n < 20) return ID_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return ID_TENS[tensDigit]; + return `${ID_TENS[tensDigit]} ${ID_ONES[onesDigit]}`; +}; + +/** + * Convert a number to Indonesian words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Indonesian word representation + * + * @example + * indonesian(42) // 'empat puluh dua' + * indonesian(1000) // 'seribu' + * indonesian(1000000) // 'satu juta' + */ +const indonesian = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'nol'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h === 0) continue; + + if (i === 0) { + // Ones group: no scale word + const hund = hundredId(h); + if (hund) s += hund + ' '; + const t = tenment(num, i); + const tenWord = tenId(t); + if (tenWord) s += tenWord + ' '; + } else if (i === 1) { + // Thousands: 1 before ribu uses "se-" prefix: seribu + if (h === 1) { + s += 'seribu '; + } else { + const hund = hundredId(h); + if (hund) s += hund + ' '; + const t = tenment(num, i); + const tenWord = tenId(t); + if (tenWord) s += tenWord + ' '; + s += 'ribu '; + } + } else { + // Millions and above: "satu" is used (no se- prefix) + const hund = hundredId(h); + if (hund) s += hund + ' '; + const t = tenment(num, i); + const tenWord = tenId(t); + if (tenWord) s += tenWord + ' '; + s += `${ID_ILLIONS[i]} `; + } + } + + return s.trim(); +}; + +export default indonesian; +export { indonesian, ID_ONES, ID_TENS, ID_TEENS, ID_ILLIONS, MAX_VALUE }; diff --git a/languages/index.js b/languages/index.js index 83bdccd..de73e7b 100644 --- a/languages/index.js +++ b/languages/index.js @@ -20,6 +20,19 @@ import chinese from './zh.js'; import hindi from './hi.js'; import russian from './ru.js'; import portuguese from './pt.js'; +import japanese from './ja.js'; +import korean from './ko.js'; +import arabic from './ar.js'; +import italian from './it.js'; +import dutch from './nl.js'; +import turkish from './tr.js'; +import polish from './pl.js'; +import swedish from './sv.js'; +import indonesian from './id.js'; +import thai from './th.js'; +import norwegian from './no.js'; +import finnish from './fi.js'; +import icelandic from './is.js'; /** Supported language codes and aliases */ const LANGUAGES = Object.freeze({ @@ -49,7 +62,46 @@ const LANGUAGES = Object.freeze({ 'русский': 'russian', pt: 'portuguese', portuguese: 'portuguese', - português: 'portuguese' + português: 'portuguese', + ja: 'japanese', + japanese: 'japanese', + '日本語': 'japanese', + ko: 'korean', + korean: 'korean', + '한국어': 'korean', + ar: 'arabic', + arabic: 'arabic', + 'العربية': 'arabic', + it: 'italian', + italian: 'italian', + italiano: 'italian', + nl: 'dutch', + dutch: 'dutch', + nederlands: 'dutch', + tr: 'turkish', + turkish: 'turkish', + türkçe: 'turkish', + pl: 'polish', + polish: 'polish', + polski: 'polish', + sv: 'swedish', + swedish: 'swedish', + svenska: 'swedish', + id: 'indonesian', + indonesian: 'indonesian', + 'bahasa indonesia': 'indonesian', + th: 'thai', + thai: 'thai', + 'ไทย': 'thai', + no: 'norwegian', + norwegian: 'norwegian', + norsk: 'norwegian', + fi: 'finnish', + finnish: 'finnish', + suomi: 'finnish', + is: 'icelandic', + icelandic: 'icelandic', + íslenska: 'icelandic' }); export { @@ -62,5 +114,18 @@ export { hindi, russian, portuguese, + japanese, + korean, + arabic, + italian, + dutch, + turkish, + polish, + swedish, + indonesian, + thai, + norwegian, + finnish, + icelandic, LANGUAGES }; diff --git a/languages/is.js b/languages/is.js new file mode 100644 index 0000000..4ce9a39 --- /dev/null +++ b/languages/is.js @@ -0,0 +1,103 @@ +/** + * Icelandic number-to-words converter + * Icelandic uses long scale and "og" connector between ones and tens + * @module languages/is + */ + +const IS_ONES = Object.freeze(['', 'einn', 'tveir', 'þrír', 'fjórir', 'fimm', 'sex', 'sjö', 'átta', 'níu']); +const IS_TENS = Object.freeze(['', '', 'tuttugu', 'þrjátíu', 'fjörutíu', 'fimmtíu', 'sextíu', 'sjötíu', 'áttatíu', 'níutíu']); +const IS_TEENS = Object.freeze(['tíu', 'ellefu', 'tólf', 'þrettán', 'fjórtán', 'fimmtán', 'sextán', 'sautján', 'átján', 'nítján']); +const IS_ILLIONS = Object.freeze(['', 'þúsund', 'milljón', 'milljarður', 'billjón', 'billjarður', 'trilljón', 'trilljarður', 'kvadrilljón', 'kvadrilljarður', 'kvintilljón', 'kvintilljarður']); +const IS_ILLIONS_PLURAL = Object.freeze(['', 'þúsund', 'milljónir', 'milljarðar', 'billjónir', 'billjarðar', 'trilljónir', 'trilljarðar', 'kvadrilljónir', 'kvadrilljarðar', 'kvintilljónir', 'kvintilljarðar']); + +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +const tenIs = (n) => { + if (n === 0) return ''; + if (n < 10) return IS_ONES[n]; + if (n < 20) return IS_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return IS_TENS[tensDigit]; + return `${IS_TENS[tensDigit]} og ${IS_ONES[onesDigit]}`; +}; + +const hundredIs = (n) => { + if (n < 100 || n >= 1000) return ''; + const h = Math.floor(n / 100); + if (h === 1) return 'eitt hundrað'; + return `${IS_ONES[h]} hundruð`; +}; + +/** + * Convert a number to Icelandic words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Icelandic word representation + * + * @example + * icelandic(42) // 'fjörutíu og tveir' + * icelandic(1000) // 'eitt þúsund' + */ +const icelandic = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'núll'; + if (num === 1n) return 'einn'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h > 0) { + if (i === 0) { + if (h >= 100) s += hundredIs(h) + ' '; + const t = tenment(num, i); + if (t > 0) s += tenIs(t); + } else if (i === 1) { + if (h === 1) { + s += 'eitt þúsund '; + } else { + if (h >= 100) s += hundredIs(h) + ' '; + const t = tenment(num, i); + if (t > 0) s += tenIs(t) + ' '; + s += 'þúsund '; + } + } else { + if (h >= 100) s += hundredIs(h) + ' '; + const t = tenment(num, i); + if (t > 0) { + if (t === 1) { + s += 'einn '; + } else { + s += tenIs(t) + ' '; + } + } else if (h < 100 && h >= 1 && h === 1) { + s += 'einn '; + } + const illionWord = h === 1 ? IS_ILLIONS[i] : IS_ILLIONS_PLURAL[i]; + s += `${illionWord} `; + } + } + } + + return s.trim(); +}; + +export default icelandic; +export { icelandic }; diff --git a/languages/it.js b/languages/it.js new file mode 100644 index 0000000..13b9c47 --- /dev/null +++ b/languages/it.js @@ -0,0 +1,124 @@ +/** + * Italian number-to-words converter + * Handles Italian elision rules and special plural forms + * @module languages/it + */ + +const IT_ONES = Object.freeze(['', 'uno', 'due', 'tre', 'quattro', 'cinque', 'sei', 'sette', 'otto', 'nove']); +const IT_TENS = Object.freeze(['', '', 'venti', 'trenta', 'quaranta', 'cinquanta', 'sessanta', 'settanta', 'ottanta', 'novanta']); +const IT_TEENS = Object.freeze(['dieci', 'undici', 'dodici', 'tredici', 'quattordici', 'quindici', 'sedici', 'diciassette', 'diciotto', 'diciannove']); +const IT_HUNDREDS = Object.freeze(['', 'cento', 'duecento', 'trecento', 'quattrocento', 'cinquecento', 'seicento', 'settecento', 'ottocento', 'novecento']); +const IT_ILLIONS = Object.freeze([ + ['', ''], // ones + ['mille', 'mila'], // thousands (singular, plural) + ['milione', 'milioni'], // millions + ['miliardo', 'miliardi'], // billions + ['bilione', 'bilioni'], // trillions + ['biliardo', 'biliardi'], // quadrillions + ['trilione', 'trilioni'], // quintillions + ['triliardo', 'triliardi'], // sextillions + ['quadrilione', 'quadrilioni'], // septillions + ['quadriliardo', 'quadriliardi'], // octillions + ['quintilione', 'quintilioni'], // nonillions + ['quintiliardo', 'quintiliardi'] // decillions +]); + +/** Maximum supported value (10^36 - 1, up to decillions) */ +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +const hundredIt = (n) => { + if (n < 100 || n >= 1000) return ''; + const h = Math.floor(n / 100); + return IT_HUNDREDS[h]; +}; + +const tenIt = (n) => { + if (n === 0) return ''; + if (n < 10) return IT_ONES[n]; + if (n < 20) return IT_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return IT_TENS[tensDigit]; + // Elision: drop final vowel of tens before uno (1) and otto (8) + if (onesDigit === 1 || onesDigit === 8) { + const tensWord = IT_TENS[tensDigit].slice(0, -1); + return `${tensWord}${IT_ONES[onesDigit]}`; + } + // tré gets accent in compounds + if (onesDigit === 3) { + return `${IT_TENS[tensDigit]}tré`; + } + return `${IT_TENS[tensDigit]}${IT_ONES[onesDigit]}`; +}; + +/** + * Convert a number to Italian words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Italian word representation + * + * @example + * italian(42) // 'quarantadue' + * italian(1000) // 'mille' + * italian(2000) // 'duemila' + */ +const italian = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'zero'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h > 0) { + if (i === 0) { + // Units group: just hundreds + tens + if (h >= 100) s += hundredIt(h); + const t = tenment(num, i); + if (t > 0) s += tenIt(t); + } else if (i === 1) { + // Thousands: 1000 = mille, 2000+ = [n]mila + if (h === 1) { + s += 'mille'; + } else { + if (h >= 100) s += hundredIt(h); + const t = tenment(num, i); + if (t > 0) s += tenIt(t); + s += 'mila'; + } + } else { + // Millions and above: use "un milione", "due milioni", etc. + // These are separated by spaces + if (h === 1) { + s += `un ${IT_ILLIONS[i][0]} `; + } else { + if (h >= 100) s += hundredIt(h); + const t = tenment(num, i); + if (t > 0) s += tenIt(t); + s += ` ${IT_ILLIONS[i][1]} `; + } + } + } + } + + return s.trim().replace(/\s+/g, ' '); +}; + +export default italian; +export { italian }; diff --git a/languages/ja.js b/languages/ja.js new file mode 100644 index 0000000..4b7ce4b --- /dev/null +++ b/languages/ja.js @@ -0,0 +1,122 @@ +/** + * Japanese number-to-words converter + * Uses the man (万) system for grouping by 10,000 + * @module languages/ja + */ + +const JA_DIGITS = Object.freeze(['', '一', '二', '三', '四', '五', '六', '七', '八', '九']); +const JA_SCALES = Object.freeze(['', '万', '億', '兆', '京', '垓', '𥝱', '穣', '溝']); + +/** Maximum supported value (10^36 - 1, up to 溝) */ +const MAX_VALUE = 10n ** 36n - 1n; + + +/** + * Convert a 4-digit group (0-9999) to Japanese + * @param {number} grp - The group value (0-9999) + * @returns {string} The Japanese representation + */ +const groupToJa = (grp) => { + if (grp === 0) return ''; + + const thousands = Math.floor(grp / 1000); + const hundreds = Math.floor((grp % 1000) / 100); + const tens = Math.floor((grp % 100) / 10); + const ones = grp % 10; + + let result = ''; + + // Thousands: 1 before 千 is omitted + if (thousands > 0) { + if (thousands === 1) { + result += '千'; + } else { + result += JA_DIGITS[thousands] + '千'; + } + } + + // Hundreds: 1 before 百 is omitted + if (hundreds > 0) { + if (hundreds === 1) { + result += '百'; + } else { + result += JA_DIGITS[hundreds] + '百'; + } + } + + // Tens: 1 before 十 is omitted + if (tens > 0) { + if (tens === 1) { + result += '十'; + } else { + result += JA_DIGITS[tens] + '十'; + } + } + + // Ones + if (ones > 0) { + result += JA_DIGITS[ones]; + } + + return result; +}; + +/** + * Convert a number to Japanese words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Japanese word representation + * + * @example + * japanese(42) // '四十二' + * japanese(1000) // '千' + * japanese(10000) // '一万' + */ +const japanese = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'ゼロ'; + + const str = num.toString(); + const len = str.length; + + // Split into groups of 4 from the right + const groups = []; + for (let i = len; i > 0; i -= 4) { + const start = Math.max(0, i - 4); + groups.unshift(str.slice(start, i)); + } + + let result = ''; + + for (let i = 0; i < groups.length; i++) { + const grp = parseInt(groups[i], 10); + const grpIdx = groups.length - 1 - i; + + if (grp === 0) continue; + + const grpStr = groupToJa(grp); + + // 1 before 万 and above IS included (handled naturally by groupToJa + // since grp=1 produces '一' for the ones digit in the group) + result += grpStr; + if (grpIdx > 0) { + result += JA_SCALES[grpIdx]; + } + } + + return result; +}; + +export default japanese; +export { japanese, JA_DIGITS, JA_SCALES, MAX_VALUE }; diff --git a/languages/ko.js b/languages/ko.js new file mode 100644 index 0000000..63bf8fc --- /dev/null +++ b/languages/ko.js @@ -0,0 +1,122 @@ +/** + * Korean number-to-words converter (Sino-Korean) + * Uses the man (만) system for grouping by 10,000 + * @module languages/ko + */ + +const KO_DIGITS = Object.freeze(['', '일', '이', '삼', '사', '오', '육', '칠', '팔', '구']); +const KO_SCALES = Object.freeze(['', '만', '억', '조', '경', '해', '자', '양', '구']); + +/** Maximum supported value (10^36 - 1) */ +const MAX_VALUE = 10n ** 36n - 1n; + + +/** + * Convert a 4-digit group (0-9999) to Sino-Korean + * @param {number} grp - The group value (0-9999) + * @returns {string} The Korean representation + */ +const groupToKo = (grp) => { + if (grp === 0) return ''; + + const thousands = Math.floor(grp / 1000); + const hundreds = Math.floor((grp % 1000) / 100); + const tens = Math.floor((grp % 100) / 10); + const ones = grp % 10; + + let result = ''; + + // Thousands: 1 before 천 is omitted + if (thousands > 0) { + if (thousands === 1) { + result += '천'; + } else { + result += KO_DIGITS[thousands] + '천'; + } + } + + // Hundreds: 1 before 백 is omitted + if (hundreds > 0) { + if (hundreds === 1) { + result += '백'; + } else { + result += KO_DIGITS[hundreds] + '백'; + } + } + + // Tens: 1 before 십 is omitted + if (tens > 0) { + if (tens === 1) { + result += '십'; + } else { + result += KO_DIGITS[tens] + '십'; + } + } + + // Ones + if (ones > 0) { + result += KO_DIGITS[ones]; + } + + return result; +}; + +/** + * Convert a number to Korean words (Sino-Korean system) + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Korean word representation + * + * @example + * korean(42) // '사십이' + * korean(1000) // '천' + * korean(10000) // '일만' + */ +const korean = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return '영'; + + const str = num.toString(); + const len = str.length; + + // Split into groups of 4 from the right + const groups = []; + for (let i = len; i > 0; i -= 4) { + const start = Math.max(0, i - 4); + groups.unshift(str.slice(start, i)); + } + + let result = ''; + + for (let i = 0; i < groups.length; i++) { + const grp = parseInt(groups[i], 10); + const grpIdx = groups.length - 1 - i; + + if (grp === 0) continue; + + const grpStr = groupToKo(grp); + + // 1 before 만 and above IS included (handled naturally by groupToKo + // since grp=1 produces '일' for the ones digit in the group) + result += grpStr; + if (grpIdx > 0) { + result += KO_SCALES[grpIdx]; + } + } + + return result; +}; + +export default korean; +export { korean, KO_DIGITS, KO_SCALES, MAX_VALUE }; diff --git a/languages/nl.js b/languages/nl.js new file mode 100644 index 0000000..8133702 --- /dev/null +++ b/languages/nl.js @@ -0,0 +1,112 @@ +/** + * Dutch number-to-words converter + * Dutch reverses ones and tens (eenentwintig = one-and-twenty) + * @module languages/nl + */ + +const NL_ONES = Object.freeze(['', 'een', 'twee', 'drie', 'vier', 'vijf', 'zes', 'zeven', 'acht', 'negen']); +const NL_TENS = Object.freeze(['', '', 'twintig', 'dertig', 'veertig', 'vijftig', 'zestig', 'zeventig', 'tachtig', 'negentig']); +const NL_TEENS = Object.freeze(['tien', 'elf', 'twaalf', 'dertien', 'veertien', 'vijftien', 'zestien', 'zeventien', 'achttien', 'negentien']); +const NL_ILLIONS = Object.freeze(['', 'duizend', 'miljoen', 'miljard', 'biljoen', 'biljard', 'triljoen', 'triljard', 'quadriljoen', 'quadriljard', 'quintiljoen', 'quintiljard']); +const NL_ILLIONS_PLURAL = Object.freeze(['', 'duizend', 'miljoen', 'miljard', 'biljoen', 'biljard', 'triljoen', 'triljard', 'quadriljoen', 'quadriljard', 'quintiljoen', 'quintiljard']); + +/** Maximum supported value (10^36 - 1, up to decillions) */ +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +const tenNl = (n) => { + if (n === 0) return ''; + if (n < 10) return NL_ONES[n]; + if (n < 20) return NL_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return NL_TENS[tensDigit]; + // Use "en" connector between ones and tens + // Use "ën" after words ending in vowel-e (twee, drie) + const connector = (onesDigit === 2 || onesDigit === 3) ? 'ën' : 'en'; + return `${NL_ONES[onesDigit]}${connector}${NL_TENS[tensDigit]}`; +}; + +const hundredNl = (n) => { + if (n < 100 || n >= 1000) return ''; + const h = Math.floor(n / 100); + // 100 = "honderd" (not "eenhonderd") + if (h === 1) return 'honderd'; + return `${NL_ONES[h]}honderd`; +}; + +/** + * Convert a number to Dutch words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Dutch word representation + * + * @example + * dutch(42) // 'tweeënveertig' + * dutch(1000) // 'duizend' + * dutch(21) // 'eenentwintig' + */ +const dutch = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'nul'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h > 0) { + if (i === 0) { + // Units group + if (h >= 100) s += hundredNl(h); + const t = tenment(num, i); + if (t > 0) s += tenNl(t); + } else if (i === 1) { + // Thousands: 1000 = "duizend" (not "eenduizend") + if (h === 1) { + s += 'duizend'; + } else { + if (h >= 100) s += hundredNl(h); + const t = tenment(num, i); + if (t > 0) s += tenNl(t); + s += 'duizend'; + } + } else { + // Millions and above: separated by spaces + // 1 million = "een miljoen", 2 million = "twee miljoen" + if (h >= 100) s += hundredNl(h); + const t = tenment(num, i); + if (t > 0) { + if (t === 1) { + s += 'een '; + } else { + s += tenNl(t) + ' '; + } + } else if (h < 100 && h === 1) { + s += 'een '; + } + const illionWord = h === 1 ? NL_ILLIONS[i] : NL_ILLIONS_PLURAL[i]; + s += `${illionWord} `; + } + } + } + + return s.trim(); +}; + +export default dutch; +export { dutch }; diff --git a/languages/no.js b/languages/no.js new file mode 100644 index 0000000..a9a10b2 --- /dev/null +++ b/languages/no.js @@ -0,0 +1,103 @@ +/** + * Norwegian (Bokmål) number-to-words converter + * Norwegian uses long scale like Danish but with simpler tens + * @module languages/no + */ + +const NO_ONES = Object.freeze(['', 'en', 'to', 'tre', 'fire', 'fem', 'seks', 'sju', 'åtte', 'ni']); +const NO_TENS = Object.freeze(['', '', 'tjue', 'tretti', 'førti', 'femti', 'seksti', 'sytti', 'åtti', 'nitti']); +const NO_TEENS = Object.freeze(['ti', 'elleve', 'tolv', 'tretten', 'fjorten', 'femten', 'seksten', 'sytten', 'atten', 'nitten']); +const NO_ILLIONS = Object.freeze(['', 'tusen', 'million', 'milliard', 'billion', 'billiard', 'trillion', 'trilliard', 'kvadrillion', 'kvadrilliard', 'kvintillion', 'kvintilliard']); +const NO_ILLIONS_PLURAL = Object.freeze(['', 'tusen', 'millioner', 'milliarder', 'billioner', 'billiarder', 'trillioner', 'trilliarder', 'kvadrillioner', 'kvadrilliarder', 'kvintillioner', 'kvintilliarder']); + +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +const tenNo = (n) => { + if (n === 0) return ''; + if (n < 10) return NO_ONES[n]; + if (n < 20) return NO_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return NO_TENS[tensDigit]; + return `${NO_TENS[tensDigit]}${NO_ONES[onesDigit]}`; +}; + +const hundredNo = (n) => { + if (n < 100 || n >= 1000) return ''; + const h = Math.floor(n / 100); + if (h === 1) return 'etthundre'; + return `${NO_ONES[h]}hundre`; +}; + +/** + * Convert a number to Norwegian words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Norwegian word representation + * + * @example + * norwegian(42) // 'førtito' + * norwegian(1000) // 'ettusen' + */ +const norwegian = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'null'; + if (num === 1n) return 'en'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h > 0) { + if (i === 0) { + if (h >= 100) s += hundredNo(h) + ' '; + const t = tenment(num, i); + if (t > 0) s += tenNo(t); + } else if (i === 1) { + if (h === 1) { + s += 'ettusen '; + } else { + if (h >= 100) s += hundredNo(h) + ' '; + const t = tenment(num, i); + if (t > 0) s += tenNo(t); + s += 'tusen '; + } + } else { + if (h >= 100) s += hundredNo(h) + ' '; + const t = tenment(num, i); + if (t > 0) { + if (t === 1) { + s += 'en '; + } else { + s += tenNo(t) + ' '; + } + } else if (h < 100 && h >= 1 && h === 1) { + s += 'en '; + } + const illionWord = h === 1 ? NO_ILLIONS[i] : NO_ILLIONS_PLURAL[i]; + s += `${illionWord} `; + } + } + } + + return s.trim(); +}; + +export default norwegian; +export { norwegian }; diff --git a/languages/pl.js b/languages/pl.js new file mode 100644 index 0000000..ec00f27 --- /dev/null +++ b/languages/pl.js @@ -0,0 +1,146 @@ +/** + * Polish number-to-words converter + * Handles Polish plural forms (singular, 2-4, 5+) + * @module languages/pl + */ + +const PL_ONES = Object.freeze(['', 'jeden', 'dwa', 'trzy', 'cztery', 'pięć', 'sześć', 'siedem', 'osiem', 'dziewięć']); +const PL_TENS = Object.freeze(['', '', 'dwadzieścia', 'trzydzieści', 'czterdzieści', 'pięćdziesiąt', 'sześćdziesiąt', 'siedemdziesiąt', 'osiemdziesiąt', 'dziewięćdziesiąt']); +const PL_TEENS = Object.freeze(['dziesięć', 'jedenaście', 'dwanaście', 'trzynaście', 'czternaście', 'piętnaście', 'szesnaście', 'siedemnaście', 'osiemnaście', 'dziewiętnaście']); +const PL_HUNDREDS = Object.freeze(['', 'sto', 'dwieście', 'trzysta', 'czterysta', 'pięćset', 'sześćset', 'siedemset', 'osiemset', 'dziewięćset']); +const PL_ILLIONS = Object.freeze([ + ['', '', ''], // ones + ['tysiąc', 'tysiące', 'tysięcy'], // thousands + ['milion', 'miliony', 'milionów'], // millions + ['miliard', 'miliardy', 'miliardów'], // billions + ['bilion', 'biliony', 'bilionów'], // trillions + ['biliard', 'biliardy', 'biliardów'], // quadrillions + ['trylion', 'tryliony', 'trylionów'], // quintillions + ['tryliard', 'tryliardy', 'tryliardów'], // sextillions + ['kwadrylion', 'kwadryliony', 'kwadrylionów'], // septillions + ['kwadryliard', 'kwadryliardy', 'kwadryliardów'], // octillions + ['kwintylion', 'kwintyliony', 'kwintylionów'], // nonillions + ['kwintyliard', 'kwintyliardy', 'kwintyliardów'] // decillions +]); + +/** Maximum supported value (10^36 - 1, up to decillions) */ +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +/** + * Get the correct Polish plural form based on number + * Polish has 3 forms: singular (1), plural 2-4, plural 5+ + * @param {number} n - The number to check + * @param {string[]} forms - [singular, plural2_4, plural5_plus] + * @returns {string} The correct plural form + */ +const getPlPlural = (n, forms) => { + if (n === 1) return forms[0]; + const lastTwo = n % 100; + const lastOne = n % 10; + if (lastTwo >= 12 && lastTwo <= 14) return forms[2]; + if (lastOne >= 2 && lastOne <= 4) return forms[1]; + return forms[2]; +}; + +const hundredPl = (n) => { + if (n < 100 || n >= 1000) return ''; + return PL_HUNDREDS[Math.floor(n / 100)]; +}; + +const tenPl = (n) => { + if (n === 0) return ''; + if (n < 10) return PL_ONES[n]; + if (n < 20) return PL_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return PL_TENS[tensDigit]; + return `${PL_TENS[tensDigit]} ${PL_ONES[onesDigit]}`; +}; + +/** + * Convert a number to Polish words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Polish word representation + * + * @example + * polish(42) // 'czterdzieści dwa' + * polish(1000) // 'tysiąc' + * polish(2000) // 'dwa tysiące' + * polish(5000) // 'pięć tysięcy' + */ +const polish = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'zero'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h > 0) { + const hund = hundredPl(h); + if (hund) s += hund + ' '; + + const t = tenment(num, i); + + if (i === 0) { + // Units group: just tens + const tenWord = tenPl(t); + if (tenWord) s += tenWord + ' '; + } else if (i === 1) { + // Thousands + if (h === 1 && t === 0) { + // Just "tysiąc" for exactly 1 thousand (with possible hundreds) + s += 'tysiąc '; + } else { + if (t > 0) { + // Skip "jeden" before "tysiąc" when it's just 1 + if (t === 1) { + s += 'tysiąc '; + } else { + s += tenPl(t) + ' '; + s += getPlPlural(t, PL_ILLIONS[i]) + ' '; + } + } else { + // h >= 100 with no tens portion, use h for plural + s += getPlPlural(h, PL_ILLIONS[i]) + ' '; + } + } + } else { + // Millions and above + if (t > 0) { + if (t === 1) { + s += 'jeden '; + } else { + s += tenPl(t) + ' '; + } + } else if (h < 100 && h === 1) { + s += 'jeden '; + } + const illionWord = getPlPlural(t || h, PL_ILLIONS[i]); + s += illionWord + ' '; + } + } + } + + return s.trim().replace(/\s+/g, ' '); +}; + +export default polish; +export { polish }; diff --git a/languages/sv.js b/languages/sv.js new file mode 100644 index 0000000..d75d582 --- /dev/null +++ b/languages/sv.js @@ -0,0 +1,103 @@ +/** + * Swedish number-to-words converter + * Swedish uses long scale (miljon, miljard, biljon, etc.) + * @module languages/sv + */ + +const SV_ONES = Object.freeze(['', 'en', 'två', 'tre', 'fyra', 'fem', 'sex', 'sju', 'åtta', 'nio']); +const SV_TENS = Object.freeze(['', '', 'tjugo', 'trettio', 'fyrtio', 'femtio', 'sextio', 'sjuttio', 'åttio', 'nittio']); +const SV_TEENS = Object.freeze(['tio', 'elva', 'tolv', 'tretton', 'fjorton', 'femton', 'sexton', 'sjutton', 'arton', 'nitton']); +const SV_ILLIONS = Object.freeze(['', 'tusen', 'miljon', 'miljard', 'biljon', 'biljard', 'triljon', 'triljard', 'kvadriljon', 'kvadriljard', 'kvintiljon', 'kvintiljard']); +const SV_ILLIONS_PLURAL = Object.freeze(['', 'tusen', 'miljoner', 'miljarder', 'biljoner', 'biljarder', 'triljoner', 'triljarder', 'kvadriljoner', 'kvadriljarder', 'kvintiljoner', 'kvintiljarder']); + +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +const tenSv = (n) => { + if (n === 0) return ''; + if (n < 10) return SV_ONES[n]; + if (n < 20) return SV_TEENS[n - 10]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return SV_TENS[tensDigit]; + return `${SV_TENS[tensDigit]}${SV_ONES[onesDigit]}`; +}; + +const hundredSv = (n) => { + if (n < 100 || n >= 1000) return ''; + const h = Math.floor(n / 100); + if (h === 1) return 'etthundra'; + return `${SV_ONES[h]}hundra`; +}; + +/** + * Convert a number to Swedish words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Swedish word representation + * + * @example + * swedish(42) // 'fyrtiotvå' + * swedish(1000) // 'ettusen' + */ +const swedish = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'noll'; + if (num === 1n) return 'en'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h > 0) { + if (i === 0) { + if (h >= 100) s += hundredSv(h) + ' '; + const t = tenment(num, i); + if (t > 0) s += tenSv(t); + } else if (i === 1) { + if (h === 1) { + s += 'ettusen '; + } else { + if (h >= 100) s += hundredSv(h) + ' '; + const t = tenment(num, i); + if (t > 0) s += tenSv(t); + s += 'tusen '; + } + } else { + if (h >= 100) s += hundredSv(h) + ' '; + const t = tenment(num, i); + if (t > 0) { + if (t === 1) { + s += 'en '; + } else { + s += tenSv(t) + ' '; + } + } else if (h < 100 && h >= 1 && h === 1) { + s += 'en '; + } + const illionWord = h === 1 ? SV_ILLIONS[i] : SV_ILLIONS_PLURAL[i]; + s += `${illionWord} `; + } + } + } + + return s.trim(); +}; + +export default swedish; +export { swedish }; diff --git a/languages/th.js b/languages/th.js new file mode 100644 index 0000000..1875ce7 --- /dev/null +++ b/languages/th.js @@ -0,0 +1,123 @@ +/** + * Thai number-to-words converter + * Uses the lan (ล้าน) system for grouping by 1,000,000 + * @module languages/th + */ + +const TH_ONES = Object.freeze(['', 'หนึ่ง', 'สอง', 'สาม', 'สี่', 'ห้า', 'หก', 'เจ็ด', 'แปด', 'เก้า']); +const TH_POSITIONS = Object.freeze(['', 'สิบ', 'ร้อย', 'พัน', 'หมื่น', 'แสน']); + +/** Maximum supported value (10^36 - 1) */ +const MAX_VALUE = 10n ** 36n - 1n; + + +/** + * Convert a group of up to 6 digits (0-999999) to Thai + * @param {number} grp - The group value (0-999999) + * @returns {string} The Thai representation + */ +const groupToTh = (grp) => { + if (grp === 0) return ''; + + let result = ''; + const digits = []; + + // Extract digits from right to left (position 0 = ones, 5 = hundred-thousands) + let temp = grp; + for (let i = 0; i < 6; i++) { + digits[i] = temp % 10; + temp = Math.floor(temp / 10); + } + + // Process from highest position to lowest + for (let pos = 5; pos >= 0; pos--) { + const d = digits[pos]; + if (d === 0) continue; + + if (pos === 0) { + // Ones place: use เอ็ด when tens digit or higher exists + if (d === 1 && grp > 1) { + result += 'เอ็ด'; + } else { + result += TH_ONES[d]; + } + } else if (pos === 1) { + // Tens place + if (d === 1) { + // 1 in tens place: just สิบ, not หนึ่งสิบ + result += 'สิบ'; + } else if (d === 2) { + // 2 in tens place: ยี่สิบ, not สองสิบ + result += 'ยี่สิบ'; + } else { + result += TH_ONES[d] + 'สิบ'; + } + } else { + // Hundreds, thousands, ten-thousands, hundred-thousands + result += TH_ONES[d] + TH_POSITIONS[pos]; + } + } + + return result; +}; + +/** + * Convert a number to Thai words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Thai word representation + * + * @example + * thai(42) // 'สี่สิบสอง' + * thai(11) // 'สิบเอ็ด' + * thai(1000000) // 'หนึ่งล้าน' + */ +const thai = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'ศูนย์'; + + const str = num.toString(); + const len = str.length; + + // Split into groups of 6 from the right (ล้าน = million grouping) + const groups = []; + for (let i = len; i > 0; i -= 6) { + const start = Math.max(0, i - 6); + groups.unshift(str.slice(start, i)); + } + + let result = ''; + + for (let i = 0; i < groups.length; i++) { + const grp = parseInt(groups[i], 10); + const grpIdx = groups.length - 1 - i; + + if (grp === 0) continue; + + const grpStr = groupToTh(grp); + result += grpStr; + + // Append ล้าน scale for each group above the lowest + if (grpIdx > 0) { + for (let j = 0; j < grpIdx; j++) { + result += 'ล้าน'; + } + } + } + + return result; +}; + +export default thai; +export { thai, TH_ONES, TH_POSITIONS, MAX_VALUE }; diff --git a/languages/tr.js b/languages/tr.js new file mode 100644 index 0000000..4a0767b --- /dev/null +++ b/languages/tr.js @@ -0,0 +1,102 @@ +/** + * Turkish number-to-words converter + * Turkish is very regular with simple concatenation rules + * @module languages/tr + */ + +const TR_ONES = Object.freeze(['', 'bir', 'iki', 'üç', 'dört', 'beş', 'altı', 'yedi', 'sekiz', 'dokuz']); +const TR_TENS = Object.freeze(['', 'on', 'yirmi', 'otuz', 'kırk', 'elli', 'altmış', 'yetmiş', 'seksen', 'doksan']); +const TR_ILLIONS = Object.freeze(['', 'bin', 'milyon', 'milyar', 'trilyon', 'katrilyon', 'kentilyon', 'sekstilyon', 'septilyon', 'oktilyon', 'nonilyon', 'desilyon']); + +/** Maximum supported value (10^36 - 1, up to decillions) */ +const MAX_VALUE = 10n ** 36n - 1n; + +const group = (n) => Math.ceil(n.toString().length / 3) - 1; +const power = (g) => 10n ** BigInt(g * 3); +const segment = (n, g) => n % power(g + 1); +const hundment = (n, g) => Number(segment(n, g) / power(g)); +const tenment = (n, g) => hundment(n, g) % 100; + +const hundredTr = (n) => { + if (n < 100 || n >= 1000) return ''; + const h = Math.floor(n / 100); + // 1 before yüz is omitted: just "yüz", not "bir yüz" + if (h === 1) return 'yüz'; + return `${TR_ONES[h]} yüz`; +}; + +const tenTr = (n) => { + if (n === 0) return ''; + if (n < 10) return TR_ONES[n]; + const onesDigit = n % 10; + const tensDigit = Math.floor(n / 10); + if (onesDigit === 0) return TR_TENS[tensDigit]; + return `${TR_TENS[tensDigit]} ${TR_ONES[onesDigit]}`; +}; + +/** + * Convert a number to Turkish words + * @param {number|bigint} n - The number to convert + * @returns {string|false} The Turkish word representation + * + * @example + * turkish(42) // 'kırk iki' + * turkish(1000) // 'bin' + * turkish(1000000) // 'bir milyon' + */ +const turkish = (n) => { + let num; + + if (typeof n === 'bigint') { + if (n < 0n || n > MAX_VALUE) return false; + num = n; + } else if (typeof n === 'number') { + if (isNaN(n) || n < 0 || !Number.isInteger(n)) return false; + if (n > Number.MAX_SAFE_INTEGER) return false; + num = BigInt(n); + } else { + return false; + } + + if (num === 0n) return 'sıfır'; + + let s = ''; + for (let i = group(num); i >= 0; i--) { + const h = hundment(num, i); + if (h === 0) continue; + + if (i === 0) { + // Ones group: no scale word + const hund = hundredTr(h); + if (hund) s += hund + ' '; + const t = tenment(num, i); + const tenWord = tenTr(t); + if (tenWord) s += tenWord + ' '; + } else if (i === 1) { + // Thousands: 1 before bin is omitted + if (h === 1) { + s += 'bin '; + } else { + const hund = hundredTr(h); + if (hund) s += hund + ' '; + const t = tenment(num, i); + const tenWord = tenTr(t); + if (tenWord) s += tenWord + ' '; + s += 'bin '; + } + } else { + // Millions and above: 1 IS included ("bir milyon") + const hund = hundredTr(h); + if (hund) s += hund + ' '; + const t = tenment(num, i); + const tenWord = tenTr(t); + if (tenWord) s += tenWord + ' '; + s += `${TR_ILLIONS[i]} `; + } + } + + return s.trim(); +}; + +export default turkish; +export { turkish, TR_ONES, TR_TENS, TR_ILLIONS, MAX_VALUE }; diff --git a/package.json b/package.json index 073683c..8654a0a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "numberstring", - "version": "1.0.0", + "version": "1.0.1", "description": "Number One Way to Makes Words from Numbers", "type": "module", "main": "index.js", diff --git a/test/index.test.js b/test/index.test.js index c07a89b..2c8f30a 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import numberstring, { comma, group, ordinal, decimal, currency, roman, parse, negative, fraction, year, telephone, percent, spanish, french, german, danish, chinese, hindi, russian, portuguese, toWords } from '../index.js'; +import numberstring, { comma, group, ordinal, decimal, currency, roman, parse, negative, fraction, year, telephone, percent, spanish, french, german, danish, chinese, hindi, russian, portuguese, japanese, korean, arabic, italian, dutch, turkish, polish, swedish, indonesian, thai, norwegian, finnish, icelandic, toWords } from '../index.js'; describe('numberstring', () => { describe('basic conversions', () => { @@ -1097,3 +1097,338 @@ describe('group', () => { expect(group(1000000000000000)).toBe(5); }); }); + +// ============================================================================ +// NEW LANGUAGE TESTS +// ============================================================================ + +describe('japanese', () => { + it('converts basic numbers', () => { + expect(japanese(0)).toBe('ゼロ'); + expect(japanese(1)).toBe('一'); + expect(japanese(10)).toBe('十'); + expect(japanese(42)).toBe('四十二'); + expect(japanese(100)).toBe('百'); + expect(japanese(1000)).toBe('千'); + }); + + it('converts large numbers', () => { + expect(japanese(10000)).toBe('一万'); + expect(japanese(12345)).toBe('一万二千三百四十五'); + expect(japanese(100000000)).toBe('一億'); + expect(japanese(1000000000000)).toBe('一兆'); + }); + + it('returns false for invalid input', () => { + expect(japanese(-1)).toBe(false); + expect(japanese(NaN)).toBe(false); + expect(japanese('abc')).toBe(false); + }); +}); + +describe('korean', () => { + it('converts basic numbers', () => { + expect(korean(0)).toBe('영'); + expect(korean(1)).toBe('일'); + expect(korean(10)).toBe('십'); + expect(korean(42)).toBe('사십이'); + expect(korean(100)).toBe('백'); + expect(korean(1000)).toBe('천'); + }); + + it('converts large numbers', () => { + expect(korean(10000)).toBe('일만'); + expect(korean(12345)).toBe('일만이천삼백사십오'); + expect(korean(100000000)).toBe('일억'); + }); + + it('returns false for invalid input', () => { + expect(korean(-1)).toBe(false); + expect(korean(NaN)).toBe(false); + }); +}); + +describe('arabic', () => { + it('converts basic numbers', () => { + expect(arabic(0)).toBe('صفر'); + expect(arabic(1)).toBe('واحد'); + expect(arabic(2)).toBe('اثنان'); + expect(arabic(10)).toBe('عشرة'); + expect(arabic(11)).toBe('أحد عشر'); + }); + + it('converts tens with ones (ones before tens)', () => { + expect(arabic(21)).toContain('واحد'); + expect(arabic(21)).toContain('عشرون'); + expect(arabic(42)).toContain('اثنان'); + expect(arabic(42)).toContain('أربعون'); + }); + + it('converts thousands', () => { + expect(arabic(1000)).toBe('ألف'); + expect(arabic(2000)).toBe('ألفان'); + }); + + it('returns false for invalid input', () => { + expect(arabic(-1)).toBe(false); + expect(arabic(NaN)).toBe(false); + }); +}); + +describe('italian', () => { + it('converts basic numbers', () => { + expect(italian(0)).toBe('zero'); + expect(italian(1)).toBe('uno'); + expect(italian(10)).toBe('dieci'); + expect(italian(11)).toBe('undici'); + expect(italian(42)).toBe('quarantadue'); + }); + + it('handles elision rules', () => { + expect(italian(21)).toBe('ventuno'); + expect(italian(28)).toBe('ventotto'); + expect(italian(23)).toBe('ventitré'); + }); + + it('converts thousands', () => { + expect(italian(1000)).toBe('mille'); + expect(italian(2000)).toBe('duemila'); + }); + + it('converts millions', () => { + expect(italian(1000000)).toBe('un milione'); + expect(italian(2000000)).toBe('due milioni'); + }); + + it('returns false for invalid input', () => { + expect(italian(-1)).toBe(false); + expect(italian(NaN)).toBe(false); + }); +}); + +describe('dutch', () => { + it('converts basic numbers', () => { + expect(dutch(0)).toBe('nul'); + expect(dutch(1)).toBe('een'); + expect(dutch(10)).toBe('tien'); + expect(dutch(12)).toBe('twaalf'); + }); + + it('reverses ones and tens', () => { + expect(dutch(21)).toBe('eenentwintig'); + expect(dutch(42)).toBe('tweeënveertig'); + expect(dutch(99)).toBe('negenennegent\u0069g'); + }); + + it('converts thousands', () => { + expect(dutch(1000)).toBe('duizend'); + expect(dutch(2000)).toBe('tweeduizend'); + }); + + it('returns false for invalid input', () => { + expect(dutch(-1)).toBe(false); + expect(dutch(NaN)).toBe(false); + }); +}); + +describe('turkish', () => { + it('converts basic numbers', () => { + expect(turkish(0)).toBe('sıfır'); + expect(turkish(1)).toBe('bir'); + expect(turkish(42)).toBe('kırk iki'); + expect(turkish(100)).toBe('yüz'); + }); + + it('omits bir before yüz and bin', () => { + expect(turkish(100)).toBe('yüz'); + expect(turkish(1000)).toBe('bin'); + }); + + it('includes bir before milyon', () => { + expect(turkish(1000000)).toBe('bir milyon'); + }); + + it('returns false for invalid input', () => { + expect(turkish(-1)).toBe(false); + expect(turkish(NaN)).toBe(false); + }); +}); + +describe('polish', () => { + it('converts basic numbers', () => { + expect(polish(0)).toBe('zero'); + expect(polish(1)).toBe('jeden'); + expect(polish(42)).toBe('czterdzieści dwa'); + }); + + it('handles plural forms', () => { + expect(polish(1000)).toBe('tysiąc'); + expect(polish(2000)).toContain('tysiące'); + expect(polish(5000)).toContain('tysięcy'); + }); + + it('returns false for invalid input', () => { + expect(polish(-1)).toBe(false); + expect(polish(NaN)).toBe(false); + }); +}); + +describe('swedish', () => { + it('converts basic numbers', () => { + expect(swedish(0)).toBe('noll'); + expect(swedish(1)).toBe('en'); + expect(swedish(42)).toBe('fyrtiotvå'); + }); + + it('converts thousands', () => { + expect(swedish(1000)).toBe('ettusen'); + expect(swedish(2000)).toContain('tusen'); + }); + + it('returns false for invalid input', () => { + expect(swedish(-1)).toBe(false); + expect(swedish(NaN)).toBe(false); + }); +}); + +describe('indonesian', () => { + it('converts basic numbers', () => { + expect(indonesian(0)).toBe('nol'); + expect(indonesian(1)).toBe('satu'); + expect(indonesian(11)).toBe('sebelas'); + expect(indonesian(42)).toBe('empat puluh dua'); + }); + + it('uses se- prefix', () => { + expect(indonesian(100)).toBe('seratus'); + expect(indonesian(1000)).toBe('seribu'); + }); + + it('uses satu for million', () => { + expect(indonesian(1000000)).toBe('satu juta'); + }); + + it('returns false for invalid input', () => { + expect(indonesian(-1)).toBe(false); + expect(indonesian(NaN)).toBe(false); + }); +}); + +describe('thai', () => { + it('converts basic numbers', () => { + expect(thai(0)).toBe('ศูนย์'); + expect(thai(1)).toBe('หนึ่ง'); + expect(thai(11)).toBe('สิบเอ็ด'); + expect(thai(20)).toBe('ยี่สิบ'); + expect(thai(21)).toBe('ยี่สิบเอ็ด'); + expect(thai(42)).toBe('สี่สิบสอง'); + }); + + it('converts thousands', () => { + expect(thai(1000)).toBe('หนึ่งพัน'); + expect(thai(10000)).toBe('หนึ่งหมื่น'); + expect(thai(100000)).toBe('หนึ่งแสน'); + }); + + it('converts millions', () => { + expect(thai(1000000)).toBe('หนึ่งล้าน'); + }); + + it('returns false for invalid input', () => { + expect(thai(-1)).toBe(false); + expect(thai(NaN)).toBe(false); + }); +}); + +describe('norwegian', () => { + it('converts basic numbers', () => { + expect(norwegian(0)).toBe('null'); + expect(norwegian(1)).toBe('en'); + expect(norwegian(42)).toBe('førtito'); + }); + + it('converts thousands', () => { + expect(norwegian(1000)).toBe('ettusen'); + }); + + it('returns false for invalid input', () => { + expect(norwegian(-1)).toBe(false); + expect(norwegian(NaN)).toBe(false); + }); +}); + +describe('finnish', () => { + it('converts basic numbers', () => { + expect(finnish(0)).toBe('nolla'); + expect(finnish(1)).toBe('yksi'); + expect(finnish(42)).toBe('neljäkymmentäkaksi'); + }); + + it('converts hundreds and thousands', () => { + expect(finnish(100)).toBe('sata'); + expect(finnish(200)).toContain('sataa'); + expect(finnish(1000)).toBe('tuhat'); + expect(finnish(2000)).toContain('tuhatta'); + }); + + it('returns false for invalid input', () => { + expect(finnish(-1)).toBe(false); + expect(finnish(NaN)).toBe(false); + }); +}); + +describe('icelandic', () => { + it('converts basic numbers', () => { + expect(icelandic(0)).toBe('núll'); + expect(icelandic(1)).toBe('einn'); + expect(icelandic(42)).toBe('fjörutíu og tveir'); + }); + + it('converts thousands', () => { + expect(icelandic(1000)).toBe('eitt þúsund'); + }); + + it('returns false for invalid input', () => { + expect(icelandic(-1)).toBe(false); + expect(icelandic(NaN)).toBe(false); + }); +}); + +describe('toWords with new languages', () => { + it('converts using language codes', () => { + expect(toWords(42, { lang: 'ja' })).toBe('四十二'); + expect(toWords(42, { lang: 'ko' })).toBe('사십이'); + expect(toWords(42, { lang: 'it' })).toBe('quarantadue'); + expect(toWords(42, { lang: 'nl' })).toBe('tweeënveertig'); + expect(toWords(42, { lang: 'tr' })).toBe('kırk iki'); + expect(toWords(42, { lang: 'pl' })).toBe('czterdzieści dwa'); + expect(toWords(42, { lang: 'sv' })).toBe('fyrtiotvå'); + expect(toWords(42, { lang: 'id' })).toBe('empat puluh dua'); + expect(toWords(42, { lang: 'no' })).toBe('førtito'); + expect(toWords(42, { lang: 'fi' })).toBe('neljäkymmentäkaksi'); + expect(toWords(42, { lang: 'is' })).toBe('fjörutíu og tveir'); + }); + + it('converts using language names', () => { + expect(toWords(42, { lang: 'japanese' })).toBe('四十二'); + expect(toWords(42, { lang: 'korean' })).toBe('사십이'); + expect(toWords(42, { lang: 'italian' })).toBe('quarantadue'); + expect(toWords(42, { lang: 'dutch' })).toBe('tweeënveertig'); + expect(toWords(42, { lang: 'turkish' })).toBe('kırk iki'); + expect(toWords(42, { lang: 'polish' })).toBe('czterdzieści dwa'); + expect(toWords(42, { lang: 'swedish' })).toBe('fyrtiotvå'); + expect(toWords(42, { lang: 'indonesian' })).toBe('empat puluh dua'); + expect(toWords(42, { lang: 'norwegian' })).toBe('førtito'); + expect(toWords(42, { lang: 'finnish' })).toBe('neljäkymmentäkaksi'); + expect(toWords(42, { lang: 'icelandic' })).toBe('fjörutíu og tveir'); + }); + + it('converts using native language names', () => { + expect(toWords(42, { lang: '日本語' })).toBe('四十二'); + expect(toWords(42, { lang: '한국어' })).toBe('사십이'); + expect(toWords(42, { lang: 'italiano' })).toBe('quarantadue'); + expect(toWords(42, { lang: 'türkçe' })).toBe('kırk iki'); + expect(toWords(42, { lang: 'suomi' })).toBe('neljäkymmentäkaksi'); + expect(toWords(42, { lang: 'íslenska' })).toBe('fjörutíu og tveir'); + }); +});