use crate::Result;
use crate::currency::vocab::FIL 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, split_groups};
use crate::options::options;
use num_bigint::BigUint;
use num_traits::{One, ToPrimitive, Zero};
const ONES: [&str; 10] = [
"", "isa", "dalawa", "tatlo", "apat", "lima", "anim", "pito", "walo", "siyam",
];
const TEENS: [&str; 10] = [
"sampu",
"labinisa",
"labindalawa",
"labintatlo",
"labinapat",
"labinlima",
"labinanum",
"labimpito",
"labingwalo",
"labinsiyam",
];
const TENS: [&str; 10] = [
"",
"",
"dalawampu",
"tatlumpu",
"apatnapu",
"limampu",
"animnapu",
"pitumpu",
"walumpu",
"siyamnapu",
];
const HUNDRED: &str = "daang";
const THOUSAND: &str = "libong";
const ZERO: &str = "zero";
const NEGATIVE: &str = "negatibo";
const DECIMAL_SEP: &str = "punto";
const SCALE_WORDS: &[&str] = &["", THOUSAND, "milyong", "bilyong", "trilyong"];
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 ORDINAL_PREFIX: &str = "ika";
const ORDINAL_FIRST: &str = "una";
const ORDINAL_SECOND: &str = "ikalawa";
const VOWELS: [char; 5] = ['a', 'e', 'i', 'o', 'u'];
fn add_linker(word: &str) -> String {
if word.ends_with(VOWELS) {
format!("{word}ng")
} else {
format!("{word} na")
}
}
fn link_last_word(words: &str) -> String {
match words.rfind(' ') {
None => {
if words.ends_with("ng") {
return words.to_string(); }
add_linker(words)
}
Some(last_space_idx) => {
let (prefix, last_word) = words.split_at(last_space_idx + 1);
if last_word.ends_with("ng") {
return words.to_string(); }
format!("{prefix}{}", add_linker(last_word))
}
}
}
fn build_segment(n: u32) -> String {
if n == 0 {
return String::new();
}
let ones = (n % 10) as usize;
let tens_digit = ((n / 10) % 10) as usize;
let hundreds_digit = (n / 100) as usize;
let mut parts: Vec<String> = Vec::new();
if hundreds_digit > 0 {
let hundred_prefix = add_linker(ONES[hundreds_digit]);
parts.push(format!("{hundred_prefix} {HUNDRED}"));
}
let tens_ones = (n % 100) as usize;
if tens_ones == 0 {
} else if tens_ones < 10 {
parts.push(ONES[ones].to_string());
} else if tens_ones < 20 {
parts.push(TEENS[ones].to_string());
} else if ones == 0 {
parts.push(TENS[tens_digit].to_string());
} else if tens_digit == 5 {
parts.push(format!("{}ng {}", TENS[tens_digit], ONES[ones]));
} else {
parts.push(format!("{} {}", TENS[tens_digit], ONES[ones]));
}
parts.join(" ")
}
fn build_segment_with_linker(n: u32) -> String {
let segment_word = build_segment(n);
if segment_word.is_empty() {
return String::new();
}
link_last_word(&segment_word)
}
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 build_segment(small as u32);
}
}
build_large_number_words(n)
}
fn build_large_number_words(n: &BigUint) -> String {
let segment_values = split_groups(n, 1000);
let mut result = String::new();
for (i, &segment) in segment_values.iter().enumerate().rev() {
if segment == 0 {
continue;
}
let scale_word = SCALE_WORDS.get(i).copied().unwrap_or("");
if !result.is_empty() {
result.push(' ');
}
if i == 0 {
result.push_str(&build_segment(segment));
} else {
result.push_str(&build_segment_with_linker(segment));
result.push(' ');
result.push_str(scale_word);
}
}
result
}
fn decimal_part_to_words(decimal: &str) -> String {
decimal
.bytes()
.map(|b| {
let d = (b - b'0') as usize;
if d == 0 { ZERO } else { ONES[d] }
})
.collect::<Vec<_>>()
.join(" ")
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let number = parse_cardinal(&value.into())?;
CARDINAL_MAX.check(&number.integer, None)?;
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.to_u64() == Some(2) {
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())?;
ORDINAL_MAX.check(&n, None)?;
Ok(integer_to_ordinal(&n))
}
options! {
pub struct CurrencyOptions {
currency: Currency = Currency::PHP, 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 peso = words.major_at(0);
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
if !money.major.is_zero() || money.minor == 0 {
let peso_words = integer_to_words(&money.major);
if money.major.is_zero() {
result.push_str(&peso_words);
} else {
result.push_str(&link_last_word(&peso_words));
}
result.push(' ');
result.push_str(peso);
}
if money.minor > 0 {
let sentimo = words.minor_at(0);
if !money.major.is_zero() {
result.push_str(" at ");
}
let sentimo_words = integer_to_words(&BigUint::from(money.minor));
result.push_str(&link_last_word(&sentimo_words));
result.push(' ');
result.push_str(sentimo);
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "fil-PH",
name: "Filipino (Philippines)",
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)),
};