use crate::base::{
clean, set_low_numwords, set_mid_numwords, splitnum, Cards, Lang, N2WError, Node, Result,
};
use crate::currency::{parse_currency_parts, CurrencyForms, CurrencyValue};
use crate::floatpath::{default_to_cardinal_float, float2tuple, FloatValue};
use crate::strnum::python_decimal_str;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const GIGA_SUFFIX: &str = "ቢሊዮን";
const MEGA_SUFFIX: &str = "ሚሊዮን";
const HUNDRED_TXT: &str = "መቶ";
const ORD_SUFFIX: &str = "ኛ";
const ZERO_TXT: &str = "ዜሮ";
const CENTS_DIVISOR: i64 = 100;
struct AmCurrency {
forms: CurrencyForms,
separator: &'static str,
}
fn build_currency() -> HashMap<&'static str, AmCurrency> {
const SANTIM: [&str; 2] = ["ሳንቲም", "ሳንቲም"];
const SEP: &str = " ከ";
let mut m: HashMap<&'static str, AmCurrency> = HashMap::new();
m.insert(
"ETB",
AmCurrency {
forms: CurrencyForms::new(&["ብር", "ብር"], &SANTIM),
separator: SEP,
},
);
m.insert(
"USD",
AmCurrency {
forms: CurrencyForms::new(&["ዶላር", "ዶላር"], &SANTIM),
separator: SEP,
},
);
m.insert(
"JPY",
AmCurrency {
forms: CurrencyForms::new(&["የን", "የን"], &["ሴን", "ሴን"]),
separator: SEP,
},
);
m
}
fn build_currency_adjectives() -> HashMap<&'static str, &'static str> {
[
("AUD", "Australian"),
("BYN", "Belarusian"),
("CAD", "Canadian"),
("EEK", "Estonian"),
("USD", "US"),
("RUB", "Russian"),
("NOK", "Norwegian"),
("MXN", "Mexican"),
("RON", "Romanian"),
("INR", "Indian"),
("HUF", "Hungarian"),
("ISK", "íslenskar"),
("UZS", "Uzbekistan"),
("SAR", "Saudi"),
("JPY", "Japanese"),
("KRW", "Korean"),
]
.into_iter()
.collect()
}
fn am_py_num_str(value: &FloatValue) -> String {
match value {
FloatValue::Float { value, .. } => format!("{}", value),
FloatValue::Decimal { value, .. } => python_decimal_str(value),
}
}
pub struct LangAm {
cards: Cards,
maxval: BigInt,
ords: HashMap<&'static str, &'static str>,
exclude_title: Vec<String>,
hundred: BigInt,
ten: BigInt,
currency: HashMap<&'static str, AmCurrency>,
currency_adjectives: HashMap<&'static str, &'static str>,
}
impl Default for LangAm {
fn default() -> Self {
Self::new()
}
}
impl LangAm {
pub fn new() -> Self {
let mut cards = Cards::new();
let ten_big = BigInt::from(10u8);
for (exp, word) in [
(33u32, "ዴሲሊዮን"),
(30, "ኖኒሊዮን"),
(27, "ኦክቲሊዮን"),
(24, "ሴፕቲሊዮን"),
(21, "ሴክስቲሊዮን"),
(18, "ኲንቲሊዮን"),
(15, "ኳድሪሊዮን"),
(12, "ትሪሊዮን"),
(9, GIGA_SUFFIX),
(6, MEGA_SUFFIX),
] {
cards.insert(ten_big.pow(exp), word);
}
set_mid_numwords(
&mut cards,
&[
(1000, "ሺህ"),
(100, HUNDRED_TXT),
(90, "ዘጠና"),
(80, "ሰማንያ"),
(70, "ሰባ"),
(60, "ስድሳ"),
(50, "አምሳ"),
(40, "አርባ"),
(30, "ሠላሳ"),
],
);
set_low_numwords(
&mut cards,
&[
"ሃያ",
"አሥራ ዘጠኝ",
"አሥራ ስምንት",
"አሥራ ሰባት",
"አስራ ስድስት",
"አሥራ አምስት",
"አሥራ አራት",
"አሥራ ሦስት",
"አሥራ ሁለት",
"አሥራ አንድ",
"አሥር",
"ዘጠኝ",
"ስምንት",
"ሰባት",
"ስድስት",
"አምስት",
"አራት",
"ሦስት",
"ሁለት",
"አንድ",
"ዜሮ",
],
);
let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
let ords: HashMap<&str, &str> = [
("አንድ", "አንደኛ"),
("ሁለት", "ሁለተኛ"),
("ሦስት", "ሦስተኛ"),
("አራት", "አራተኛ"),
("አምስት", "አምስተኛ"),
("ስድስት", "ስድስተኛ"),
("ሰባት", "ሰባተኛ"),
("ስምንት", "ስምንተኛ"),
("ዘጠኝ", "ዘጠነኛ"),
("አሥር", "አሥረኛ"),
("አሥራ አንድ", "አሥራ አንደኛ"),
("አሥራ ሁለት", "አሥራ ሁለተኛ"),
("አሥራ ሦስት", "አሥራ ሦስተኛ"),
("አሥራ አራት", "አሥራ አራተኛ"),
("አሥራ አምስት", "አሥራ አምስተኛ"),
("አሥራ ስድስት", "አሥራ ስድስተኛ"),
("አሥራ ሰባት", "አሥራ ሰባተኛ"),
("አሥራ ስምንት", "አሥራ ስምንተኛ"),
("አሥራ ዘጠኝ", "አሥራ ዘጠነኛ"),
]
.into_iter()
.collect();
LangAm {
cards,
maxval,
ords,
exclude_title: vec!["እና".into(), "ነጥብ".into(), "አሉታዊ".into()],
hundred: BigInt::from(100),
ten: BigInt::from(10),
currency: build_currency(),
currency_adjectives: build_currency_adjectives(),
}
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
Ok(())
}
fn verify_ordinal_num(&self, value: &FloatValue) -> Result<BigInt> {
let whole = match value.as_whole_int() {
Some(i) => i,
None => {
return Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
am_py_num_str(value)
)))
}
};
if whole.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
am_py_num_str(value)
)));
}
Ok(whole)
}
fn cardinal_num_dec(&self, x: &BigDecimal) -> Result<String> {
if x.is_integer() {
return self.to_cardinal(&x.with_scale(0).as_bigint_and_exponent().0);
}
let precision = x.as_bigint_and_exponent().1.max(0) as u32;
default_to_cardinal_float(
self,
&FloatValue::Decimal {
value: x.clone(),
precision,
},
None,
)
}
fn inflect(&self, _value: &BigInt, text: &str) -> String {
text.to_string()
}
fn currency_not_implemented(&self, currency: &str) -> N2WError {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
}
}
impl Lang for LangAm {
fn default_currency(&self) -> &str {
"ETB"
}
fn default_separator(&self) -> &str {
","
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
"ሰልቢ "
}
fn pointword(&self) -> &str {
"ነጥብ"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
let (ltext, lnum) = l;
let (rtext, rnum) = r;
let hundred = &self.hundred;
if lnum.is_one() && rnum < hundred {
(rtext.to_string(), rnum.clone())
} else if hundred > lnum && lnum > rnum {
(format!("{} {}", ltext, rtext), lnum + rnum)
} else if lnum >= hundred && hundred > rnum {
(format!("{} {}", ltext, rtext), lnum + rnum)
} else if rnum > lnum {
(format!("{} {}", ltext, rtext), lnum * rnum)
} else {
(format!("{} {}", ltext, rtext), lnum + rnum)
}
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let inner = self.to_cardinal(&(-value))?;
return Ok(format!("{}{}", self.negword(), inner));
}
if value >= &self.maxval {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
value, self.maxval
)));
}
if value == &self.hundred {
return Ok(self.title(HUNDRED_TXT));
}
let tree = splitnum(self, value)
.ok_or_else(|| N2WError::Type("object of type 'NoneType' has no len()".into()))?;
let words = match clean(self, tree) {
Node::Leaf(t, _) => t,
Node::List(_) => return Err(N2WError::Type("clean did not reduce".into())),
};
Ok(self.title(&words))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
let cardinal = self.to_cardinal(value)?;
let mut outwords: Vec<String> = cardinal.split(' ').map(String::from).collect();
let last = outwords[outwords.len() - 1].clone();
let mut lastwords: Vec<String> = last.split('-').map(String::from).collect();
let lastword = lastwords[lastwords.len() - 1].to_lowercase();
let newlast = match self.ords.get(lastword.as_str()) {
Some(o) => (*o).to_string(),
None => format!("{}{}", lastword, ORD_SUFFIX),
};
let li = lastwords.len() - 1;
lastwords[li] = self.title(&newlast);
let oi = outwords.len() - 1;
outwords[oi] = lastwords.join(" ");
Ok(outwords.join(" "))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
let ord = self.to_ordinal(value)?;
let suffix: String = ord.chars().last().map(String::from).unwrap_or_default();
Ok(format!("{}{}", value, suffix))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
if value
.div_floor(&self.hundred)
.mod_floor(&self.ten)
.is_zero()
{
return self.to_cardinal(value);
}
let (high, low) = value.div_mod_floor(&self.hundred);
let mut out: Vec<String> = Vec::new();
if !high.is_zero() {
let hightxt = self.title(&self.inflect(&high, HUNDRED_TXT));
out.push(self.to_cardinal(&high)?);
if !low.is_zero() {
if !hightxt.is_empty() {
out.push(hightxt);
}
} else if !hightxt.is_empty() {
out.push(hightxt);
}
}
if !low.is_zero() {
out.push(self.to_cardinal(&low)?);
}
Ok(out.join(" "))
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let whole = self.verify_ordinal_num(value)?;
self.to_ordinal(&whole)
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
let whole = self.verify_ordinal_num(value)?;
let ord = self.to_ordinal(&whole)?;
let suffix: String = ord.chars().last().map(String::from).unwrap_or_default();
Ok(format!("{}{}", repr_str, suffix))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
match value {
FloatValue::Float { value: v, .. } => {
let v = *v;
if (v / 100.0).floor().rem_euclid(10.0) == 0.0 {
return self.cardinal_float_entry(value, None);
}
let (high, low) = float2tuple(value);
let mut out: Vec<String> = Vec::new();
if !high.is_zero() {
let hightxt = self.title(&self.inflect(&high, HUNDRED_TXT));
out.push(self.to_cardinal(&high)?);
if !hightxt.is_empty() {
out.push(hightxt);
}
}
if !low.is_zero() {
out.push(self.to_cardinal(&low)?);
}
Ok(out.join(" "))
}
FloatValue::Decimal { value: d, .. } => {
let hundred = BigDecimal::from(100);
let q = (d / &hundred).with_scale_round(0, bigdecimal::RoundingMode::Down);
let q_int = q.as_bigint_and_exponent().0;
if (&q_int % BigInt::from(10)).is_zero() {
return self.cardinal_float_entry(value, None);
}
let high = q;
let low = d - &high * &hundred;
let mut out: Vec<String> = Vec::new();
if !high.is_zero() {
out.push(self.cardinal_num_dec(&high)?);
out.push(HUNDRED_TXT.to_string());
}
if !low.is_zero() {
out.push(self.cardinal_num_dec(&low)?);
}
Ok(out.join(" "))
}
}
}
fn lang_name(&self) -> &str {
"Num2Word_AM"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency.get(code).map(|e| &e.forms)
}
fn currency_adjective(&self, code: &str) -> Option<&str> {
self.currency_adjectives.get(code).copied()
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let form = if n.is_one() { 0 } else { 1 };
forms
.get(form)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
let is_integer_input = matches!(val, CurrencyValue::Int(_));
let (left, right, is_negative, has_fractional_cents) = match val {
CurrencyValue::Int(v) => (v.abs(), BigDecimal::zero(), v.is_negative(), false),
CurrencyValue::Decimal { value, .. } => {
let scaled = value * BigDecimal::from(CENTS_DIVISOR);
let has_fc = &scaled - scaled.with_scale(0) != BigDecimal::zero();
let (l, r, neg) = parse_currency_parts(val, false, has_fc, CENTS_DIVISOR);
(l, r, neg, has_fc)
}
};
let entry = self
.currency
.get(currency)
.ok_or_else(|| self.currency_not_implemented(currency))?;
let cr1 = &entry.forms.unit;
let cr2 = &entry.forms.subunit;
let mut separator = separator.unwrap_or(self.default_separator());
if separator == "," {
separator = entry.separator;
}
let minus_str = if is_negative {
format!("{} ", self.negword().trim())
} else {
String::new()
};
let money_str = self.to_cardinal(&left)?;
if is_integer_input {
return Ok(format!(
"{}{} {}",
minus_str,
money_str,
self.pluralize(&left, cr1)?
));
}
let right_int = right.as_bigint_and_exponent().0;
let cents_str = if cents {
if has_fractional_cents {
if right > BigDecimal::zero() {
self.cardinal_from_decimal(&right)?
} else {
ZERO_TXT.to_string()
}
} else if right_int.is_positive() {
self.to_cardinal(&right_int)?
} else {
ZERO_TXT.to_string()
}
} else {
if has_fractional_cents {
format!("{}", right.to_f64().unwrap_or(f64::INFINITY))
} else {
right_int.to_string()
}
};
let cents_word = if has_fractional_cents {
cr2.get(1)
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))?
} else {
self.pluralize(&right_int, cr2)?
};
Ok(format!(
"{}{} {}{} {} {}",
minus_str,
money_str,
self.pluralize(&left, cr1)?,
separator,
cents_str,
cents_word
))
}
fn to_cheque(&self, _val: &BigDecimal, currency: &str) -> Result<String> {
if self.currency.contains_key(currency) {
return Err(N2WError::Value(
"too many values to unpack (expected 2)".into(),
));
}
Err(self.currency_not_implemented(currency))
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use crate::floatpath::FloatValue;
use std::str::FromStr;
fn f(value: f64, precision: u32) -> String {
LangAm::new()
.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn d(s: &str, precision: u32) -> String {
LangAm::new()
.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
},
None,
)
.unwrap()
}
#[test]
fn corpus_cardinal_float() {
let rows: &[(f64, u32, &str)] = &[
(0.5, 1, "ዜሮ ነጥብ አምስት"),
(1.5, 1, "አንድ ነጥብ አምስት"),
(2.25, 2, "ሁለት ነጥብ ሁለት አምስት"),
(3.14, 2, "ሦስት ነጥብ አንድ አራት"),
(0.01, 2, "ዜሮ ነጥብ ዜሮ አንድ"),
(0.1, 1, "ዜሮ ነጥብ አንድ"),
(0.99, 2, "ዜሮ ነጥብ ዘጠኝ ዘጠኝ"),
(1.01, 2, "አንድ ነጥብ ዜሮ አንድ"),
(12.34, 2, "አሥራ ሁለት ነጥብ ሦስት አራት"),
(99.99, 2, "ዘጠና ዘጠኝ ነጥብ ዘጠኝ ዘጠኝ"),
(100.5, 1, "መቶ ነጥብ አምስት"),
(1234.56, 2, "አንድ ሺህ ሁለት መቶ ሠላሳ አራት ነጥብ አምስት ስድስት"),
(-0.5, 1, "ሰልቢ ዜሮ ነጥብ አምስት"),
(-1.5, 1, "ሰልቢ አንድ ነጥብ አምስት"),
(-12.34, 2, "ሰልቢ አሥራ ሁለት ነጥብ ሦስት አራት"),
(1.005, 3, "አንድ ነጥብ ዜሮ ዜሮ አምስት"),
(2.675, 3, "ሁለት ነጥብ ስድስት ሰባት አምስት"),
];
for &(v, p, want) in rows {
assert_eq!(f(v, p), want, "float {}", v);
}
}
#[test]
fn corpus_cardinal_dec() {
let rows: &[(&str, u32, &str)] = &[
("0.01", 2, "ዜሮ ነጥብ ዜሮ አንድ"),
("1.10", 2, "አንድ ነጥብ አንድ ዜሮ"),
("12.345", 3, "አሥራ ሁለት ነጥብ ሦስት አራት አምስት"),
(
"98746251323029.99",
2,
"ዘጠና ስምንት ትሪሊዮን ሰባት መቶ አርባ ስድስት ቢሊዮን ሁለት መቶ አምሳ አንድ ሚሊዮን \
ሦስት መቶ ሃያ ሦስት ሺህ ሃያ ዘጠኝ ነጥብ ዘጠኝ ዘጠኝ",
),
("0.001", 3, "ዜሮ ነጥብ ዜሮ ዜሮ አንድ"),
];
for &(s, p, want) in rows {
assert_eq!(d(s, p), want, "decimal {}", s);
}
}
#[test]
fn float_and_decimal_arms_differ_at_trillion_scale() {
let as_float = f(98746251323029.99, 2);
let as_decimal = d("98746251323029.99", 2);
assert!(as_float.ends_with("ነጥብ ዘጠኝ ስምንት"), "{}", as_float);
assert!(as_decimal.ends_with("ነጥብ ዘጠኝ ዘጠኝ"), "{}", as_decimal);
assert_ne!(as_float, as_decimal);
}
#[test]
fn extra_live_interpreter_rows() {
assert_eq!(f(-100.5, 1), "ሰልቢ መቶ ነጥብ አምስት");
assert_eq!(f(-0.01, 2), "ሰልቢ ዜሮ ነጥብ ዜሮ አንድ");
assert_eq!(f(0.001, 3), "ዜሮ ነጥብ ዜሮ ዜሮ አንድ");
assert_eq!(
f(12345678901234.5, 1),
"አሥራ ሁለት ትሪሊዮን ሦስት መቶ አርባ አምስት ቢሊዮን ስድስት መቶ ሰባ ስምንት ሚሊዮን \
ዘጠኝ መቶ አንድ ሺህ ሁለት መቶ ሠላሳ አራት ነጥብ አምስት"
);
}
#[test]
fn int_valued_floats() {
assert_eq!(f(2.0, 1), "ሁለት ነጥብ ዜሮ");
assert_eq!(f(100.0, 1), "መቶ ነጥብ ዜሮ");
assert_eq!(LangAm::new().to_cardinal(&BigInt::from(1)).unwrap(), "አንድ");
}
}