use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::sync::OnceLock;
const NEGWORD: &str = "phak ";
const POINTWORD: &str = "decimal";
const ONES: [&str; 10] = [
"", "pakhat", "pahnih", "pathum", "pali", "panga", "paruk", "pasarih", "pariat", "pakua",
];
const TENS: [&str; 10] = [
"", "sawm", "sawmhnih", "sawmthum", "sawmli", "sawmnga", "sawmruk", "sawmsarih", "sawmriat",
"sawmkua",
];
const HUNDRED: &str = "za";
const THOUSAND: &str = "sang";
const MILLION: &str = "nuai";
const ZERO_WORD: &str = "a awmlo";
const LEH: &str = " leh ";
const CURRENCY_FORMS_DECL: [(&str, [&str; 2], [&str; 2]); 3] = [
("INR", ["rupee", "rupee"], ["paisa", "paisa"]),
("USD", ["dollar", "dollar"], ["cent", "cent"]),
("EUR", ["euro", "euro"], ["cent", "cent"]),
];
const SEPARATOR_DEFAULT: &str = " ";
const SEPARATOR_UNSET: &str = ",";
fn split_currency(n: &BigDecimal) -> Result<(BigInt, BigInt)> {
let (digits, scale) = n.as_bigint_and_exponent();
if scale < 0 {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
n
)));
}
if scale == 0 {
return Ok((digits, BigInt::zero()));
}
let s = digits.to_string();
let scale = scale as usize;
let (int_part, frac_part) = if s.len() > scale {
let (a, b) = s.split_at(s.len() - scale);
(a.to_string(), b.to_string())
} else {
("0".to_string(), format!("{:0>width$}", s, width = scale))
};
let left = int_part.parse::<BigInt>().unwrap_or_else(|_| BigInt::zero());
let head: String = frac_part.chars().take(2).collect();
let right = format!("{:0<2}", head)
.parse::<BigInt>()
.unwrap_or_else(|_| BigInt::zero());
Ok((left, right))
}
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return ZERO_WORD.to_string();
}
let ten = BigInt::from(10u32);
if number < &ten {
return ONES[small(number)].to_string();
}
let hundred = BigInt::from(100u32);
if number < &hundred {
let (t, o) = number.div_mod_floor(&ten);
let mut out = TENS[small(&t)].to_string();
if !o.is_zero() {
out.push_str(LEH);
out.push_str(ONES[small(&o)]);
}
return out;
}
let thousand = BigInt::from(1_000u32);
if number < &thousand {
let (h, r) = number.div_mod_floor(&hundred);
let mut out = format!("{} {}", ONES[small(&h)], HUNDRED);
if !r.is_zero() {
out.push_str(LEH);
out.push_str(&int_to_word(&r));
}
return out;
}
let million = BigInt::from(1_000_000u32);
if number < &million {
let (t, r) = number.div_mod_floor(&thousand);
let mut out = format!("{} {}", int_to_word(&t), THOUSAND);
if !r.is_zero() {
out.push(' ');
out.push_str(&int_to_word(&r));
}
return out;
}
let billion = BigInt::from(1_000_000_000u32);
if number < &billion {
let (m, r) = number.div_mod_floor(&million);
let mut out = format!("{} {}", int_to_word(&m), MILLION);
if !r.is_zero() {
out.push(' ');
out.push_str(&int_to_word(&r));
}
return out;
}
number.to_string()
}
fn small(n: &BigInt) -> usize {
debug_assert!(!n.is_negative() && n < &BigInt::from(10u32));
n.to_usize().expect("guarded to 0..=9 by the caller")
}
fn shortest_digits(a: f64) -> (String, i32) {
let sci = format!("{:e}", a);
let (mant, exp) = sci
.split_once('e')
.expect("{:e} on a finite f64 always emits an exponent");
let mut digits: Vec<u8> = mant.bytes().filter(|c| *c != b'.').collect();
let mut decpt: i32 = exp.parse::<i32>().expect("{:e} exponent is an integer") + 1;
let bits = a.to_bits();
let biased = ((bits >> 52) & 0x7ff) as i32;
let frac = bits & ((1u64 << 52) - 1);
let (mut m, mut e) = if biased == 0 {
(frac, -1074i32) } else {
(frac | (1u64 << 52), biased - 1075)
};
if m == 0 {
return (String::from_utf8(digits).expect("ASCII digits"), decpt);
}
let z = m.trailing_zeros() as i32;
m >>= z;
e += z;
let q = digits.len() as i32 - decpt;
let mut tie = e + q + 1 == 0;
if tie && q < 0 {
let r = -q as u32;
tie = r <= 22 && m % 5u64.pow(r) == 0;
}
if !tie {
return (String::from_utf8(digits).expect("ASCII digits"), decpt);
}
let last = digits[digits.len() - 1] - b'0';
if last % 2 == 1 {
if m % 4 == 1 {
*digits.last_mut().expect("non-empty") -= 1;
} else {
let mut i = digits.len();
loop {
if i == 0 {
digits.insert(0, b'1');
decpt += 1;
break;
}
i -= 1;
if digits[i] == b'9' {
digits[i] = b'0';
} else {
digits[i] += 1;
break;
}
}
}
while digits.len() > 1 && *digits.last().expect("non-empty") == b'0' {
digits.pop();
}
}
(String::from_utf8(digits).expect("ASCII digits"), decpt)
}
fn py_float_repr(value: f64) -> String {
if value.is_nan() {
return "nan".to_string();
}
if value.is_infinite() {
return if value > 0.0 { "inf" } else { "-inf" }.to_string();
}
let sign = if value.is_sign_negative() { "-" } else { "" };
let (digits, decpt) = shortest_digits(value.abs());
let ndigits = digits.len() as i32;
if decpt <= -4 || decpt > 16 {
let exp = decpt - 1;
let mut mant = String::from(&digits[..1]);
if digits.len() > 1 {
mant.push('.');
mant.push_str(&digits[1..]);
}
format!(
"{}{}e{}{:02}",
sign,
mant,
if exp < 0 { '-' } else { '+' },
exp.abs()
)
} else if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat(-decpt as usize), digits)
} else if decpt >= ndigits {
format!(
"{}{}{}.0",
sign,
digits,
"0".repeat((decpt - ndigits) as usize)
)
} else {
let d = decpt as usize;
format!("{}{}.{}", sign, &digits[..d], &digits[d..])
}
}
fn py_decimal_str(value: &BigDecimal) -> String {
let (int_val, scale) = value.as_bigint_and_exponent();
let exp = -(scale as i128);
let sign = if int_val.is_negative() { "-" } else { "" };
let int_digits = int_val.abs().to_string(); let len = int_digits.len() as i128;
let leftdigits = exp + len;
let dotplace = if exp <= 0 && leftdigits > -6 {
leftdigits
} else {
1
};
let (intpart, fracpart) = if dotplace <= 0 {
(
"0".to_string(),
format!(".{}{}", "0".repeat(-dotplace as usize), int_digits),
)
} else if dotplace >= len {
(
format!("{}{}", int_digits, "0".repeat((dotplace - len) as usize)),
String::new(),
)
} else {
let d = dotplace as usize;
(int_digits[..d].to_string(), format!(".{}", &int_digits[d..]))
};
let expstr = if leftdigits == dotplace {
String::new()
} else {
let d = leftdigits - dotplace;
format!("E{}{}", if d < 0 { '-' } else { '+' }, d.abs())
};
format!("{}{}{}{}", sign, intpart, fracpart, expstr)
}
fn py_int(s: &str) -> Result<BigInt> {
let err = || N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s));
let t = s.trim();
let (negative, body) = match t.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, t.strip_prefix('+').unwrap_or(t)),
};
if body.is_empty()
|| body.starts_with('_')
|| body.ends_with('_')
|| body.contains("__")
|| !body.chars().all(|c| c.is_ascii_digit() || c == '_')
{
return Err(err());
}
let digits: String = body.chars().filter(|c| *c != '_').collect();
let n: BigInt = digits.parse().map_err(|_| err())?;
Ok(if negative { -n } else { n })
}
fn cardinal_from_str(number: &str) -> Result<String> {
let n = number.trim();
if let Some(rest) = n.strip_prefix('-') {
let inner = cardinal_from_str(rest)?;
return Ok(format!("{}{}", NEGWORD, inner).trim().to_string());
}
match n.find('.') {
Some(dot) => {
let (left, right) = (&n[..dot], &n[dot + 1..]);
let mut ret = format!("{} {}", int_to_word(&py_int(left)?), POINTWORD);
for d in right.chars() {
let mut buf = [0u8; 4];
let digit = py_int(d.encode_utf8(&mut buf))?;
ret.push(' ');
ret.push_str(&int_to_word(&digit));
}
Ok(ret.trim().to_string())
}
None => Ok(int_to_word(&py_int(n)?)),
}
}
pub struct LangLus {
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_fallback: CurrencyForms,
}
impl Default for LangLus {
fn default() -> Self {
Self::new()
}
}
impl LangLus {
pub fn new() -> Self {
let mut currency_forms = HashMap::with_capacity(CURRENCY_FORMS_DECL.len());
for (code, unit, subunit) in CURRENCY_FORMS_DECL {
currency_forms.insert(code, CurrencyForms::new(&unit, &subunit));
}
let (_, unit0, subunit0) = CURRENCY_FORMS_DECL[0];
LangLus {
currency_forms,
currency_fallback: CurrencyForms::new(&unit0, &subunit0),
}
}
fn pick_form(forms: &[String], n: &BigInt) -> String {
if n.is_one() {
forms[0].clone()
} else {
forms[1].clone()
}
}
}
impl Lang for LangLus {
fn cardinal_float_entry(
&self,
value: &crate::floatpath::FloatValue,
precision_override: Option<u32>,
) -> crate::base::Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!("{}-na", self.cardinal_float_entry(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}-na", repr_str))
}
fn inf_result(&self, negative: bool, to: &str) -> Result<String> {
match to {
"ordinal_num" => Ok(format!(
"{}Infinity-na",
if negative { "-" } else { "" }
)),
_ => 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("NaN-na".into()),
_ => Err(N2WError::Value(
"invalid literal for int() with base 10: 'NaN'".into(),
)),
}
}
fn default_currency(&self) -> &str {
"INR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"decimal"
}
fn exclude_title(&self) -> &[String] {
static EXCL: OnceLock<Vec<String>> = OnceLock::new();
EXCL.get_or_init(|| {
vec!["leh".to_string(), "phak".to_string(), "decimal".to_string()]
})
}
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(int_to_word(value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-na", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-na", 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::Float { value, .. } => py_float_repr(*value),
FloatValue::Decimal { value, .. } => py_decimal_str(value),
};
cardinal_from_str(&n)
}
fn lang_name(&self) -> &str {
"Num2Word_LUS"
}
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 == SEPARATOR_UNSET {
SEPARATOR_DEFAULT
} else {
separator
};
let is_negative = val.is_negative();
let abs_val = match val {
CurrencyValue::Int(v) => BigDecimal::from(v.abs()),
CurrencyValue::Decimal { value: d, .. } => d.abs(),
};
let (left, right) = split_currency(&abs_val)?;
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.currency_fallback);
let mut result = format!(
"{} {}",
int_to_word(&left),
LangLus::pick_form(&forms.unit, &left)
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&format!(
"{} {}",
int_to_word(&right),
LangLus::pick_form(&forms.subunit, &right)
));
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}