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 = "ଋଣ ";
const ZERO_WORD: &str = "ଶୂନ୍ୟ";
const ONES: [&str; 10] = [
"", "ଏକ", "ଦୁଇ", "ତିନି", "ଚାରି", "ପାଞ୍ଚ", "ଛଅ", "ସାତ", "ଆଠ", "ନଅ",
];
const TEENS: [&str; 10] = [
"ଦଶ", "ଏଗାର", "ବାର", "ତେର", "ଚଉଦ", "ପନ୍ଦର", "ଷୋହଳ", "ସତର", "ଅଠର", "ଉଣେଇଶ",
];
const TENS: [&str; 10] = [
"", "ଦଶ", "କୋଡ଼ିଏ", "ତିରିଶ", "ଚାଳିଶ", "ପଚାଶ", "ଷାଠିଏ", "ସତୁରୀ", "ଅଶୀ", "ନବେ",
];
const ORDINAL_IRREGULARS: [&str; 11] = [
"",
"ପ୍ରଥମ",
"ଦ୍ୱିତୀୟ",
"ତୃତୀୟ",
"ଚତୁର୍ଥ",
"ପଞ୍ଚମ",
"ଷଷ୍ଠ",
"ସପ୍ତମ",
"ଅଷ୍ଟମ",
"ନବମ",
"ଦଶମ",
];
const ORDINAL_SUFFIX: &str = "ମ";
const HUNDRED: &str = "ଶହ";
const THOUSAND: &str = "ହଜାର";
const MILLION: &str = "ଦଶ ଲକ୍ଷ";
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(&["ଟଙ୍କା", "ଟଙ୍କା"], &["ପଇସା", "ପଇସା"]),
);
forms.insert(
"USD",
CurrencyForms::new(&["ଡଲାର", "ଡଲାର"], &["ସେଣ୍ଟ", "ସେଣ୍ଟ"]),
);
forms.insert(
"EUR",
CurrencyForms::new(&["ୟୁରୋ", "ୟୁରୋ"], &["ସେଣ୍ଟ", "ସେଣ୍ଟ"]),
);
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(" ଓ ");
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(" ଓ ");
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 {
"ଦଶମିକ"
}
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> {
if let Some(i) = value.to_usize() {
if (1..=10).contains(&i) {
return Ok(ORDINAL_IRREGULARS[i].to_string());
}
}
Ok(format!("{}{}", self.to_cardinal(value)?, ORDINAL_SUFFIX))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}{}", value, ORDINAL_SUFFIX))
}
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!(
"{}{}",
self.to_cardinal_float(value, None)?,
ORDINAL_SUFFIX
))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}{}", repr_str, ORDINAL_SUFFIX))
}
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!("{}{}", token, ORDINAL_SUFFIX))
}
_ => 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(format!("NaN{}", ORDINAL_SUFFIX)),
_ => 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), "ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ"),
(f(0.5, 1), "ଶୂନ୍ୟ ଦଶମିକ ପାଞ୍ଚ"),
(f(1.0, 1), "ଏକ ଦଶମିକ ଶୂନ୍ୟ"),
(f(1.5, 1), "ଏକ ଦଶମିକ ପାଞ୍ଚ"),
(f(2.25, 2), "ଦୁଇ ଦଶମିକ ଦୁଇ ପାଞ୍ଚ"),
(f(3.14, 2), "ତିନି ଦଶମିକ ଏକ ଚାରି"),
(f(0.01, 2), "ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ ଏକ"),
(f(0.1, 1), "ଶୂନ୍ୟ ଦଶମିକ ଏକ"),
(f(0.99, 2), "ଶୂନ୍ୟ ଦଶମିକ ନଅ ନଅ"),
(f(1.01, 2), "ଏକ ଦଶମିକ ଶୂନ୍ୟ ଏକ"),
(f(12.34, 2), "ବାର ଦଶମିକ ତିନି ଚାରି"),
(f(99.99, 2), "ନବେ ଓ ନଅ ଦଶମିକ ନଅ ନଅ"),
(f(100.5, 1), "ଏକ ଶହ ଦଶମିକ ପାଞ୍ଚ"),
(f(1234.56, 2), "ଏକ ହଜାର ଦୁଇ ଶହ ଓ ତିରିଶ ଓ ଚାରି ଦଶମିକ ପାଞ୍ଚ ଛଅ"),
(f(-0.5, 1), "ଋଣ ଶୂନ୍ୟ ଦଶମିକ ପାଞ୍ଚ"),
(f(-1.5, 1), "ଋଣ ଏକ ଦଶମିକ ପାଞ୍ଚ"),
(f(-12.34, 2), "ଋଣ ବାର ଦଶମିକ ତିନି ଚାରି"),
(f(1.005, 3), "ଏକ ଦଶମିକ ଶୂନ୍ୟ ଶୂନ୍ୟ ପାଞ୍ଚ"),
(f(2.675, 3), "ଦୁଇ ଦଶମିକ ଛଅ ସାତ ପାଞ୍ଚ"),
];
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), "ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ ଏକ"),
(d("1.10", 2), "ଏକ ଦଶମିକ ଏକ ଶୂନ୍ୟ"),
(d("12.345", 3), "ବାର ଦଶମିକ ତିନି ଚାରି ପାଞ୍ଚ"),
(d("98746251323029.99", 2), "98746251323029 ଦଶମିକ ନଅ ନଅ"),
(d("0.001", 3), "ଶୂନ୍ୟ ଦଶମିକ ଶୂନ୍ୟ ଶୂନ୍ୟ ଏକ"),
];
for (v, want) in cases {
let got = l.to_cardinal_float(v, None).unwrap();
assert_eq!(&got, want, "decimal {:?}", v);
}
}
}