use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "ūna ";
const POINTWORD: &str = "bindu";
const ZERO_WORD: &str = "suñña";
const ONES: [&str; 10] = [
"",
"eka",
"dvi",
"ti",
"catu",
"pañca",
"cha",
"satta",
"aṭṭha",
"nava",
];
const TEENS: [&str; 10] = [
"dasa",
"ekādasa",
"dvādasa",
"terasa",
"cuddasa",
"pannarasa",
"soḷasa",
"sattarasa",
"aṭṭhārasa",
"ekūnavīsati",
];
const TENS: [&str; 10] = [
"",
"dasa",
"vīsati",
"tiṃsa",
"cattāḷīsa",
"paññāsa",
"saṭṭhi",
"sattati",
"asīti",
"navuti",
];
const HUNDRED: &str = "sata";
const THOUSAND: &str = "sahassa";
const MILLION: &str = "dasa-lakkha";
const LANG_NAME: &str = "Num2Word_PLI";
const DEFAULT_SEPARATOR: &str = " ";
const BASE_DEFAULT_SEPARATOR: &str = ",";
const FALLBACK_CURRENCY: &str = "USD";
pub struct LangPli {
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl Default for LangPli {
fn default() -> Self {
Self::new()
}
}
impl LangPli {
pub fn new() -> Self {
let mut currency_forms: HashMap<&'static str, CurrencyForms> = HashMap::new();
currency_forms.insert(
"USD",
CurrencyForms::new(&["ḍolara", "ḍolara"], &["senta", "senta"]),
);
currency_forms.insert(
"EUR",
CurrencyForms::new(&["yuro", "yuro"], &["senta", "senta"]),
);
currency_forms.insert(
"INR",
CurrencyForms::new(&["rūpa", "rūpa"], &["paisā", "paisā"]),
);
let fallback_forms = currency_forms
.get(FALLBACK_CURRENCY)
.expect("FALLBACK_CURRENCY is inserted above")
.clone();
LangPli {
exclude_title: ["ca", "bindu", "ūna"]
.iter()
.map(|s| s.to_string())
.collect(),
currency_forms,
fallback_forms,
}
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
if number < &BigInt::from(10) {
let i = number.to_usize().expect("guarded: number < 10");
return ONES[i].to_string();
}
if number < &BigInt::from(20) {
let i = number.to_usize().expect("guarded: number < 20");
return TEENS[i - 10].to_string();
}
if number < &BigInt::from(100) {
let (t, o) = number.div_mod_floor(&BigInt::from(10));
let t = t.to_usize().expect("guarded: number < 100 => t < 10");
let o = o.to_usize().expect("remainder mod 10 < 10");
return if o != 0 {
format!("{} ca {}", TENS[t], ONES[o])
} else {
TENS[t].to_string()
};
}
if number < &BigInt::from(1000) {
let (h, r) = number.div_mod_floor(&BigInt::from(100));
let h = h.to_usize().expect("guarded: number < 1000 => h < 10");
let base = format!("{} {}", ONES[h], HUNDRED);
return if !r.is_zero() {
format!("{} ca {}", base, self.int_to_word(&r))
} else {
base
};
}
if number < &BigInt::from(1_000_000) {
let (t, r) = number.div_mod_floor(&BigInt::from(1000));
let base = format!("{} {}", self.int_to_word(&t), THOUSAND);
return if !r.is_zero() {
format!("{} {}", base, self.int_to_word(&r))
} else {
base
};
}
if number < &BigInt::from(1_000_000_000) {
let (m, r) = number.div_mod_floor(&BigInt::from(1_000_000));
let base = format!("{} {}", self.int_to_word(&m), MILLION);
return if !r.is_zero() {
format!("{} {}", base, self.int_to_word(&r))
} else {
base
};
}
number.to_string()
}
fn cardinal_from_numstr(&self, n: &str) -> Result<String> {
let n = n.trim();
if let Some(rest) = n.strip_prefix('-') {
let inner = self.cardinal_from_numstr(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
if let Some(dot) = n.find('.') {
let left = &n[..dot];
let right = &n[dot + 1..];
let left = BigInt::from_str(left).map_err(|e| N2WError::Value(e.to_string()))?;
let mut ret = format!("{} {}", self.int_to_word(&left), POINTWORD);
for ch in right.chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch))
})? as usize;
let word = ONES[d];
ret.push(' ');
ret.push_str(if word.is_empty() { ZERO_WORD } else { word });
}
return Ok(ret.trim().to_string());
}
let bi = BigInt::from_str(n).map_err(|e| N2WError::Value(e.to_string()))?;
Ok(self.int_to_word(&bi))
}
}
fn python_repr_f64(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f < 0.0 { "-inf" } else { "inf" }.to_string();
}
let neg = f.is_sign_negative();
let s = format!("{:e}", f.abs()); let (mantissa, exp_s) = match s.split_once('e') {
Some(parts) => parts,
None => return s, };
let exp: i32 = exp_s.parse().unwrap_or(0);
let digits: String = mantissa.chars().filter(|c| c.is_ascii_digit()).collect();
let ndigits = digits.len() as i32;
let decpt = exp + 1;
let body = if decpt <= -4 || decpt > 16 {
let mut m = String::new();
m.push_str(&digits[..1]);
if ndigits > 1 {
m.push('.');
m.push_str(&digits[1..]);
}
let e = decpt - 1;
let (esign, eabs) = if e < 0 { ('-', -e) } else { ('+', e) };
format!("{}e{}{:02}", m, esign, eabs)
} else if decpt <= 0 {
format!("0.{}{}", "0".repeat((-decpt) as usize), digits)
} else if decpt >= ndigits {
format!("{}{}.0", digits, "0".repeat((decpt - ndigits) as usize))
} else {
let dp = decpt as usize;
format!("{}.{}", &digits[..dp], &digits[dp..])
};
if neg {
format!("-{}", body)
} else {
body
}
}
fn python_str_decimal(bd: &BigDecimal) -> String {
let (coeff, scale) = bd.as_bigint_and_exponent();
let py_exp: i64 = -scale; let neg = coeff.is_negative(); let int_str = coeff.abs().to_string(); let ndigits = int_str.len() as i64;
let leftdigits = py_exp + ndigits;
let dotplace: i64 = if py_exp <= 0 && leftdigits > -6 {
leftdigits
} else {
1
};
let (intpart, fracpart) = if dotplace <= 0 {
(
"0".to_string(),
format!(".{}{}", "0".repeat((-dotplace) as usize), int_str),
)
} else if dotplace >= ndigits {
(
format!("{}{}", int_str, "0".repeat((dotplace - ndigits) as usize)),
String::new(),
)
} else {
let dp = dotplace as usize;
(int_str[..dp].to_string(), format!(".{}", &int_str[dp..]))
};
let exp = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
let sign = if neg { "-" } else { "" };
format!("{}{}{}{}", sign, intpart, fracpart, exp)
}
impl Lang for LangPli {
fn default_currency(&self) -> &str {
"USD"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"bindu"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = self.to_cardinal(&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("paṭhama".to_string());
}
Ok(format!("{}ma", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}ma", 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_override;
let n = match value {
FloatValue::Decimal { value, .. } => python_str_decimal(value),
FloatValue::Float { value, .. } => python_repr_f64(*value),
};
self.cardinal_from_numstr(&n)
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if value.as_whole_int() == Some(BigInt::from(1)) {
return Ok("paṭhama".to_string());
}
Ok(format!("{}ma", self.to_cardinal_float(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}ma", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.to_cardinal_float(value, None)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
let parsed = python_decimal_parse(s)?;
Ok(parsed)
}
fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
match to {
"ordinal_num" => {
let s = if negative { "-Infinity" } else { "Infinity" };
Ok(format!("{}ma", s))
}
_ => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
}
}
fn nan_result(&self, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok("NaNma".to_string()),
_ => Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".into(),
)),
}
}
fn lang_name(&self) -> &str {
LANG_NAME
}
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 _ = adjective;
let separator = if separator == BASE_DEFAULT_SEPARATOR {
DEFAULT_SEPARATOR
} else {
separator
};
let is_negative = val.is_negative();
let s = match val {
CurrencyValue::Int(v) => v.abs().to_string(),
CurrencyValue::Decimal { value: d, .. } => d.abs().to_string(),
};
let mut parts = s.split('.');
let part0 = parts.next().unwrap_or("");
let part1 = parts.next();
let left = if part0.is_empty() {
BigInt::zero()
} else {
BigInt::from_str(part0).map_err(|e| N2WError::Value(e.to_string()))?
};
let right = match part1 {
Some(f) if !f.is_empty() => {
let mut two: String = f.chars().take(2).collect();
while two.chars().count() < 2 {
two.push('0');
}
BigInt::from_str(&two).map_err(|e| N2WError::Value(e.to_string()))?
}
_ => BigInt::zero(),
};
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.fallback_forms);
let cr1 = &forms.unit;
let cr2 = &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())
}
}