use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::cmp::Ordering;
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "point";
const ONES: [&str; 10] = [
"", "en", "de", "twa", "kat", "senk", "sis", "sèt", "uit", "nèf",
];
const TENS: [&str; 10] = [
"",
"dis",
"ven",
"trant",
"karant",
"senkant",
"swasant",
"swasantdis",
"katreven",
"katrevendis",
];
const HUNDRED: &str = "san";
const THOUSAND: &str = "mil";
const MILLION: &str = "milyon";
const HT_SEPARATOR: &str = " ";
const SEPARATOR_UNSET: &str = ",";
fn billion() -> BigInt {
BigInt::from(1_000_000_000u64)
}
fn int_to_word(number: &BigInt) -> String {
if number.is_zero() {
return "zero".to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, int_to_word(&number.abs()));
}
if number >= &billion() {
return number.to_string();
}
let n = number
.to_u64()
.expect("bounded above by 10^9 and below by 1 — narrowing is total");
int_to_word_small(n)
}
fn int_to_word_small(n: u64) -> String {
if n == 0 {
return "zero".to_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 < 1000 {
let hundreds_val = (n / 100) as usize;
let remainder = n % 100;
let mut result = format!("{} {}", ONES[hundreds_val], HUNDRED);
if remainder != 0 {
result.push(' ');
result.push_str(&int_to_word_small(remainder));
}
result
} else if n < 1_000_000 {
let thousands_val = n / 1000;
let remainder = n % 1000;
let mut result = format!("{} {}", int_to_word_small(thousands_val), THOUSAND);
if remainder != 0 {
result.push(' ');
result.push_str(&int_to_word_small(remainder));
}
result
} else {
let millions_val = n / 1_000_000;
let remainder = n % 1_000_000;
let mut result = format!("{} {}", int_to_word_small(millions_val), MILLION);
if remainder != 0 {
result.push(' ');
result.push_str(&int_to_word_small(remainder));
}
result
}
}
fn split_currency_parts(val: &CurrencyValue) -> Result<(BigInt, BigInt)> {
let d = match val {
CurrencyValue::Int(v) => return Ok((v.abs(), BigInt::zero())),
CurrencyValue::Decimal { value: d, .. } => d.abs(),
};
let (mantissa, scale) = d.as_bigint_and_exponent();
if scale <= 0 {
if scale == 0 {
return Ok((mantissa, BigInt::zero()));
}
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
d
)));
}
let digits = mantissa.to_string();
let scale = scale as usize;
let padded = if digits.len() <= scale {
format!("{}{}", "0".repeat(scale + 1 - digits.len()), digits)
} else {
digits
};
let split_at = padded.len() - scale;
let int_part: String = padded.chars().take(split_at).collect();
let frac_part: String = padded.chars().skip(split_at).collect();
let left = BigInt::from_str(&int_part).map_err(|e| N2WError::Value(e.to_string()))?;
let mut frac: String = frac_part.chars().take(2).collect();
while frac.chars().count() < 2 {
frac.push('0');
}
let right = BigInt::from_str(&frac).map_err(|e| N2WError::Value(e.to_string()))?;
Ok((left, right))
}
fn python_nonfinite_str(v: f64) -> String {
if v.is_nan() {
"nan".to_string()
} else if v > 0.0 {
"inf".to_string()
} else {
"-inf".to_string()
}
}
fn py_float_sci_string(v: f64) -> String {
let s = format!("{:e}", v);
match s.split_once('e') {
Some((mant, exp)) => {
let (sign, mag) = match exp.strip_prefix('-') {
Some(rest) => ("-", rest),
None => ("+", exp),
};
let mag = if mag.len() < 2 {
format!("0{}", mag)
} else {
mag.to_string()
};
format!("{}e{}{}", mant, sign, mag)
}
None => s,
}
}
fn decimal_scientific_string(mantissa: &BigInt, scale: i64) -> String {
let exp = -scale;
let negative = mantissa.is_negative();
let digits = mantissa.abs().to_string();
let adjusted = exp + digits.chars().count() as i64 - 1;
let mut s = String::new();
if negative {
s.push('-');
}
let mut chars = digits.chars();
s.push(chars.next().unwrap_or('0'));
let rest: String = chars.collect();
if !rest.is_empty() {
s.push('.');
s.push_str(&rest);
}
s.push('E');
if adjusted >= 0 {
s.push('+');
}
s.push_str(&adjusted.to_string());
s
}
fn scientific_int_token(value: &FloatValue) -> Option<String> {
match value {
FloatValue::Float { value: v, .. } => {
if !v.is_finite() {
return Some(python_nonfinite_str(*v));
}
if *v == 0.0 {
return None;
}
let a = v.abs();
if a >= 1e16 || a < 1e-4 {
Some(py_float_sci_string(*v))
} else {
None
}
}
FloatValue::Decimal { value: d, .. } => {
let (mantissa, scale) = d.as_bigint_and_exponent();
let exp = -scale;
let ndigits = if mantissa.is_zero() {
1
} else {
mantissa.abs().to_string().chars().count() as i64
};
let adjusted = exp + ndigits - 1;
if exp <= 0 && adjusted >= -6 {
None
} else {
Some(decimal_scientific_string(&mantissa, scale))
}
}
}
}
fn f64_to_exact_bigdecimal(v: f64) -> BigDecimal {
debug_assert!(v.is_finite() && v >= 0.0);
if v == 0.0 {
return BigDecimal::from(0);
}
let bits = v.to_bits();
let exp_field = ((bits >> 52) & 0x7ff) as i64;
let mant_field = (bits & 0x000f_ffff_ffff_ffff) as u64;
let (mant, exp2) = if exp_field == 0 {
(mant_field, -1074) } else {
(mant_field | 0x0010_0000_0000_0000, exp_field - 1075)
};
let mant_bi = BigInt::from(mant);
if exp2 >= 0 {
BigDecimal::from(mant_bi * BigInt::from(2).pow(exp2 as u32))
} else {
let k = (-exp2) as u32;
BigDecimal::new(mant_bi * BigInt::from(5).pow(k), k as i64)
}
}
fn round_half_even_nonneg(bd: &BigDecimal) -> BigInt {
let (mant, scale) = bd.as_bigint_and_exponent();
if scale <= 0 {
return mant * BigInt::from(10).pow((-scale) as u32);
}
let divisor = BigInt::from(10).pow(scale as u32);
let floor = &mant / &divisor;
let rem = &mant % &divisor;
match (BigInt::from(2) * &rem).cmp(&divisor) {
Ordering::Less => floor,
Ordering::Greater => &floor + BigInt::one(),
Ordering::Equal => {
if (&floor % BigInt::from(2)).is_zero() {
floor
} else {
&floor + BigInt::one()
}
}
}
}
fn positional_parts(value: &FloatValue) -> (bool, BigInt, Option<String>) {
let (exact_abs, precision, neg) = match value {
FloatValue::Float { value: v, precision } => {
(f64_to_exact_bigdecimal(v.abs()), *precision, v.is_sign_negative())
}
FloatValue::Decimal { value: d, precision } => {
(d.abs(), *precision, d.is_negative())
}
};
let (mant, scale) = exact_abs.as_bigint_and_exponent();
let pre = if scale <= 0 {
mant.clone() * BigInt::from(10).pow((-scale) as u32)
} else {
&mant / &BigInt::from(10).pow(scale as u32)
};
if precision == 0 {
return (neg, pre, None);
}
let frac_scaled = (&exact_abs - BigDecimal::from(pre.clone()))
* BigDecimal::from(BigInt::from(10).pow(precision));
let post = round_half_even_nonneg(&frac_scaled);
let mut frac = post.to_string();
let p = precision as usize;
if frac.chars().count() < p {
frac = format!("{}{}", "0".repeat(p - frac.chars().count()), frac);
}
(neg, pre, Some(frac))
}
pub struct LangHt {
currency_forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl Default for LangHt {
fn default() -> Self {
Self::new()
}
}
impl LangHt {
pub fn new() -> Self {
let mut currency_forms = HashMap::new();
currency_forms.insert(
"HTG",
CurrencyForms::new(&["goud", "goud"], &["santim", "santim"]),
);
currency_forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
currency_forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euros"], &["cent", "cents"]),
);
LangHt {
fallback_forms: currency_forms["HTG"].clone(),
currency_forms,
}
}
}
impl Lang for LangHt {
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> {
let cardinal = self.cardinal_float_entry(value, None)?;
Ok(format!("{}-yèm", cardinal))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", repr_str))
}
fn str_to_number(&self, s: &str) -> Result<crate::strnum::ParsedNumber> {
match crate::strnum::python_decimal_parse(s)? {
crate::strnum::ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
p => Ok(p),
}
}
fn default_currency(&self) -> &str {
"HTG"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"point"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (ret, magnitude) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", ret, int_to_word(&magnitude))
.trim()
.to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
let cardinal = self.to_cardinal(value)?;
Ok(format!("{}-yèm", cardinal))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}.", value))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
if let Some(token) = scientific_int_token(value) {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
token
)));
}
let (neg, pre, frac) = positional_parts(value);
let mut ret = String::new();
if neg {
ret.push_str(NEGWORD); }
ret.push_str(&int_to_word(&pre));
if let Some(frac) = frac {
ret.push(' ');
ret.push_str(POINTWORD); for ch in frac.chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
ch
))
})?;
ret.push(' ');
ret.push_str(&int_to_word(&BigInt::from(d)));
}
}
Ok(ret.trim().to_string())
}
fn lang_name(&self) -> &str {
"Num2Word_HT"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
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 separator = if separator == SEPARATOR_UNSET {
HT_SEPARATOR
} else {
separator
};
let _ = adjective;
let is_negative = val.is_negative();
let (left, right) = split_currency_parts(val)?;
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.fallback_forms);
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let one = BigInt::one();
let mut result = format!(
"{} {}",
int_to_word(&left),
if left != one { &cr1[1] } else { &cr1[0] }
);
if cents && !right.is_zero() {
result.push_str(separator);
result.push_str(&int_to_word(&right));
result.push(' ');
result.push_str(if right != one { &cr2[1] } else { &cr2[0] });
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}