use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const NEGWORD: &str = "menos ";
const POINTWORD: &str = "punto";
const ONES: [&str; 10] = [
"", "isa", "dalawa", "tatlo", "apat", "lima", "anim", "pito", "walo", "siyam",
];
const TEENS: [&str; 10] = [
"sampu",
"labing-isa",
"labindalawa",
"labintatlo",
"labing-apat",
"labinlima",
"labing-anim",
"labimpito",
"labingwalo",
"labinsiyam",
];
const TENS: [&str; 10] = [
"",
"sampu",
"dalawampu",
"tatlumpu",
"apatnapu",
"limampu",
"animnapu",
"pitumpu",
"walumpu",
"siyamnapu",
];
const HUNDRED: &str = "daan";
const THOUSAND: &str = "libo";
const MILLION: &str = "milyon";
const FALLBACK_CURRENCY: &str = "PHP";
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const SENTIMO: [&str; 2] = ["sentimo", "sentimo"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("PHP", CurrencyForms::new(&["piso", "piso"], &SENTIMO));
m.insert("USD", CurrencyForms::new(&["dolyar", "dolyar"], &SENTIMO));
m.insert("EUR", CurrencyForms::new(&["euro", "euro"], &SENTIMO));
m
}
fn idx(n: &BigInt) -> usize {
debug_assert!(!n.is_negative() && n < &BigInt::from(1000));
n.to_usize()
.expect("guarded to 0..=999 by the range check at the call site")
}
pub struct LangFil {
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangFil {
fn default() -> Self {
Self::new()
}
}
impl LangFil {
pub fn new() -> Self {
LangFil {
exclude_title: vec!["at".into(), "menos".into(), "punto".into()],
currency_forms: build_currency_forms(),
}
}
fn currency_parts(val: &CurrencyValue) -> (BigInt, BigInt) {
match val {
CurrencyValue::Int(i) => (i.abs(), BigInt::zero()),
CurrencyValue::Decimal { value, .. } => {
let abs = value.abs();
let left = abs.with_scale(0).as_bigint_and_exponent().0;
let frac = &abs - BigDecimal::from(left.clone());
let right = (frac * BigDecimal::from(100))
.with_scale(0)
.as_bigint_and_exponent()
.0;
(left, right)
}
}
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return "sero".to_string();
}
if number < &BigInt::from(10) {
return ONES[idx(number)].to_string();
}
if number < &BigInt::from(20) {
return TEENS[idx(&(number - BigInt::from(10)))].to_string();
}
if number < &BigInt::from(100) {
let (t, o) = number.div_mod_floor(&BigInt::from(10));
let mut s = TENS[idx(&t)].to_string();
if !o.is_zero() {
s.push_str(" at ");
s.push_str(ONES[idx(&o)]);
}
return s;
}
if number < &BigInt::from(1000) {
let (h, r) = number.div_mod_floor(&BigInt::from(100));
let mut s = format!("{} {}", ONES[idx(&h)], HUNDRED);
if !r.is_zero() {
s.push_str(" at ");
s.push_str(&self.int_to_word(&r));
}
return s;
}
if number < &BigInt::from(1_000_000) {
let (t, r) = number.div_mod_floor(&BigInt::from(1000));
let mut s = format!("{} {}", self.int_to_word(&t), THOUSAND);
if !r.is_zero() {
s.push(' ');
s.push_str(&self.int_to_word(&r));
}
return s;
}
if number < &BigInt::from(1_000_000_000) {
let (m, r) = number.div_mod_floor(&BigInt::from(1_000_000));
let mut s = format!("{} {}", self.int_to_word(&m), MILLION);
if !r.is_zero() {
s.push(' ');
s.push_str(&self.int_to_word(&r));
}
return s;
}
number.to_string()
}
}
fn python_float_repr_abs(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return "inf".to_string();
}
if f != 0.0 && (f >= 1e16 || f < 1e-4) {
let s = format!("{:e}", f);
let (mant, exp) = s.split_once('e').expect("LowerExp always emits an e");
let exp: i32 = exp.parse().expect("f64 exponent is a small integer");
return format!(
"{}e{}{:02}",
mant,
if exp < 0 { '-' } else { '+' },
exp.abs()
);
}
let s = format!("{}", f);
if s.contains('.') {
s
} else {
format!("{}.0", s)
}
}
fn int_value_error(lit: &str) -> N2WError {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
lit
))
}
impl Lang for LangFil {
fn default_currency(&self) -> &str {
"PHP"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"punto"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = self.int_to_word(&value.abs());
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
Ok(self.int_to_word(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if value.is_one() {
return Ok("una".to_string());
}
Ok(format!("ika-{}", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("ika-{}", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let precision = value.precision();
let is_negative = value.is_negative();
let (pre, post) = float2tuple(value);
let left = pre.abs();
if precision == 0 {
let mut ret = self.int_to_word(&left);
if is_negative {
ret = format!("{}{}", NEGWORD, ret);
}
return Ok(ret.trim().to_string());
}
let post_str = post.to_string();
let post_str = format!(
"{}{}",
"0".repeat((precision as usize).saturating_sub(post_str.len())),
post_str
);
let mut ret = format!("{} {}", self.int_to_word(&left), POINTWORD);
for ch in post_str.chars().take(precision as usize) {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("non-digit {:?} in fractional part", ch))
})?;
ret.push(' ');
ret.push_str(if d == 0 { "sero" } else { ONES[d as usize] });
}
if is_negative {
ret = format!("{}{}", NEGWORD, ret);
}
Ok(ret.trim().to_string())
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
let n = match value {
FloatValue::Float { value: f, .. } => python_float_repr_abs(f.abs()),
FloatValue::Decimal { value: d, .. } => {
let s = python_decimal_str(d);
s.strip_prefix('-').map(str::to_string).unwrap_or(s)
}
};
match n.split_once('.') {
Some((left, right)) => {
if left.is_empty() || !left.bytes().all(|b| b.is_ascii_digit()) {
return Err(int_value_error(left));
}
if let Some(bad) = right.chars().find(|c| !c.is_ascii_digit()) {
return Err(int_value_error(&bad.to_string()));
}
self.to_cardinal_float(value, precision_override)
}
None => {
if n.is_empty() || !n.bytes().all(|b| b.is_ascii_digit()) {
return Err(int_value_error(&n));
}
let magnitude: BigInt = n.parse().expect("all-ASCII-digit string parses");
if value.is_negative() {
return Ok(format!("{}{}", NEGWORD, self.int_to_word(&magnitude))
.trim()
.to_string());
}
Ok(self.int_to_word(&magnitude))
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if value.as_whole_int() == Some(BigInt::from(1u32)) {
return Ok("una".to_string());
}
Ok(format!("ika-{}", self.cardinal_float_entry(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("ika-{}", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.cardinal_float_entry(value, None)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
ParsedNumber::NaN => Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".into(),
)),
other => Ok(other),
}
}
fn lang_name(&self) -> &str {
"Num2Word_FIL"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
if forms.is_empty() {
return Ok(String::new());
}
Ok(if n.is_one() {
forms[0].clone()
} else {
forms[forms.len() - 1].clone()
})
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
let separator = separator.unwrap_or(self.default_separator());
let is_negative = val.is_negative();
let (left, right) = Self::currency_parts(val);
let forms = self
.currency_forms
.get(currency)
.or_else(|| self.currency_forms.get(FALLBACK_CURRENCY))
.expect("PHP is always present in CURRENCY_FORMS");
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let mut result = format!(
"{} {}",
self.int_to_word(&left),
if left.is_one() { &cr1[0] } else { &cr1[1] }
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(if right.is_one() { &cr2[0] } else { &cr2[1] });
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}