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::{Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "zarez";
const ONES: [&str; 10] = [
"", "jedan", "dva", "tri", "četiri", "pet", "šest", "sedam", "osam", "devet",
];
const TENS: [&str; 10] = [
"",
"deset",
"dvadeset",
"trideset",
"četrdeset",
"pedeset",
"šezdeset",
"sedamdeset",
"osamdeset",
"devedeset",
];
const FALLBACK_THRESHOLD: u64 = 1_000_000_000;
const SEPARATOR_UNSET: &str = ",";
const FALLBACK_CURRENCY: &str = "BAM";
fn py_int(s: &str) -> Result<BigInt> {
BigInt::from_str(s.trim()).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
s
))
})
}
fn strip_trailing_zeros(mut q: BigInt, mut e: i32) -> (BigInt, i32) {
let ten = BigInt::from(10u32);
while !q.is_zero() {
let (div, rem) = q.div_rem(&ten);
if !rem.is_zero() {
break;
}
q = div;
e += 1;
}
(q, e)
}
fn shortest_digits(f: f64) -> (bool, String, i32) {
let neg = f.is_sign_negative();
let a = f.abs();
if a == 0.0 {
return (neg, "0".to_string(), 1);
}
let bits = a.to_bits();
let biased = ((bits >> 52) & 0x7ff) as i32;
let frac = bits & ((1u64 << 52) - 1);
let (mant, exp2) = if biased == 0 {
(frac, -1074) } else {
(frac | (1u64 << 52), biased - 1075)
};
let (c, k) = if exp2 >= 0 {
(BigInt::from(mant) << (exp2 as usize), 0i32)
} else {
let k = (-exp2) as u32;
(BigInt::from(mant) * BigInt::from(5u32).pow(k), k as i32)
};
let c_digits = c.to_string().len() as i32;
for n in 1..=17i32 {
let drop = c_digits - n;
let (q, e) = if drop <= 0 {
(c.clone(), -k)
} else {
let pow = BigInt::from(10u32).pow(drop as u32);
let (mut q, rem) = c.div_rem(&pow);
let two_rem = &rem * 2u32;
if two_rem > pow || (two_rem == pow && q.is_odd()) {
q += 1u32;
}
(q, drop - k)
};
let (q, e) = strip_trailing_zeros(q, e);
if format!("{}e{}", q, e).parse::<f64>() == Ok(a) {
let digits = q.to_string();
let decpt = digits.len() as i32 + e;
return (neg, digits, decpt);
}
}
let (q, e) = strip_trailing_zeros(c, -k);
let digits = q.to_string();
let decpt = digits.len() as i32 + e;
(neg, digits, decpt)
}
fn py_repr_float(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f.is_sign_negative() {
"-inf".to_string()
} else {
"inf".to_string()
};
}
let (neg, digits, decpt) = shortest_digits(f);
let sign = if neg { "-" } else { "" };
let len = digits.len() as i32;
if decpt <= -4 || decpt > 16 {
let exp = decpt - 1;
let mut mant = String::new();
let mut chars = digits.chars();
mant.push(chars.next().expect("dtoa never returns an empty digit string"));
let rest = chars.as_str();
if !rest.is_empty() {
mant.push('.');
mant.push_str(rest);
}
format!(
"{}{}e{}{:02}",
sign,
mant,
if exp < 0 { '-' } else { '+' },
exp.unsigned_abs()
)
} else if decpt <= 0 {
format!("{}0.{}{}", sign, "0".repeat((-decpt) as usize), digits)
} else if decpt < len {
let cut = decpt as usize;
format!("{}{}.{}", sign, &digits[..cut], &digits[cut..])
} else {
format!(
"{}{}{}.0",
sign,
digits,
"0".repeat((decpt - len) as usize)
)
}
}
fn py_str_decimal(d: &BigDecimal) -> String {
let (coeff, scale) = d.as_bigint_and_exponent();
let sign = if coeff.is_negative() { "-" } else { "" };
let int_digits = coeff.abs().to_string();
let len = int_digits.len() as i64;
let exp = -scale;
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 cut = dotplace as usize;
(
int_digits[..cut].to_string(),
format!(".{}", &int_digits[cut..]),
)
};
let exp_part = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
format!("{}{}{}{}", sign, intpart, fracpart, exp_part)
}
pub struct LangBs {
currency_forms: HashMap<&'static str, CurrencyForms>,
bam: CurrencyForms,
}
impl Default for LangBs {
fn default() -> Self {
Self::new()
}
}
impl LangBs {
pub fn new() -> Self {
let bam = CurrencyForms::new(
&["marka", "marke", "maraka"],
&["feninga", "feninga", "feninga"],
);
let currency_forms: HashMap<&'static str, CurrencyForms> = [
(FALLBACK_CURRENCY, bam.clone()),
(
"EUR",
CurrencyForms::new(&["euro", "eura", "eura"], &["cent", "centa", "centi"]),
),
(
"USD",
CurrencyForms::new(&["dolar", "dolara", "dolara"], &["cent", "centa", "centi"]),
),
]
.into_iter()
.collect();
LangBs { currency_forms, bam }
}
fn select_form<'a>(n: &BigInt, forms: &'a [String]) -> &'a str {
if n == &BigInt::from(1u32) {
return forms[0].as_str();
}
let m10 = n.mod_floor(&BigInt::from(10u32)).to_u32().unwrap_or(0);
let m100 = n.mod_floor(&BigInt::from(100u32)).to_u32().unwrap_or(0);
if matches!(m10, 2 | 3 | 4) && !matches!(m100, 12 | 13 | 14) {
forms[1].as_str()
} else {
forms[2].as_str()
}
}
fn split_value(val: &CurrencyValue) -> (BigInt, u32, bool) {
match val {
CurrencyValue::Int(v) => (v.abs(), 0, v.is_negative()),
CurrencyValue::Decimal { value: d, .. } => {
let is_negative = d.is_negative();
let (digits, scale) = d.as_bigint_and_exponent();
let digits = digits.abs();
if scale <= 0 {
let pow = BigInt::from(10u32).pow((-scale) as u32);
return (digits * pow, 0, is_negative);
}
let pow = BigInt::from(10u32).pow(scale as u32);
let (left, frac) = digits.div_rem(&pow);
let right = (frac * BigInt::from(100u32) / &pow).to_u32().unwrap_or(0);
(left, right, is_negative)
}
}
}
fn cardinal_from_str(&self, s: &str) -> Result<String> {
let n = s.trim();
let (n, mut ret) = match n.strip_prefix('-') {
Some(rest) => (rest, NEGWORD.to_string()),
None => (n, String::new()),
};
match n.split_once('.') {
Some((left, right)) => {
ret.push_str(&self.int_to_word(&py_int(left)?));
ret.push(' ');
ret.push_str(POINTWORD);
ret.push(' ');
for digit in right.chars() {
ret.push_str(&self.int_to_word(&py_int(&digit.to_string())?));
ret.push(' ');
}
Ok(ret.trim().to_string())
}
None => {
ret.push_str(&self.int_to_word(&py_int(n)?));
Ok(ret.trim().to_string())
}
}
}
fn int_to_word(&self, number: &BigInt) -> String {
if number.is_zero() {
return "nula".to_string();
}
if number.is_negative() {
return format!("{}{}", NEGWORD, self.int_to_word(&number.abs()));
}
match number.to_u64() {
Some(n) if n < FALLBACK_THRESHOLD => self.int_to_word_small(n),
_ => number.to_string(),
}
}
fn int_to_word_small(&self, number: u64) -> String {
if number < 10 {
return ONES[number as usize].to_string();
}
if number == 10 {
return TENS[1].to_string();
}
if number < 20 {
return format!("{}aest", ONES[(number - 10) as usize]);
}
if number < 100 {
let tens_val = (number / 10) as usize;
let ones_val = (number % 10) as usize;
return if ones_val == 0 {
TENS[tens_val].to_string()
} else {
format!("{} {}", TENS[tens_val], ONES[ones_val])
};
}
if number < 1000 {
let hundreds_val = (number / 100) as usize;
let remainder = number % 100;
let mut result = match hundreds_val {
1 => "sto".to_string(),
2 => "dvjesto".to_string(),
_ => format!("{}sto", ONES[hundreds_val]),
};
if remainder != 0 {
result.push(' ');
result.push_str(&self.int_to_word_small(remainder));
}
return result;
}
if number < 1_000_000 {
let thousands_val = number / 1000;
let remainder = number % 1000;
let mut result = if thousands_val == 1 {
"hiljada".to_string()
} else if thousands_val < 5 {
format!("{} hiljade", self.int_to_word_small(thousands_val))
} else {
format!("{} hiljada", self.int_to_word_small(thousands_val))
};
if remainder != 0 {
result.push(' ');
result.push_str(&self.int_to_word_small(remainder));
}
return result;
}
let millions_val = number / 1_000_000;
let remainder = number % 1_000_000;
let mut result = if millions_val == 1 {
"milion".to_string()
} else {
format!("{} miliona", self.int_to_word_small(millions_val))
};
if remainder != 0 {
result.push(' ');
result.push_str(&self.int_to_word_small(remainder));
}
result
}
}
impl Lang for LangBs {
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> {
if let Some(i) = value.as_whole_int() {
if i == BigInt::from(1) {
return Ok("prvi".to_string());
}
if i == BigInt::from(2) {
return Ok("drugi".to_string());
}
if i == BigInt::from(3) {
return Ok("treći".to_string());
}
}
Ok(format!("{}.", self.to_cardinal_float(value, None)?))
}
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<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
other => Ok(other),
}
}
fn default_currency(&self) -> &str {
"BAM"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"zarez"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (ret, n) = if value.is_negative() {
(NEGWORD, value.abs())
} else {
("", value.clone())
};
Ok(format!("{}{}", ret, self.int_to_word(&n)).trim().to_string())
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if value == &BigInt::from(1) {
return Ok("prvi".to_string());
}
if value == &BigInt::from(2) {
return Ok("drugi".to_string());
}
if value == &BigInt::from(3) {
return Ok("treći".to_string());
}
Ok(format!("{}.", self.to_cardinal(value)?))
}
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> {
let s = match value {
FloatValue::Float { value, .. } => py_repr_float(*value),
FloatValue::Decimal { value, .. } => py_str_decimal(value),
};
self.cardinal_from_str(&s)
}
fn lang_name(&self) -> &str {
"Num2Word_BS"
}
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 {
" "
} else {
separator
};
let (left, right, is_negative) = Self::split_value(val);
let forms = self.currency_forms.get(currency).unwrap_or(&self.bam);
let left_str = self.int_to_word(&left);
let currency_word = Self::select_form(&left, &forms.unit);
let mut result = format!("{} {}", left_str, currency_word);
if cents && right != 0 {
let right_big = BigInt::from(right);
let cents_str = self.int_to_word(&right_big);
let cents_word = Self::select_form(&right_big, &forms.subunit);
result.push_str(separator);
result.push_str(¢s_str);
result.push(' ');
result.push_str(cents_word);
}
if is_negative {
result.insert_str(0, NEGWORD);
}
Ok(result.trim().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn f(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn d(s: &str) -> FloatValue {
let value = BigDecimal::from_str(s).unwrap();
let precision = value.as_bigint_and_exponent().1.unsigned_abs() as u32;
FloatValue::Decimal { value, precision }
}
fn go(v: &FloatValue) -> String {
LangBs::new().to_cardinal_float(v, None).unwrap()
}
fn err(v: &FloatValue) -> String {
match LangBs::new().to_cardinal_float(v, None) {
Err(N2WError::Value(m)) => m,
other => panic!("expected N2WError::Value, got {:?}", other.map(|s| s)),
}
}
#[test]
fn corpus_cardinal_float_rows() {
assert_eq!(go(&f(0.0, 1)), "nula zarez nula");
assert_eq!(go(&f(0.5, 1)), "nula zarez pet");
assert_eq!(go(&f(1.0, 1)), "jedan zarez nula");
assert_eq!(go(&f(1.5, 1)), "jedan zarez pet");
assert_eq!(go(&f(2.25, 2)), "dva zarez dva pet");
assert_eq!(go(&f(3.14, 2)), "tri zarez jedan četiri");
assert_eq!(go(&f(0.01, 2)), "nula zarez nula jedan");
assert_eq!(go(&f(0.1, 1)), "nula zarez jedan");
assert_eq!(go(&f(0.99, 2)), "nula zarez devet devet");
assert_eq!(go(&f(1.01, 2)), "jedan zarez nula jedan");
assert_eq!(go(&f(12.34, 2)), "dvaaest zarez tri četiri");
assert_eq!(go(&f(99.99, 2)), "devedeset devet zarez devet devet");
assert_eq!(go(&f(100.5, 1)), "sto zarez pet");
assert_eq!(
go(&f(1234.56, 2)),
"hiljada dvjesto trideset četiri zarez pet šest"
);
assert_eq!(go(&f(-0.5, 1)), "minus nula zarez pet");
assert_eq!(go(&f(-1.5, 1)), "minus jedan zarez pet");
assert_eq!(go(&f(-12.34, 2)), "minus dvaaest zarez tri četiri");
assert_eq!(go(&f(1.005, 3)), "jedan zarez nula nula pet");
assert_eq!(go(&f(2.675, 3)), "dva zarez šest sedam pet");
}
#[test]
fn corpus_cardinal_dec_rows() {
assert_eq!(go(&d("0.01")), "nula zarez nula jedan");
assert_eq!(go(&d("1.10")), "jedan zarez jedan nula");
assert_eq!(go(&d("12.345")), "dvaaest zarez tri četiri pet");
assert_eq!(
go(&d("98746251323029.99")),
"98746251323029 zarez devet devet"
);
assert_eq!(go(&d("0.001")), "nula zarez nula nula jedan");
}
#[test]
fn float_artefact_cases_go_through_repr_not_float2tuple() {
assert_eq!(py_repr_float(2.675), "2.675");
assert_eq!(py_repr_float(1.005), "1.005");
assert_eq!((2.675f64 - 2.0).abs() * 1000.0, 674.9999999999998);
assert_eq!(go(&f(2.675, 3)), "dva zarez šest sedam pet");
}
#[test]
fn negative_zero_float() {
assert_eq!(py_repr_float(-0.0), "-0.0");
assert_eq!(go(&f(-0.0, 1)), "minus nula zarez nula");
}
#[test]
fn shortest_digits_breaks_exact_ties_to_even_like_dtoa() {
assert_eq!(py_repr_float(953749603507345.2), "953749603507345.2");
assert_eq!(format!("{}", 953749603507345.2f64), "953749603507345.3");
assert_eq!(py_repr_float(14325669370808.562), "14325669370808.562");
assert_eq!(format!("{}", 14325669370808.562f64), "14325669370808.563");
assert_eq!(py_repr_float(-91334146377350.12), "-91334146377350.12");
assert_eq!(format!("{}", -91334146377350.12f64), "-91334146377350.13");
assert_eq!(go(&f(953749603507345.2, 1)), "953749603507345 zarez dva");
assert_eq!(
go(&f(-91334146377350.12, 2)),
"minus 91334146377350 zarez jedan dva"
);
}
#[test]
fn shortest_not_closest() {
assert_eq!(py_repr_float(1e23), "1e+23");
assert_eq!(py_repr_float(1e22), "1e+22");
assert_eq!(py_repr_float(5e-324), "5e-324");
assert_eq!(py_repr_float(f64::MAX), "1.7976931348623157e+308");
}
#[test]
fn negative_zero_decimal_loses_its_sign_upstream() {
assert!(!BigDecimal::from_str("-0.00").unwrap().is_negative());
assert_eq!(go(&d("-0.00")), "nula zarez nula nula");
assert_eq!(go(&d("-0.5")), "minus nula zarez pet");
assert_eq!(go(&d("-0.01")), "minus nula zarez nula jedan");
}
#[test]
fn add_dot_0_keeps_integral_floats_on_the_fraction_branch() {
assert_eq!(py_repr_float(1e15), "1000000000000000.0");
assert_eq!(go(&f(1e15, 1)), "1000000000000000 zarez nula");
assert_eq!(py_repr_float(100.0), "100.0");
assert_eq!(go(&f(100.0, 1)), "sto zarez nula");
}
#[test]
fn large_values_fall_back_to_digits_on_the_left() {
assert_eq!(go(&f(1000000000.5, 1)), "1000000000 zarez pet");
assert_eq!(go(&f(1234567890.5, 1)), "1234567890 zarez pet");
assert_eq!(go(&f(-1000000000.5, 1)), "minus 1000000000 zarez pet");
assert_eq!(
go(&f(123456789.25, 2)),
"sto dvadeset tri miliona četiristo pedeset šest hiljada \
sedamsto osamdeset devet zarez dva pet"
);
assert_eq!(
go(&d("123456789012345678901.5")),
"123456789012345678901 zarez pet"
);
}
#[test]
fn exponent_notation_without_a_dot_raises_valueerror_naming_the_literal() {
assert_eq!(py_repr_float(1e16), "1e+16");
assert_eq!(
err(&f(1e16, 16)),
"invalid literal for int() with base 10: '1e+16'"
);
assert_eq!(py_repr_float(1e-5), "1e-05");
assert_eq!(
err(&f(1e-5, 5)),
"invalid literal for int() with base 10: '1e-05'"
);
assert_eq!(
err(&f(1e100, 0)),
"invalid literal for int() with base 10: '1e+100'"
);
assert_eq!(
err(&f(1e-300, 300)),
"invalid literal for int() with base 10: '1e-300'"
);
assert_eq!(py_str_decimal(&BigDecimal::from_str("1E+2").unwrap()), "1E+2");
assert_eq!(
err(&d("1E+2")),
"invalid literal for int() with base 10: '1E+2'"
);
assert_eq!(py_str_decimal(&BigDecimal::from_str("1e-7").unwrap()), "1E-7");
assert_eq!(
err(&d("1e-7")),
"invalid literal for int() with base 10: '1E-7'"
);
}
#[test]
fn exponent_notation_with_a_dot_raises_valueerror_naming_the_letter() {
assert_eq!(py_repr_float(1.5e-5), "1.5e-05");
assert_eq!(err(&f(1.5e-5, 6)), "invalid literal for int() with base 10: 'e'");
assert_eq!(py_repr_float(1.5e16), "1.5e+16");
assert_eq!(err(&f(1.5e16, 0)), "invalid literal for int() with base 10: 'e'");
assert_eq!(
py_str_decimal(&BigDecimal::from_str("1.5E+3").unwrap()),
"1.5E+3"
);
assert_eq!(err(&d("1.5E+3")), "invalid literal for int() with base 10: 'E'");
}
#[test]
fn nan_and_inf() {
assert_eq!(py_repr_float(f64::NAN), "nan");
assert_eq!(py_repr_float(-f64::NAN), "nan");
assert_eq!(py_repr_float(f64::INFINITY), "inf");
assert_eq!(py_repr_float(f64::NEG_INFINITY), "-inf");
assert_eq!(
err(&f(f64::NAN, 0)),
"invalid literal for int() with base 10: 'nan'"
);
assert_eq!(
err(&f(f64::INFINITY, 0)),
"invalid literal for int() with base 10: 'inf'"
);
assert_eq!(
err(&f(f64::NEG_INFINITY, 0)),
"invalid literal for int() with base 10: 'inf'"
);
}
#[test]
fn small_positional_boundary() {
assert_eq!(py_repr_float(1e-4), "0.0001");
assert_eq!(go(&f(1e-4, 4)), "nula zarez nula nula nula jedan");
assert_eq!(go(&d("1e-6")), "nula zarez nula nula nula nula nula jedan");
}
#[test]
fn decimal_scale_is_preserved() {
assert_eq!(py_str_decimal(&BigDecimal::from_str("5.00").unwrap()), "5.00");
assert_eq!(go(&d("5.00")), "pet zarez nula nula");
assert_eq!(go(&d("0.0")), "nula zarez nula");
assert_eq!(go(&d("-0.5")), "minus nula zarez pet");
assert_eq!(go(&d("-12.34")), "minus dvaaest zarez tri četiri");
assert_eq!(py_str_decimal(&BigDecimal::from_str("100").unwrap()), "100");
assert_eq!(go(&d("100")), "sto");
assert_eq!(go(&d("2.675")), "dva zarez šest sedam pet");
assert_eq!(go(&d("1.005")), "jedan zarez nula nula pet");
}
#[test]
fn precision_override_is_ignored() {
let l = LangBs::new();
let v = f(1.005, 3);
for p in [None, Some(0), Some(1), Some(3), Some(5)] {
assert_eq!(
l.to_cardinal_float(&v, p).unwrap(),
"jedan zarez nula nula pet",
"precision={:?} must not change BS's output",
p
);
}
assert_eq!(go(&f(12.34, 99)), "dvaaest zarez tri četiri");
}
#[test]
fn integers_are_unaffected() {
let l = LangBs::new();
assert_eq!(l.to_cardinal(&BigInt::from(12)).unwrap(), "dvaaest");
assert_eq!(l.to_cardinal(&BigInt::from(0)).unwrap(), "nula");
assert_eq!(l.to_cardinal(&BigInt::from(-12)).unwrap(), "minus dvaaest");
}
}