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_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
use std::str::FromStr;
const NEGWORD: &str = "minus ";
const POINTWORD: &str = "point";
const ONES: [&str; 10] = [
"", "unan", "daou", "tri", "pevar", "pemp", "c'hwec'h", "seizh", "eizh", "nav",
];
const TENS: [&str; 10] = [
"",
"dek",
"ugent",
"tregont",
"daou-ugent",
"hanter-kant",
"tri-ugent",
"dek ha tri-ugent",
"pevar-ugent",
"dek ha pevar-ugent",
];
const DEFAULT_SEPARATOR: &str = " ";
const BASE_DEFAULT_SEPARATOR: &str = ",";
const HUNDRED: &str = "kant";
const THOUSAND: &str = "mil";
const MILLION: &str = "milion";
fn digit(n: &BigInt) -> usize {
n.to_usize().expect("guarded to 0..=9 by the caller")
}
pub 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 < BigInt::from(10u32) {
return ONES[digit(number)].to_string();
}
if *number < BigInt::from(100u32) {
let tens_val = number / 10u32;
let ones_val = number % 10u32;
if ones_val.is_zero() {
return TENS[digit(&tens_val)].to_string();
}
return format!("{} {}", TENS[digit(&tens_val)], ONES[digit(&ones_val)]);
}
if *number < BigInt::from(1_000u32) {
let hundreds_val = number / 100u32;
let remainder = number % 100u32;
let mut result = format!("{} {}", ONES[digit(&hundreds_val)], HUNDRED);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
if *number < BigInt::from(1_000_000u32) {
let thousands_val = number / 1_000u32;
let remainder = number % 1_000u32;
let mut result = format!("{} {}", int_to_word(&thousands_val), THOUSAND);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
if *number < BigInt::from(1_000_000_000u32) {
let millions_val = number / 1_000_000u32;
let remainder = number % 1_000_000u32;
let mut result = format!("{} {}", int_to_word(&millions_val), MILLION);
if !remainder.is_zero() {
result.push(' ');
result.push_str(&int_to_word(&remainder));
}
return result;
}
number.to_string()
}
fn py_int(s: &str) -> Result<BigInt> {
BigInt::from_str(s)
.map_err(|_| N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s)))
}
fn py_float_str(v: f64) -> String {
if v.is_nan() {
return "nan".to_string();
}
if v.is_infinite() {
return if v < 0.0 { "-inf" } else { "inf" }.to_string();
}
let sign = if v.is_sign_negative() { "-" } else { "" };
let shortest = format!("{:e}", v.abs());
let ndigits = shortest
.split_once('e')
.expect("{:e} always emits an exponent marker")
.0
.chars()
.filter(char::is_ascii_digit)
.count();
let exact = format!("{:.*e}", ndigits.saturating_sub(1), v.abs());
let (mant, exp) = exact
.split_once('e')
.expect("{:e} always emits an exponent marker");
let exp: i32 = exp.parse().expect("{:e} always emits a decimal exponent");
let digits: String = mant.chars().filter(|c| *c != '.').collect();
let digits_len = digits.chars().count() as i32;
let decpt = exp + 1;
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("dtoa never returns an empty digit string"));
let rest: String = it.collect();
if !rest.is_empty() {
out.push('.');
out.push_str(&rest);
}
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 >= digits_len {
format!(
"{}{}{}.0",
sign,
digits,
"0".repeat((decpt - digits_len) as usize)
)
} else {
let head: String = digits.chars().take(decpt as usize).collect();
let tail: String = digits.chars().skip(decpt as usize).collect();
format!("{}{}.{}", sign, head, tail)
}
}
fn py_decimal_str(d: &BigDecimal) -> String {
let (coefficient, scale) = d.as_bigint_and_exponent();
let exp = -scale; let sign = if coefficient.is_negative() { "-" } else { "" };
let int_digits = coefficient.abs().to_string();
let len = int_digits.chars().count() as i64;
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 head: String = int_digits.chars().take(dotplace as usize).collect();
let tail: String = int_digits.chars().skip(dotplace as usize).collect();
(head, format!(".{}", tail))
};
let expstr = if leftdigits == dotplace {
String::new()
} else {
let e = leftdigits - dotplace;
format!("E{}{}", if e < 0 { "-" } else { "+" }, e.abs())
};
format!("{}{}{}{}", sign, intpart, fracpart, expstr)
}
fn cardinal_from_py_str(number: &str) -> Result<String> {
let n = number.trim();
let (ret, n) = match n.strip_prefix('-') {
Some(rest) => (NEGWORD, rest),
None => ("", n),
};
match n.split_once('.') {
Some((left, right)) => {
let mut out = String::from(ret);
out.push_str(&int_to_word(&py_int(left)?));
out.push(' ');
out.push_str(POINTWORD);
out.push(' ');
for digit in right.chars() {
let d = py_int(digit.encode_utf8(&mut [0u8; 4]))?;
out.push_str(&int_to_word(&d));
out.push(' ');
}
Ok(out.trim().to_string())
}
None => Ok(format!("{}{}", ret, int_to_word(&py_int(n)?))
.trim()
.to_string()),
}
}
pub struct LangBr {
currency_forms: HashMap<&'static str, CurrencyForms>,
fallback_forms: CurrencyForms,
}
impl LangBr {
pub fn new() -> Self {
let mut currency_forms = HashMap::new();
currency_forms.insert(
"EUR",
CurrencyForms::new(&["euro", "euroioù"], &["sentim", "sentimoù"]),
);
currency_forms.insert(
"USD",
CurrencyForms::new(&["dollar", "dollars"], &["cent", "cents"]),
);
let fallback_forms = currency_forms
.get("EUR")
.expect("CURRENCY_FORMS[\"EUR\"] is inserted directly above")
.clone();
LangBr {
currency_forms,
fallback_forms,
}
}
}
impl Default for LangBr {
fn default() -> Self {
LangBr::new()
}
}
impl Lang for LangBr {
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!("{}-vet", 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 {
"EUR"
}
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_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let n = match value {
FloatValue::Float { value, .. } => py_float_str(*value),
FloatValue::Decimal { value, .. } => py_decimal_str(value),
};
cardinal_from_py_str(&n)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
Ok(format!("{}-vet", 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 lang_name(&self) -> &str {
"Num2Word_BR"
}
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 _ = adjective;
let separator = if separator == BASE_DEFAULT_SEPARATOR {
DEFAULT_SEPARATOR
} else {
separator
};
let is_negative = val.is_negative();
let s = match val {
CurrencyValue::Int(v) => {
if is_negative {
v.abs().to_string()
} else {
v.to_string()
}
}
CurrencyValue::Decimal { value: d, .. } => {
if is_negative {
d.abs().to_string()
} else {
d.to_string()
}
}
};
let mut parts = s.split('.');
let part0 = parts.next().unwrap_or("");
let part1 = parts.next();
let left = if part0.is_empty() {
BigInt::zero()
} else {
BigInt::from_str(part0).map_err(|e| N2WError::Value(e.to_string()))?
};
let right = match part1 {
Some(f) if !f.is_empty() => {
let mut two: String = f.chars().take(2).collect();
while two.chars().count() < 2 {
two.push('0');
}
BigInt::from_str(&two).map_err(|e| N2WError::Value(e.to_string()))?
}
_ => BigInt::zero(),
};
let forms = self
.currency_forms
.get(currency)
.unwrap_or(&self.fallback_forms);
let cr1 = &forms.unit;
let cr2 = &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())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn f(v: f64) -> Result<String> {
LangBr::new().to_cardinal_float(
&FloatValue::Float {
value: v,
precision: 0,
},
None,
)
}
fn d(s: &str) -> Result<String> {
LangBr::new().to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
},
None,
)
}
fn ok(r: Result<String>) -> String {
r.expect("expected Ok")
}
fn value_err(r: Result<String>) -> String {
match r {
Err(N2WError::Value(m)) => m,
other => panic!("expected N2WError::Value, got {:?}", other),
}
}
#[test]
fn corpus_float_rows() {
assert_eq!(ok(f(0.0)), "zero point zero");
assert_eq!(ok(f(0.5)), "zero point pemp");
assert_eq!(ok(f(1.0)), "unan point zero");
assert_eq!(ok(f(1.5)), "unan point pemp");
assert_eq!(ok(f(2.25)), "daou point daou pemp");
assert_eq!(ok(f(3.14)), "tri point unan pevar");
assert_eq!(ok(f(0.01)), "zero point zero unan");
assert_eq!(ok(f(0.1)), "zero point unan");
assert_eq!(ok(f(0.99)), "zero point nav nav");
assert_eq!(ok(f(1.01)), "unan point zero unan");
assert_eq!(ok(f(12.34)), "dek daou point tri pevar");
assert_eq!(ok(f(99.99)), "dek ha pevar-ugent nav point nav nav");
assert_eq!(ok(f(100.5)), "unan kant point pemp");
assert_eq!(
ok(f(1234.56)),
"unan mil daou kant tregont pevar point pemp c'hwec'h"
);
assert_eq!(ok(f(-0.5)), "minus zero point pemp");
assert_eq!(ok(f(-1.5)), "minus unan point pemp");
assert_eq!(ok(f(-12.34)), "minus dek daou point tri pevar");
assert_eq!(ok(f(1.005)), "unan point zero zero pemp");
assert_eq!(ok(f(2.675)), "daou point c'hwec'h seizh pemp");
}
#[test]
fn corpus_decimal_rows() {
assert_eq!(ok(d("0.01")), "zero point zero unan");
assert_eq!(ok(d("1.10")), "unan point unan zero");
assert_eq!(ok(d("12.345")), "dek daou point tri pevar pemp");
assert_eq!(ok(d("98746251323029.99")), "98746251323029 point nav nav");
assert_eq!(ok(d("0.001")), "zero point zero zero unan");
}
#[test]
fn does_not_take_the_float2tuple_route() {
assert_eq!(
ok(f(0.17108284528077355)),
"zero point unan seizh unan zero eizh daou eizh pevar pemp daou \
eizh zero seizh seizh tri pemp pemp"
);
assert_eq!(ok(f(1.005)), "unan point zero zero pemp");
assert_eq!(ok(f(2.675)), "daou point c'hwec'h seizh pemp");
}
#[test]
fn precision_override_is_ignored() {
let l = LangBr::new();
let v = FloatValue::Float {
value: 1.2345,
precision: 4,
};
let expected = "unan point daou tri pevar pemp";
assert_eq!(l.to_cardinal_float(&v, None).unwrap(), expected);
assert_eq!(l.to_cardinal_float(&v, Some(2)).unwrap(), expected);
assert_eq!(l.to_cardinal_float(&v, Some(8)).unwrap(), expected);
}
#[test]
fn negative_zero_float_keeps_its_sign() {
assert_eq!(ok(f(-0.0)), "minus zero point zero");
assert_eq!(ok(f(0.0)), "zero point zero");
}
#[test]
fn scientific_notation_raises_value_error() {
assert_eq!(
value_err(f(1e16)),
"invalid literal for int() with base 10: '1e+16'"
);
assert_eq!(
value_err(f(1e21)),
"invalid literal for int() with base 10: '1e+21'"
);
assert_eq!(
value_err(f(1e-5)),
"invalid literal for int() with base 10: '1e-05'"
);
assert_eq!(
value_err(f(1.5e16)),
"invalid literal for int() with base 10: 'e'"
);
assert_eq!(
value_err(f(f64::MAX)),
"invalid literal for int() with base 10: 'e'"
);
assert_eq!(
value_err(f(f64::INFINITY)),
"invalid literal for int() with base 10: 'inf'"
);
assert_eq!(
value_err(f(f64::NEG_INFINITY)),
"invalid literal for int() with base 10: 'inf'"
);
assert_eq!(
value_err(f(f64::NAN)),
"invalid literal for int() with base 10: 'nan'"
);
}
#[test]
fn repr_thresholds() {
assert_eq!(ok(f(1e15)), "1000000000000000 point zero");
assert!(f(1e16).is_err());
assert_eq!(ok(f(1e-4)), "zero point zero zero zero unan");
assert!(f(1e-5).is_err());
}
#[test]
fn exact_decimal_ties_round_to_even_like_gay() {
let v = f64::from_bits(0x4313781b63796605);
assert_eq!(py_float_str(v), "1370020896463233.2");
assert_eq!(py_float_str(20682518724725.062), "20682518724725.062");
assert_eq!(py_float_str(0.30000000000000004), "0.30000000000000004");
}
#[test]
fn decimal_str_is_not_float_repr() {
assert_eq!(ok(d("5.00")), "pemp point zero zero");
assert_eq!(ok(d("5")), "pemp");
assert_eq!(ok(d("1e-5")), "zero point zero zero zero zero unan");
assert!(f(1e-5).is_err());
assert_eq!(
value_err(d("1e-7")),
"invalid literal for int() with base 10: '1E-7'"
);
assert_eq!(
value_err(d("1E+21")),
"invalid literal for int() with base 10: '1E+21'"
);
}
#[test]
fn decimal_str_matches_python_not_bigdecimal_display() {
assert_eq!(py_decimal_str(&BigDecimal::from_str("0.0").unwrap()), "0.0");
assert_eq!(BigDecimal::from_str("0.0").unwrap().to_string(), "0");
assert_eq!(ok(d("0.0")), "zero point zero");
assert_eq!(
py_decimal_str(&BigDecimal::from_str("1.10").unwrap()),
"1.10"
);
assert_eq!(
py_decimal_str(&BigDecimal::from_str("1e-6").unwrap()),
"0.000001"
);
assert_eq!(
py_decimal_str(&BigDecimal::from_str("-1.10").unwrap()),
"-1.10"
);
}
#[test]
fn large_values() {
assert_eq!(ok(d("98746251323029.99")), "98746251323029 point nav nav");
assert_eq!(ok(f(1234567890.5)), "1234567890 point pemp");
assert_eq!(
ok(f(123456789.5)),
"unan kant ugent tri milion pevar kant hanter-kant c'hwec'h mil \
seizh kant pevar-ugent nav point pemp"
);
}
}