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;
const NEGWORD: &str = "ṛṇa ";
const ZERO_WORD: &str = "śūnya";
const ONES: [&str; 10] = [
"", "eka", "duī", "tini", "cāri", "pāñca", "chha", "sāta", "āṭha", "naa",
];
const TEENS: [&str; 10] = [
"daśa", "egāra", "bāra", "tera", "caudaha", "pandara", "śohaḷa", "satara", "aṭhāra", "unīśa",
];
const TENS: [&str; 10] = [
"", "daśa", "kohi", "tirīśa", "calīśa", "paṇcāśa", "ṣaṣṭi", "satara", "aśī", "nabe",
];
const HUNDRED: &str = "śaha";
const THOUSAND: &str = "hajāra";
const MILLION: &str = "daśa lakṣa";
const FIRST_FORM_KEY: &str = "INR";
fn split_currency_parts(val: &CurrencyValue) -> (BigInt, BigInt) {
let d = match val {
CurrencyValue::Int(v) => return (v.abs(), BigInt::zero()),
CurrencyValue::Decimal { value: d, .. } => d.abs(),
};
let (unscaled, scale) = d.as_bigint_and_exponent();
let digits = unscaled.to_string();
if scale <= 0 {
let mut int_digits = digits;
int_digits.push_str(&"0".repeat((-scale) as usize));
return (parse_digits(&int_digits), BigInt::zero());
}
let scale = scale as usize;
let padded = if digits.len() <= scale {
format!("{}{}", "0".repeat(scale - digits.len() + 1), digits)
} else {
digits
};
let split_at = padded.len() - scale;
let (int_part, frac_part) = padded.split_at(split_at);
let mut cents: String = frac_part.chars().take(2).collect();
while cents.len() < 2 {
cents.push('0');
}
(parse_digits(int_part), parse_digits(¢s))
}
fn parse_digits(s: &str) -> BigInt {
s.parse::<BigInt>()
.expect("digit string built from a BigInt's own decimal form")
}
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)
}
pub struct LangOr {
forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangOr {
fn default() -> Self {
Self::new()
}
}
impl LangOr {
pub fn new() -> Self {
let mut forms = HashMap::new();
forms.insert(
"INR",
CurrencyForms::new(&["ṭaṅkā", "ṭaṅkā"], &["paisā", "paisā"]),
);
forms.insert(
"USD",
CurrencyForms::new(&["ḍolāra", "ḍolāra"], &["seṇṭa", "seṇṭa"]),
);
forms.insert(
"EUR",
CurrencyForms::new(&["yuro", "yuro"], &["seṇṭa", "seṇṭa"]),
);
LangOr { forms }
}
fn forms_or_first(&self, currency: &str) -> &CurrencyForms {
self.forms
.get(currency)
.or_else(|| self.forms.get(FIRST_FORM_KEY))
.expect("the first-entry fallback key is always present in the table")
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
let ten = BigInt::from(10);
let hundred = BigInt::from(100);
let thousand = BigInt::from(1_000);
let million = BigInt::from(1_000_000);
let billion = BigInt::from(1_000_000_000);
if *number < ten {
return ONES[number.to_usize().unwrap()].to_string();
}
if *number < BigInt::from(20) {
return TEENS[(number - &ten).to_usize().unwrap()].to_string();
}
if *number < hundred {
let (t, o) = number.div_mod_floor(&ten);
let t = t.to_usize().unwrap(); let o = o.to_usize().unwrap(); let mut out = TENS[t].to_string();
if o != 0 {
out.push_str(" o ");
out.push_str(ONES[o]);
}
return out;
}
if *number < thousand {
let (h, r) = number.div_mod_floor(&hundred);
let h = h.to_usize().unwrap(); let mut out = format!("{} {}", ONES[h], HUNDRED);
if !r.is_zero() {
out.push_str(" o ");
out.push_str(&self.int_to_word(&r));
}
return out;
}
if *number < million {
let (t, r) = number.div_mod_floor(&thousand);
let mut out = format!("{} {}", self.int_to_word(&t), THOUSAND);
if !r.is_zero() {
out.push(' ');
out.push_str(&self.int_to_word(&r));
}
return out;
}
if *number < billion {
let (m, r) = number.div_mod_floor(&million);
let mut out = format!("{} {}", self.int_to_word(&m), MILLION);
if !r.is_zero() {
out.push(' ');
out.push_str(&self.int_to_word(&r));
}
return out;
}
number.to_string()
}
fn cardinal_from_repr(&self, n: &str) -> Result<String> {
let n = n.trim();
if let Some(rest) = n.strip_prefix('-') {
let inner = self.cardinal_from_repr(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
if let Some((left, right)) = n.split_once('.') {
let left_bi = left.parse::<BigInt>().map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: {:?}", left))
})?;
let mut ret = format!("{} {}", self.int_to_word(&left_bi), self.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 = if ONES[d].is_empty() { ZERO_WORD } else { ONES[d] };
ret.push(' ');
ret.push_str(word);
}
return Ok(ret.trim().to_string());
}
let bi = n.parse::<BigInt>().map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: {:?}", n))
})?;
Ok(self.int_to_word(&bi))
}
}
impl Lang for LangOr {
fn default_currency(&self) -> &str {
"INR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"daśamika"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if *value < BigInt::zero() {
let inner = self.to_cardinal(&-value)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
Ok(self.int_to_word(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<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 n = match value {
FloatValue::Float { value, .. } => python_repr_f64(*value),
FloatValue::Decimal { value, .. } => python_str_decimal(value),
};
self.cardinal_from_repr(&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> {
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 token = if negative { "-Infinity" } else { "Infinity" };
Ok(format!("{}ma", token))
}
_ => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".to_string(),
)),
}
}
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'".to_string(),
)),
}
}
fn lang_name(&self) -> &str {
"Num2Word_OR"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let Some(last) = forms.last() else {
return Ok(String::new());
};
Ok(if n.is_one() {
forms[0].clone()
} else {
last.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) = split_currency_parts(val);
let forms = self.forms_or_first(currency);
let unit = if left.is_one() {
&forms.unit[0]
} else {
&forms.unit[1]
};
let mut result = format!("{} {}", self.int_to_word(&left), unit);
if cents && !right.is_zero() {
let subunit = if right.is_one() {
&forms.subunit[0]
} else {
&forms.subunit[1]
};
result.push_str(separator);
result.push_str(&self.int_to_word(&right));
result.push(' ');
result.push_str(subunit);
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use crate::base::Lang;
use std::str::FromStr;
fn f(v: f64, p: u32) -> FloatValue {
FloatValue::Float { value: v, precision: p }
}
fn d(s: &str, p: u32) -> FloatValue {
FloatValue::Decimal { value: BigDecimal::from_str(s).unwrap(), precision: p }
}
#[test]
fn corpus_float() {
let l = LangOr::new();
let cases: &[(FloatValue, &str)] = &[
(f(0.0, 1), "śūnya daśamika śūnya"),
(f(0.5, 1), "śūnya daśamika pāñca"),
(f(1.0, 1), "eka daśamika śūnya"),
(f(1.5, 1), "eka daśamika pāñca"),
(f(2.25, 2), "duī daśamika duī pāñca"),
(f(3.14, 2), "tini daśamika eka cāri"),
(f(0.01, 2), "śūnya daśamika śūnya eka"),
(f(0.1, 1), "śūnya daśamika eka"),
(f(0.99, 2), "śūnya daśamika naa naa"),
(f(1.01, 2), "eka daśamika śūnya eka"),
(f(12.34, 2), "bāra daśamika tini cāri"),
(f(99.99, 2), "nabe o naa daśamika naa naa"),
(f(100.5, 1), "eka śaha daśamika pāñca"),
(f(1234.56, 2), "eka hajāra duī śaha o tirīśa o cāri daśamika pāñca chha"),
(f(-0.5, 1), "ṛṇa śūnya daśamika pāñca"),
(f(-1.5, 1), "ṛṇa eka daśamika pāñca"),
(f(-12.34, 2), "ṛṇa bāra daśamika tini cāri"),
(f(1.005, 3), "eka daśamika śūnya śūnya pāñca"),
(f(2.675, 3), "duī daśamika chha sāta pāñca"),
];
for (v, want) in cases {
let got = l.to_cardinal_float(v, None).unwrap();
assert_eq!(&got, want, "float {:?}", v);
}
}
#[test]
fn corpus_decimal() {
let l = LangOr::new();
let cases: &[(FloatValue, &str)] = &[
(d("0.01", 2), "śūnya daśamika śūnya eka"),
(d("1.10", 2), "eka daśamika eka śūnya"),
(d("12.345", 3), "bāra daśamika tini cāri pāñca"),
(d("98746251323029.99", 2), "98746251323029 daśamika naa naa"),
(d("0.001", 3), "śūnya daśamika śūnya śūnya eka"),
];
for (v, want) in cases {
let got = l.to_cardinal_float(v, None).unwrap();
assert_eq!(&got, want, "decimal {:?}", v);
}
}
}