use crate::Result;
use crate::currency::vocab::SW as CURRENCY_VOCAB;
use crate::currency::{Currency, assert_minor_unit};
use crate::language::{Kind, Language, form};
use crate::max::Max;
use crate::number::{
Number, digits_to_biguint, parse_cardinal, parse_currency, parse_ordinal, split_groups,
};
use crate::options::options;
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
const ONES: [&str; 10] = [
"sifuri", "moja", "mbili", "tatu", "nne", "tano", "sita", "saba", "nane", "tisa",
];
const TENS: [&str; 10] = [
"",
"kumi",
"ishirini",
"thelathini",
"arobaini",
"hamsini",
"sitini",
"sabini",
"themanini",
"tisini",
];
const SCALE_WORDS: &[&str] = &[
"",
"elfu",
"milioni",
"bilioni",
"trilioni",
"kwadrilioni",
"kwintilioni",
];
pub const CARDINAL_MAX: Max = Max::western(SCALE_WORDS.len() as u32 - 1);
pub const ORDINAL_MAX: Max = Max::western(SCALE_WORDS.len() as u32 - 1);
pub const CURRENCY_MAX: Max = Max::western(SCALE_WORDS.len() as u32 - 1);
const ZERO: &str = "sifuri";
const NEGATIVE: &str = "minus";
const DECIMAL_SEP: &str = "nukta";
const ORDINAL_ONES: [&str; 10] = [
"",
"wa kwanza",
"wa pili",
"wa tatu",
"wa nne",
"wa tano",
"wa sita",
"wa saba",
"wa nane",
"wa tisa",
];
const ORDINAL_PREFIX: &str = "wa";
fn words_under_100(n: u32) -> String {
if n < 10 {
return ONES[n as usize].to_string();
}
if n == 10 {
return TENS[1].to_string();
}
if n < 20 {
return format!("{} na {}", TENS[1], ONES[(n - 10) as usize]);
}
let tens = (n / 10) as usize;
let ones = (n % 10) as usize;
if ones == 0 {
return TENS[tens].to_string();
}
format!("{} na {}", TENS[tens], ONES[ones])
}
fn words_under_1000(n: u32) -> String {
if n < 100 {
return words_under_100(n);
}
if n == 100 {
return "mia moja".to_string();
}
let hundreds = (n / 100) as usize;
let rest = n % 100;
let mut result = format!("mia {}", ONES[hundreds]);
if rest > 0 {
if rest < 10 {
result.push_str(" na ");
result.push_str(ONES[rest as usize]);
} else {
result.push(' ');
result.push_str(&words_under_100(rest));
}
}
result
}
fn integer_to_words(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
let segments = split_groups(n, 1000);
let mut parts: Vec<String> = Vec::new();
for (scale_index, &val) in segments.iter().enumerate().rev() {
if val == 0 {
continue;
}
if scale_index == 0 {
if val < 10 && !parts.is_empty() {
parts.push(format!("na {}", ONES[val as usize]));
} else if val == 100 && !parts.is_empty() {
parts.push("mia".to_string());
} else {
parts.push(words_under_1000(val));
}
} else {
let unit = if val == 1 {
ONES[1].to_string()
} else {
words_under_1000(val)
};
parts.push(format!("{} {unit}", SCALE_WORDS[scale_index]));
}
}
parts.join(" ")
}
fn decimal_part_to_words(decimal: &str) -> String {
let mut result = String::new();
let significant = decimal.trim_start_matches('0');
for _ in 0..decimal.len() - significant.len() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(ZERO);
}
if !significant.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(&integer_to_words(&digits_to_biguint(significant)));
}
result
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let number = parse_cardinal(&value.into())?;
CARDINAL_MAX.check(&number.integer, number.fraction.as_deref())?;
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&number.integer));
if let Some(fraction) = &number.fraction {
result.push(' ');
result.push_str(DECIMAL_SEP);
result.push(' ');
result.push_str(&decimal_part_to_words(fraction));
}
Ok(result)
}
fn integer_to_ordinal(n: &BigUint) -> String {
if let Some(small) = n.to_u64() {
if (1..=9).contains(&small) {
return ORDINAL_ONES[small as usize].to_string();
}
}
format!("{ORDINAL_PREFIX} {}", integer_to_words(n))
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
Ok(integer_to_ordinal(&n))
}
options! {
pub struct CurrencyOptions {
currency: Currency = Currency::KES, table = CURRENCY_VOCAB;
}
}
pub fn to_currency<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_currency_with(value, &CurrencyOptions::default())
}
pub fn to_currency_with<'a>(
value: impl Into<Number<'a>>,
options: &CurrencyOptions,
) -> Result<String> {
let words = CURRENCY_VOCAB.words(options.currency)?;
let money = parse_currency(&value.into(), options.currency.minor_unit_digits())?;
CURRENCY_MAX.check(&money.major, None)?;
assert_minor_unit(money.minor, options.currency)?;
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
if !money.major.is_zero() || money.minor == 0 {
result.push_str(words.major_at(0));
result.push(' ');
result.push_str(&integer_to_words(&money.major));
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push_str(" na ");
}
result.push_str(words.minor_at(0));
result.push(' ');
result.push_str(&integer_to_words(&BigUint::from(money.minor)));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "sw-KE",
name: "Swahili (Kenya)",
kind: Kind::Full,
cardinal: Some(form!(plain to_cardinal, CARDINAL_MAX)),
ordinal: Some(form!(plain to_ordinal, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};