use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, ParsedNumber};
use num_bigint::BigInt;
use num_traits::{Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const ONES: [&str; 10] = [
"", "bir", "iki", "üç", "dört", "bäş", "alty", "ýedi", "sekiz", "dokuz",
];
const TENS: [&str; 10] = [
"", "on", "ýigrimi", "otuz", "kyrk", "elli", "altmyş", "ýetmiş", "segsen", "togsan",
];
const HUNDRED: &str = "ýüz";
const THOUSAND: &str = "müň";
const MILLION: &str = "million";
const NEGWORD: &str = "minus ";
const MILLIARD: u64 = 1_000_000_000;
fn int_to_word_small(n: u64) -> String {
debug_assert!(n >= 1 && n < MILLIARD);
if n < 10 {
return ONES[n as usize].to_string();
}
if n < 100 {
let tens_val = (n / 10) as usize;
let ones_val = (n % 10) as usize;
if ones_val == 0 {
return TENS[tens_val].to_string();
}
return format!("{} {}", TENS[tens_val], ONES[ones_val]);
}
if n < 1_000 {
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));
}
return result;
}
if n < 1_000_000 {
let thousands_val = n / 1_000;
let remainder = n % 1_000;
let mut result = format!("{} {}", int_to_word_small(thousands_val), THOUSAND);
if remainder != 0 {
result.push(' ');
result.push_str(&int_to_word_small(remainder));
}
return result;
}
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 int_to_word(n: &BigInt) -> String {
if n.is_zero() {
return "zero".to_string();
}
if n.is_negative() {
return format!("{}{}", NEGWORD, int_to_word(&n.abs()));
}
match n.to_u64() {
Some(v) if v < MILLIARD => int_to_word_small(v),
_ => n.to_string(),
}
}
fn python_float_repr(v: f64) -> String {
if v.is_nan() {
return "nan".to_string();
}
if v.is_infinite() {
return (if v.is_sign_negative() { "-inf" } else { "inf" }).to_string();
}
let sign = if v.is_sign_negative() { "-" } else { "" };
let a = v.abs();
let s = format!("{:e}", a);
let (mant, exp) = s.split_once('e').expect("LowerExp always emits an 'e'");
let exp: i32 = exp.parse().expect("LowerExp emits an integer exponent");
let mut digits: String = mant.chars().filter(|c| *c != '.').collect();
let mut decpt = exp + 1;
let frac_digits = digits.chars().count() as i32 - decpt;
if frac_digits > 0 && a != 0.0 {
let t = format!("{:.*}", frac_digits as usize, a);
let (ip, fp) = t.split_once('.').expect("frac_digits > 0 forces a point");
let all = format!("{}{}", ip, fp);
let trimmed = all.trim_start_matches('0');
if !trimmed.is_empty() {
let lead = all.chars().count() - trimmed.chars().count();
digits = trimmed.to_string();
decpt = ip.chars().count() as i32 - lead as i32;
}
}
let n = digits.chars().count() as i32;
if decpt <= -4 || decpt > 16 {
let e = decpt - 1;
let mut out = String::from(sign);
let mut it = digits.chars();
out.push(it.next().expect("a finite double has at least one digit"));
if n > 1 {
out.push('.');
out.push_str(it.as_str());
}
out.push('e');
out.push(if e < 0 { '-' } else { '+' });
out.push_str(&format!("{:02}", (e as i64).abs()));
out
} else if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat((-decpt) as usize), digits)
} else if decpt >= n {
format!("{}{}{}.0", sign, digits, "0".repeat((decpt - n) as usize))
} else {
let k = decpt as usize;
format!(
"{}{}.{}",
sign,
digits.chars().take(k).collect::<String>(),
digits.chars().skip(k).collect::<String>()
)
}
}
fn python_str(v: &FloatValue) -> String {
match v {
FloatValue::Float { value, .. } => python_float_repr(*value),
FloatValue::Decimal { value, .. } => crate::strnum::python_decimal_str(value),
}
}
fn tk_cardinal_from_str(n: &str, negword: &str, pointword: &str) -> Result<String> {
let n = n.trim();
let (n, mut ret) = match n.strip_prefix('-') {
Some(rest) => (rest, negword.to_string()),
None => (n, String::new()),
};
let int_word = |field: &str| -> Result<String> {
let val = BigInt::from_str(field).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
field
))
})?;
Ok(int_to_word(&val))
};
match n.find('.') {
Some(dot) => {
let left = &n[..dot];
let right = &n[dot + 1..];
ret.push_str(&int_word(left)?);
ret.push(' ');
ret.push_str(pointword);
ret.push(' ');
for ch in right.chars() {
ret.push_str(&int_word(&ch.to_string())?);
ret.push(' ');
}
Ok(ret.trim().to_string())
}
None => {
ret.push_str(&int_word(n)?);
Ok(ret.trim().to_string())
}
}
}
const FALLBACK_CURRENCY: &str = "TMT";
const SEPARATOR_UNSET: &str = ",";
const SEPARATOR_DEFAULT: &str = " ";
pub struct LangTk {
forms: HashMap<&'static str, CurrencyForms>,
}
impl LangTk {
pub fn new() -> Self {
let mut forms = HashMap::with_capacity(3);
forms.insert(
"TMT",
CurrencyForms::new(&["manat", "manat"], &["teňňe", "teňňe"]),
);
forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euros"], &["cent", "cents"]),
);
LangTk { forms }
}
}
impl Default for LangTk {
fn default() -> Self {
Self::new()
}
}
impl Lang for LangTk {
fn default_currency(&self) -> &str {
"TMT"
}
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, n) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", ret, int_to_word(&n)).trim().to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-nji", self.to_cardinal(value)?))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}.", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
tk_cardinal_from_str(&python_str(value), self.negword(), self.pointword())
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
Ok(format!("{}-nji", self.cardinal_float_entry(value, None)?))
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("{}.", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.cardinal_float_entry(value, None)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
other => Ok(other),
}
}
fn lang_name(&self) -> &str {
"Num2Word_TK"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.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 _ = adjective;
let separator = if separator == SEPARATOR_UNSET {
SEPARATOR_DEFAULT
} else {
separator
};
let is_negative = val.is_negative();
let s = match val {
CurrencyValue::Int(i) => if is_negative { i.abs() } else { i.clone() }.to_string(),
CurrencyValue::Decimal { value: d, .. } => if is_negative { d.abs() } else { d.clone() }.to_string(),
};
let mut parts = s.split('.');
let p0 = parts.next().unwrap_or("");
let p1 = parts.next();
let left = if p0.is_empty() {
BigInt::zero()
} else {
BigInt::from_str(p0).map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", p0))
})?
};
let right = match p1 {
Some(frac) if !frac.is_empty() => {
let mut f: String = frac.chars().take(2).collect();
while f.chars().count() < 2 {
f.push('0'); }
BigInt::from_str(&f).map_err(|_| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", f))
})?
}
_ => BigInt::zero(),
};
let forms = self.forms.get(currency).unwrap_or_else(|| {
self.forms
.get(FALLBACK_CURRENCY)
.expect("new() always inserts TMT")
});
let (cr1, cr2) = (&forms.unit, &forms.subunit);
let one = BigInt::from(1);
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(&format!(
"{} {}",
int_to_word(&right),
if right != one { &cr2[1] } else { &cr2[0] }
));
}
if is_negative {
result = format!("{}{}", NEGWORD, result);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use bigdecimal::BigDecimal;
fn card_float(v: f64, precision: u32) -> String {
LangTk::new()
.to_cardinal_float(&FloatValue::Float { value: v, precision }, None)
.unwrap()
}
fn card_dec(s: &str) -> String {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.max(0) as u32;
LangTk::new()
.to_cardinal_float(&FloatValue::Decimal { value, precision }, None)
.unwrap()
}
#[test]
fn corpus_floats() {
let cases: &[(f64, u32, &str)] = &[
(0.0, 1, "zero point zero"),
(0.5, 1, "zero point bäş"),
(1.0, 1, "bir point zero"),
(1.5, 1, "bir point bäş"),
(2.25, 2, "iki point iki bäş"),
(3.14, 2, "üç point bir dört"),
(0.01, 2, "zero point zero bir"),
(0.1, 1, "zero point bir"),
(0.99, 2, "zero point dokuz dokuz"),
(1.01, 2, "bir point zero bir"),
(12.34, 2, "on iki point üç dört"),
(99.99, 2, "togsan dokuz point dokuz dokuz"),
(100.5, 1, "bir ýüz point bäş"),
(1234.56, 2, "bir müň iki ýüz otuz dört point bäş alty"),
(-0.5, 1, "minus zero point bäş"),
(-1.5, 1, "minus bir point bäş"),
(-12.34, 2, "minus on iki point üç dört"),
(1.005, 3, "bir point zero zero bäş"),
(2.675, 3, "iki point alty ýedi bäş"),
];
for (v, p, want) in cases {
assert_eq!(&card_float(*v, *p), want, "float {}", v);
}
}
#[test]
fn corpus_decimals() {
let cases: &[(&str, &str)] = &[
("0.01", "zero point zero bir"),
("1.10", "bir point bir zero"),
("12.345", "on iki point üç dört bäş"),
("98746251323029.99", "98746251323029 point dokuz dokuz"),
("0.001", "zero point zero zero bir"),
];
for (s, want) in cases {
assert_eq!(&card_dec(s), want, "decimal {}", s);
}
}
#[test]
fn precision_override_ignored() {
let got = LangTk::new()
.to_cardinal_float(
&FloatValue::Float { value: 2.675, precision: 3 },
Some(1),
)
.unwrap();
assert_eq!(got, "iki point alty ýedi bäş");
}
fn fv_f(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn fv_d(s: &str, precision: u32) -> FloatValue {
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
}
}
#[test]
fn corpus_float_entry_rows() {
let l = LangTk::new();
assert_eq!(
l.cardinal_float_entry(&fv_f(5.0, 1), None).unwrap(),
"bäş point zero"
);
assert_eq!(
l.cardinal_float_entry(&fv_f(-0.0, 1), None).unwrap(),
"minus zero point zero"
);
assert_eq!(
l.cardinal_float_entry(&fv_f(1234.0, 1), None).unwrap(),
"bir müň iki ýüz otuz dört point zero"
);
assert_eq!(
l.cardinal_float_entry(&fv_f(1e9, 1), None).unwrap(),
"1000000000 point zero"
);
assert_eq!(l.cardinal_float_entry(&fv_d("5", 0), None).unwrap(), "bäş");
assert_eq!(
l.cardinal_float_entry(&fv_d("5.00", 2), None).unwrap(),
"bäş point zero zero"
);
assert_eq!(
l.cardinal_float_entry(&fv_d("12345.000", 3), None).unwrap(),
"on iki müň üç ýüz kyrk bäş point zero zero zero"
);
for v in [
l.cardinal_float_entry(&fv_f(1e16, 16), None),
l.cardinal_float_entry(&fv_f(1e20, 20), None),
l.cardinal_float_entry(&fv_d("1E+2", 0), None),
l.cardinal_float_entry(&fv_d("1E+20", 0), None),
] {
assert!(matches!(v, Err(N2WError::Value(_))), "{v:?}");
}
}
#[test]
fn corpus_ordinal_entry_rows() {
let l = LangTk::new();
assert_eq!(
l.ordinal_float_entry(&fv_f(1.0, 1)).unwrap(),
"bir point zero-nji"
);
assert_eq!(
l.ordinal_float_entry(&fv_f(-0.0, 1)).unwrap(),
"minus zero point zero-nji"
);
assert_eq!(l.ordinal_float_entry(&fv_d("0", 0)).unwrap(), "zero-nji");
assert_eq!(l.ordinal_float_entry(&fv_d("5", 0)).unwrap(), "bäş-nji");
assert_eq!(
l.ordinal_float_entry(&fv_d("100", 0)).unwrap(),
"bir ýüz-nji"
);
assert_eq!(
l.ordinal_float_entry(&fv_f(3.25, 2)).unwrap(),
"üç point iki bäş-nji"
);
assert!(matches!(
l.ordinal_float_entry(&fv_f(1e16, 16)),
Err(N2WError::Value(_))
));
assert_eq!(
l.ordinal_num_float_entry(&fv_f(1e16, 16), "1e+16").unwrap(),
"1e+16."
);
assert_eq!(
l.ordinal_num_float_entry(&fv_d("5.00", 2), "5.00").unwrap(),
"5.00."
);
assert_eq!(
l.year_float_entry(&fv_f(5.0, 1)).unwrap(),
"bäş point zero"
);
assert!(matches!(
l.year_float_entry(&fv_d("1E+2", 0)),
Err(N2WError::Value(_))
));
}
#[test]
fn corpus_string_rows() {
let l = LangTk::new();
let parsed = l.str_to_number("1e3").unwrap();
match parsed {
ParsedNumber::Dec(d) => {
let fv = FloatValue::Decimal { value: d, precision: 0 };
assert!(matches!(
l.cardinal_float_entry(&fv, None),
Err(N2WError::Value(_))
));
}
other => panic!("expected Dec, got {other:?}"),
}
assert!(matches!(
l.str_to_number("Infinity"),
Err(N2WError::Value(_))
));
assert!(matches!(
l.str_to_number("-Infinity"),
Err(N2WError::Value(_))
));
assert!(matches!(l.str_to_number("NaN"), Ok(ParsedNumber::NaN)));
}
#[test]
fn float_repr_matches_cpython() {
for (v, want) in [
(0.0, "0.0"),
(-0.0, "-0.0"),
(1.0, "1.0"),
(0.5, "0.5"),
(1234.0, "1234.0"),
(1000000000000000.0, "1000000000000000.0"),
(1e16, "1e+16"),
(1e20, "1e+20"),
(2.675, "2.675"),
(1.005, "1.005"),
(123456789.5, "123456789.5"),
] {
assert_eq!(python_float_repr(v), want);
}
}
}