use crate::Result;
use crate::currency::vocab::YO as CURRENCY_VOCAB;
use crate::currency::{Currency, assert_minor_unit};
use crate::language::{Kind, Language, form};
use crate::max::Max;
use crate::number::{Number, parse_cardinal, parse_currency, parse_ordinal};
use crate::options::options;
use num_bigint::BigUint;
use num_traits::{One, ToPrimitive, Zero};
pub const CARDINAL_MAX: Max = Max::Unbounded;
pub const ORDINAL_MAX: Max = Max::Unbounded;
pub const CURRENCY_MAX: Max = Max::Unbounded;
const ONES: [&str; 11] = [
"",
"ọ̀kan", "èjì", "ẹ̀ta", "ẹ̀rin", "àrùn", "ẹ̀fà", "èje", "ẹ̀jọ", "ẹ̀sán", "ẹ̀wá", ];
const TEENS_ADD: [&str; 5] = [
"",
"ọ̀kànlá", "èjìlá", "ẹ̀talá", "ẹ̀rinlá", ];
const TEENS_SUB: [&str; 5] = [
"àrùndínlógún", "ẹ̀rìndínlógún", "ẹ̀tadínlógún", "èjìdínlógún", "ọ̀kàndínlógún", ];
const DECADES: [&str; 11] = [
"",
"",
"ogún", "ọgbọ̀n", "ogójì", "àádọ́ta", "ogóta", "àádọ́rin", "ogórin", "àádọ́rùn", "ọgọ́rùn", ];
const DECADE_ADD_SUFFIX: [&str; 11] = [
"",
"",
"lógún",
"lọgbọ̀n",
"lógójì",
"láàádọ́ta",
"lógóta",
"láàádọ́rin",
"lógórin",
"láàádọ́rùn",
"lọ́gọ́rùn",
];
const DECADE_SUB_SUFFIX: [&str; 11] = [
"",
"",
"dínlógún",
"dínlọgbọ̀n",
"dínlógójì",
"dínláàádọ́ta",
"dínlógóta",
"dínláàádọ́rin",
"dínlógórin",
"dínláàádọ́rùn",
"dínlọ́gọ́rùn",
];
const HUNDRED: &str = "ọgọ́rùn";
const TWO_HUNDRED: &str = "igba";
const FOUR_HUNDRED: &str = "irinwó";
const THOUSAND: &str = "ẹgbẹ̀rún";
const TEN_THOUSAND: &str = "ẹgbàárùn";
const TWENTY_THOUSAND: &str = "ọ̀kẹ́";
const MILLION: &str = "mílíọ̀nù";
const ZERO: &str = "òdo";
const NEGATIVE: &str = "àìní";
const DECIMAL_SEP: &str = "àmì";
const AND: &str = "ó lé";
const ORDINAL_FIRST: &str = "àkọ́kọ́";
const ORDINAL_SECOND: &str = "ìkejì";
const ORDINAL_PREFIX: &str = "ìkẹ";
fn build_under_100(n: u32) -> String {
let n = n as usize;
if n == 0 {
return String::new();
}
if n <= 10 {
return ONES[n].to_string();
}
if n <= 14 {
return TEENS_ADD[n - 10].to_string();
}
if n <= 19 {
return TEENS_SUB[n - 15].to_string();
}
if n % 10 == 0 {
return DECADES[n / 10].to_string();
}
let decade = n / 10;
let unit = n % 10;
if unit <= 4 {
return format!("{} lé {}", ONES[unit], DECADE_ADD_SUFFIX[decade]);
}
format!("{}{}", ONES[10 - unit], DECADE_SUB_SUFFIX[decade + 1])
}
fn convert_hundreds(n: u32) -> String {
if n < 100 {
return build_under_100(n);
}
let hundreds = (n / 100) as usize;
let remainder = n % 100;
if hundreds == 2 && remainder == 0 {
return TWO_HUNDRED.to_string();
}
if hundreds == 4 && remainder == 0 {
return FOUR_HUNDRED.to_string();
}
let mut result = match hundreds {
1 => HUNDRED.to_string(),
2 => TWO_HUNDRED.to_string(),
4 => FOUR_HUNDRED.to_string(),
_ => format!("{} {HUNDRED}", ONES[hundreds]),
};
if remainder > 0 {
result.push(' ');
result.push_str(AND);
result.push(' ');
result.push_str(&build_under_100(remainder));
}
result
}
fn integer_to_words(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1000 {
return convert_hundreds(small as u32);
}
}
let mut parts: Vec<String> = Vec::new();
let million = BigUint::from(1_000_000u32);
let below_million = if *n >= million {
let millions = n / &million;
if millions.is_one() {
parts.push(format!("{MILLION} kan"));
} else {
parts.push(format!("{MILLION} {}", integer_to_words(&millions)));
}
n % &million
} else {
n.clone()
};
let mut remaining = below_million
.to_u32()
.expect("below a million, which fits a u32");
if remaining >= 1000 {
let thousands = remaining / 1000;
remaining %= 1000;
match thousands {
1 => parts.push(format!("{THOUSAND} kan")),
10 => parts.push(TEN_THOUSAND.to_string()),
20 => parts.push(TWENTY_THOUSAND.to_string()),
_ => parts.push(format!("{THOUSAND} {}", convert_hundreds(thousands))),
}
}
if remaining > 0 {
let words = convert_hundreds(remaining);
if parts.is_empty() {
parts.push(words);
} else {
parts.push(format!("{AND} {words}"));
}
}
parts.join(", ")
}
fn decimal_part_to_words(decimal: &str) -> String {
let digits: Vec<&str> = decimal
.bytes()
.map(|b| {
let d = (b - b'0') as usize;
if d == 0 { ZERO } else { ONES[d] }
})
.collect();
digits.join(" ")
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let number = parse_cardinal(&value.into())?;
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 n.is_one() {
return ORDINAL_FIRST.to_string();
}
if *n == BigUint::from(2u32) {
return ORDINAL_SECOND.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())?;
Ok(integer_to_ordinal(&n))
}
options! {
pub struct CurrencyOptions {
currency: Currency = Currency::NGN, 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(&integer_to_words(&money.major));
result.push(' ');
result.push_str(words.major_at(0));
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push_str(" àti ");
}
result.push_str(&integer_to_words(&BigUint::from(money.minor)));
result.push(' ');
result.push_str(words.minor_at(0));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "yo-NG",
name: "Yoruba (Nigeria)",
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)),
};