use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "point";
const ZERO_WORD: &str = "බිංදුව";
const ONES: [&str; 10] = [
"", "එක", "දෙක", "තුන", "හතර", "පහ", "හය", "හත", "අට", "නවය",
];
const TENS: [&str; 10] = [
"", "දහය", "විස්ස", "තිහ", "හතළිහ", "පනහ", "හැට", "හැත්තෑව", "අසූව", "අනූව",
];
const HUNDRED: &str = "සියය";
const THOUSAND: &str = "දහස";
const LAKH: &str = "ලක්ෂය";
const CRORE: &str = "කෝටිය";
const ORDINAL_SUFFIX: &str = " වැනි";
const LANG_NAME: &str = "Num2Word_SI";
const LKR: &str = "LKR";
pub struct LangSi {
forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangSi {
fn default() -> Self {
Self::new()
}
}
impl LangSi {
pub fn new() -> Self {
let mut forms = HashMap::new();
forms.insert(
LKR,
CurrencyForms::new(&["රුපියල්", "රුපියල්"], &["සත", "සත"]),
);
forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euros"], &["cent", "cents"]),
);
LangSi { forms }
}
fn forms_or_lkr(&self, code: &str) -> &CurrencyForms {
self.forms
.get(code)
.or_else(|| self.forms.get(LKR))
.expect("new() always installs CURRENCY_FORMS[\"LKR\"] as the fallback")
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, self.int_to_word(&number.abs()));
}
match number.to_u64() {
Some(n) if n < 1_000_000_000 => self.bounded_to_word(n),
_ => number.to_string(),
}
}
fn bounded_to_word(&self, n: u64) -> String {
if n < 10 {
ONES[n as usize].to_string()
} else if n < 100 {
let tens_val = (n / 10) as usize;
let ones_val = (n % 10) as usize;
if ones_val == 0 {
TENS[tens_val].to_string()
} else {
format!("{} {}", TENS[tens_val], ONES[ones_val])
}
} else if n < 1_000 {
let hundreds_val = (n / 100) as usize;
let remainder = n % 100;
let mut result = if hundreds_val == 1 {
HUNDRED.to_string()
} else {
format!("{} {}", ONES[hundreds_val], HUNDRED)
};
if remainder != 0 {
result.push(' ');
result.push_str(&self.bounded_to_word(remainder));
}
result
} else if n < 100_000 {
let thousands_val = n / 1_000;
let remainder = n % 1_000;
let mut result = if thousands_val == 1 {
THOUSAND.to_string()
} else {
format!("{} {}", self.bounded_to_word(thousands_val), THOUSAND)
};
if remainder != 0 {
result.push(' ');
result.push_str(&self.bounded_to_word(remainder));
}
result
} else if n < 10_000_000 {
let lakh_val = n / 100_000;
let remainder = n % 100_000;
let mut result = if lakh_val == 1 {
LAKH.to_string()
} else {
format!("{} {}", self.bounded_to_word(lakh_val), LAKH)
};
if remainder != 0 {
result.push(' ');
result.push_str(&self.bounded_to_word(remainder));
}
result
} else {
let crore_val = n / 10_000_000;
let remainder = n % 10_000_000;
let mut result = if crore_val == 1 {
CRORE.to_string()
} else {
format!("{} {}", self.bounded_to_word(crore_val), CRORE)
};
if remainder != 0 {
result.push(' ');
result.push_str(&self.bounded_to_word(remainder));
}
result
}
}
}
fn plain_decimal_string(d: &BigDecimal) -> String {
let (digits, scale) = d.as_bigint_and_exponent();
let s = digits.magnitude().to_string();
if scale <= 0 {
let mut out = s;
out.push_str(&"0".repeat((-scale) as usize));
return out;
}
let scale = scale as usize;
if s.len() > scale {
let (int_part, frac_part) = s.split_at(s.len() - scale);
format!("{}.{}", int_part, frac_part)
} else {
format!("0.{}{}", "0".repeat(scale - s.len()), s)
}
}
fn float_no_point_repr(v: f64) -> String {
if v.is_nan() {
return "nan".to_string();
}
if v.is_infinite() {
return "inf".to_string();
}
let digits = BigInt::from_f64(v.abs())
.expect("finite whole f64 converts exactly")
.to_string();
let exp = digits.len() - 1; let mant = digits.trim_end_matches('0');
let mant = if mant.len() <= 1 {
digits[..1].to_string()
} else {
format!("{}.{}", &mant[..1], &mant[1..])
};
format!("{}e+{}", mant, exp)
}
fn parse_int(token: &str) -> Result<BigInt> {
BigInt::from_str(token).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
token
))
})
}
impl Lang for LangSi {
fn default_currency(&self) -> &str {
"LKR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn lang_name(&self) -> &str {
LANG_NAME
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.forms.get(code)
}
fn pointword(&self) -> &str {
"point"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (ret, n) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", ret, self.int_to_word(&n)).trim().to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", self.to_cardinal(value)?, ORDINAL_SUFFIX))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}.", 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 (is_neg, n) = match value {
FloatValue::Float { value: v, precision } => (
v.is_sign_negative(),
format!("{:.*}", *precision as usize, v.abs()),
),
FloatValue::Decimal { value: d, .. } => {
(d.is_negative(), plain_decimal_string(&d.abs()))
}
};
let neg_prefix = if is_neg { NEGWORD } else { "" };
if let Some((left, right)) = n.split_once('.') {
let mut ret = String::from(neg_prefix);
ret.push_str(&self.int_to_word(&parse_int(left)?));
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
for ch in right.chars() {
let d = parse_int(&ch.to_string())?;
ret.push_str(&self.int_to_word(&d));
ret.push(' ');
}
Ok(ret.trim().to_string())
} else {
let val = parse_int(&n)?;
Ok(format!("{}{}", neg_prefix, self.int_to_word(&val))
.trim()
.to_string())
}
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
if value.has_visible_point() {
return self.to_cardinal_float(value, precision_override);
}
let neg_prefix = if value.is_negative() { NEGWORD } else { "" };
let text = match value {
FloatValue::Float { value: v, .. } => float_no_point_repr(*v),
FloatValue::Decimal { value: d, .. } => python_decimal_str(&d.abs()),
};
let val = parse_int(&text)?;
Ok(format!("{}{}", neg_prefix, self.int_to_word(&val))
.trim()
.to_string())
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!(
"{}{}",
self.cardinal_float_entry(value, None)?,
ORDINAL_SUFFIX
))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", 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(),
)),
other => Ok(other),
}
}
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 s = match val {
CurrencyValue::Int(v) => v.abs().to_string(),
CurrencyValue::Decimal { value: d, .. } => plain_decimal_string(&d.abs()),
};
let mut parts = s.splitn(2, '.');
let p0 = parts.next().unwrap_or("");
let p1 = parts.next();
let left = if p0.is_empty() {
BigInt::zero()
} else {
parse_int(p0)?
};
let right = match p1 {
Some(f) if !f.is_empty() => {
let mut t: String = f.chars().take(2).collect();
while t.chars().count() < 2 {
t.push('0');
}
parse_int(&t)?
}
_ => BigInt::zero(),
};
let forms = self.forms_or_lkr(currency);
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let left_str = self.to_cardinal(&left)?;
let mut result = format!(
"{} {}",
left_str,
if left.is_one() { &cr1[0] } else { &cr1[1] }
);
if cents && !right.is_zero() {
let cents_str = self.to_cardinal(&right)?;
result.push_str(separator);
result.push_str(¢s_str);
result.push(' ');
result.push_str(if right.is_one() { &cr2[0] } else { &cr2[1] });
}
Ok(format!(
"{}{}",
if is_negative { NEGWORD } else { "" },
result
))
}
}
#[cfg(test)]
mod float_entry_tests {
use super::*;
use std::str::FromStr as _;
fn fv(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn dv(s: &str, precision: u32) -> FloatValue {
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
}
}
#[test]
fn entry_routing() {
let l = LangSi::new();
assert_eq!(
l.cardinal_float_entry(&fv(5.0, 1), None).unwrap(),
"පහ point බිංදුව"
);
assert_eq!(
l.ordinal_float_entry(&fv(5.0, 1)).unwrap(),
"පහ point බිංදුව වැනි"
);
assert_eq!(l.year_float_entry(&fv(5.0, 1)).unwrap(), "පහ point බිංදුව");
assert_eq!(l.ordinal_num_float_entry(&fv(5.0, 1), "5.0").unwrap(), "5.0.");
assert_eq!(
l.ordinal_float_entry(&fv(-0.0, 1)).unwrap(),
"minus බිංදුව point බිංදුව වැනි"
);
assert_eq!(
l.cardinal_float_entry(&dv("5.00", 2), None).unwrap(),
"පහ point බිංදුව බිංදුව"
);
assert_eq!(l.cardinal_float_entry(&dv("5", 0), None).unwrap(), "පහ");
assert_eq!(l.ordinal_float_entry(&dv("100", 0)).unwrap(), "සියය වැනි");
match l.cardinal_float_entry(&fv(1e16, 16), None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1e+16'")
}
other => panic!("expected ValueError, got {:?}", other),
}
match l.cardinal_float_entry(&dv("1E+2", 2), None) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '1E+2'")
}
other => panic!("expected ValueError, got {:?}", other),
}
assert!(matches!(
l.year_float_entry(&fv(1e20, 20)),
Err(N2WError::Value(_))
));
assert_eq!(l.ordinal_num_float_entry(&fv(1e16, 16), "1e+16").unwrap(), "1e+16.");
assert!(matches!(l.str_to_number("Infinity"), Err(N2WError::Value(_))));
assert!(matches!(l.str_to_number("-Infinity"), Err(N2WError::Value(_))));
assert!(matches!(l.str_to_number("1.5"), Ok(ParsedNumber::Dec(_))));
}
}