use crate::base::{set_low_numwords, set_mid_numwords, splitnum, Cards, Lang, N2WError, Node, Result};
use crate::currency::{parse_currency_parts, CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, Zero};
use std::collections::HashMap;
use std::sync::OnceLock;
const ORD_SUFFIX_K: [i64; 15] = [90, 80, 70, 60, 50, 40, 30, 20, 10, 9, 8, 7, 5, 3, 2];
const ORD_NO_K: [i64; 2] = [6, 4];
const ORD_HUNDREDS_K: [i64; 7] = [900, 800, 700, 500, 300, 200, 100];
const ORD_HUNDREDS_NO_K: [i64; 2] = [600, 400];
const LOWS: [&str; 4] = ["kuatr", "tr", "b", "m"];
const MEGA_SUFFIX: &str = "iliaun";
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const DOLLAR: [&str; 2] = ["dolar", "dolar"];
const CENTS: [&str; 2] = ["sentavu", "sentavu"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("AUD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("CAD", CurrencyForms::new(&DOLLAR, &CENTS));
m.insert("EUR", CurrencyForms::new(&["euro", "euros"], &CENTS));
m.insert(
"GBP",
CurrencyForms::new(&["pound sterling", "pound sterling"], &["pence", "pence"]),
);
m.insert("USD", CurrencyForms::new(&DOLLAR, &CENTS));
m
}
fn decimal_prec_limit() -> &'static BigDecimal {
static LIMIT: OnceLock<BigDecimal> = OnceLock::new();
LIMIT.get_or_init(|| BigDecimal::from(BigInt::from(10).pow(28)))
}
fn decimal_context_round(v: &BigDecimal) -> BigDecimal {
const PREC: u64 = 28;
let digits = v.digits();
if digits <= PREC {
return v.clone();
}
let diff = digits - PREC;
let (int_val, scale) = v.as_bigint_and_exponent();
let p = BigInt::from(10).pow(diff as u32);
let negative = int_val.is_negative();
let (mut q, r) = int_val.abs().div_rem(&p);
let twice = &r * BigInt::from(2);
if twice > p || (twice == p && q.is_odd()) {
q += BigInt::one();
}
if negative {
q = -q;
}
BigDecimal::new(q, scale - diff as i64)
}
pub struct LangTet {
cards: Cards,
maxval: BigInt,
exclude_title: Vec<String>,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangTet {
fn default() -> Self {
Self::new()
}
}
impl LangTet {
pub fn new() -> Self {
let mut cards = Cards::new();
let high = LOWS;
let mut n: i64 = 3 + 6 * high.len() as i64;
for word in high.iter() {
if n <= 3 {
break; }
cards.insert(
BigInt::from(10u8).pow((n - 3) as u32),
format!("{}{}", word, MEGA_SUFFIX),
);
n -= 6;
}
set_mid_numwords(
&mut cards,
&[
(1000, "rihun"),
(100, "atus"),
(90, "sia nulu"),
(80, "ualu nulu"),
(70, "hitu nulu"),
(60, "neen nulu"),
(50, "lima nulu"),
(40, "haat nulu"),
(30, "tolu nulu"),
(20, "rua nulu"),
],
);
set_low_numwords(
&mut cards,
&[
"sanulu", "sia", "ualu", "hitu", "neen", "lima", "haat", "tolu", "rua", "ida",
"mamuk",
],
);
let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
LangTet {
cards,
maxval,
exclude_title: vec!["resin".into(), "vírgula".into(), "menus".into()],
currency_forms: build_currency_forms(),
}
}
fn merge_counted(
&self,
curr: (&str, &BigInt),
next: (&str, &BigInt),
count: &mut i64,
) -> (String, BigInt) {
let (ctext, cnum) = curr;
let (ntext, nnum) = next;
let ten = BigInt::from(10);
let hundred = BigInt::from(100);
if cnum.is_one() && nnum < &hundred {
return (ntext.to_string(), nnum.clone());
}
if nnum < cnum {
if nnum < &ten {
let sum = cnum + nnum;
if sum > hundred {
let chars: Vec<char> = sum.to_string().chars().collect();
let zero_list: &[char] = if chars.len() >= 2 {
&chars[1..chars.len() - 1]
} else {
&[]
};
let all_zero = zero_list.iter().all(|c| *c == '0');
if all_zero {
if *count >= 1 {
return (format!("ho {} {}", ctext, ntext), sum);
}
*count += 1;
return (format!("{} {}", ctext, ntext), sum);
}
}
return (format!("{} resin {}", ctext, ntext), sum);
} else {
return (format!("{} {}", ctext, ntext), cnum + nnum);
}
}
(format!("{} {}", ntext, ctext), cnum * nnum)
}
fn clean_counted(&self, val: Vec<Node>, count: &mut i64) -> Node {
let mut val = val;
while val.len() != 1 {
let mut out: Vec<Node> = Vec::new();
let both_leaf = val.len() >= 2
&& matches!(&val[0], Node::Leaf(..))
&& matches!(&val[1], Node::Leaf(..));
if both_leaf {
let (lt, ln) = match &val[0] {
Node::Leaf(t, n) => (t.clone(), n.clone()),
_ => unreachable!(),
};
let (rt, rn) = match &val[1] {
Node::Leaf(t, n) => (t.clone(), n.clone()),
_ => unreachable!(),
};
let (mt, mn) = self.merge_counted((<, &ln), (&rt, &rn), count);
out.push(Node::Leaf(mt, mn));
if val.len() > 2 {
out.push(Node::List(val[2..].to_vec()));
}
} else {
for elem in val.into_iter() {
match elem {
Node::List(inner) => {
if inner.len() == 1 {
out.push(inner.into_iter().next().unwrap());
} else {
out.push(self.clean_counted(inner, count));
}
}
leaf => out.push(leaf),
}
}
}
val = out;
}
val.into_iter().next().unwrap()
}
fn ho_result(&self, result: &str, value: &BigInt) -> String {
let mut result = result.to_string();
if !result.contains("ho") {
return result;
}
let value_len = value.to_string().chars().count();
if result.contains("rihun") && value_len > 7 {
result = result.replace("rihun ho", "ho rihun");
}
for low in LOWS.iter() {
result = result.replace(
&format!("{}{} ho", low, MEGA_SUFFIX),
&format!("ho {}{}", low, MEGA_SUFFIX),
);
}
if result.starts_with("ho") {
result = result.chars().skip(3).collect();
}
result
}
fn remove_ho(&self, result: &str, value: &BigInt) -> String {
let value_str = value.to_string();
let mut result = self.ho_result(result, value);
let vchars: Vec<char> = value_str.chars().collect();
let end_value: String = vchars[..vchars.len().saturating_sub(4)].iter().collect();
let end_true = end_value.ends_with('0');
if !end_true && value > &BigInt::from(100) {
let last = vchars[vchars.len() - 1];
let second_last = vchars[vchars.len() - 2];
if last != '0' && second_last == '0' {
result = result.replace("ho", "");
result = result.replace(" ", " ");
}
}
result
}
fn ho_result_str(&self, result: &str, value_len: usize) -> String {
let mut result = result.to_string();
if !result.contains("ho") {
return result;
}
if result.contains("rihun") && value_len > 7 {
result = result.replace("rihun ho", "ho rihun");
}
for low in LOWS.iter() {
result = result.replace(
&format!("{}{} ho", low, MEGA_SUFFIX),
&format!("ho {}{}", low, MEGA_SUFFIX),
);
}
if result.starts_with("ho") {
result = result.chars().skip(3).collect();
}
result
}
fn remove_ho_value(&self, result: &str, value_str: &str, gt_100: bool) -> String {
let vchars: Vec<char> = value_str.chars().collect();
let mut result = self.ho_result_str(result, vchars.len());
let end_value: String = vchars[..vchars.len().saturating_sub(4)].iter().collect();
let end_true = end_value.ends_with('0');
if !end_true && gt_100 {
if vchars.len() >= 2 {
let last = vchars[vchars.len() - 1];
let second_last = vchars[vchars.len() - 2];
if last != '0' && second_last == '0' {
result = result.replace("ho", "");
result = result.replace(" ", " ");
}
}
}
result
}
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 cardinal_words(&self, value: &BigInt, count: &mut i64) -> Result<String> {
let mut out = String::new();
let mut v = value.clone();
if v.is_negative() {
v = v.abs();
out = format!("{} ", self.negword().trim());
}
if v >= self.maxval {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
v, self.maxval
)));
}
let tree = splitnum(self, &v).ok_or_else(|| {
N2WError::Overflow(format!("abs({}) must be less than {}.", v, self.maxval))
})?;
let words = match self.clean_counted(tree, count) {
Node::Leaf(t, _) => t,
Node::List(_) => return Err(N2WError::Type("clean did not reduce".into())),
};
Ok(self.title(&format!("{}{}", out, words)))
}
}
impl Lang for LangTet {
fn default_currency(&self) -> &str {
"USD"
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
"menus "
}
fn pointword(&self) -> &str {
"vírgula"
}
fn exclude_title(&self) -> &[String] {
&self.exclude_title
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let mut count: i64 = 0;
let result = self.cardinal_words(value, &mut count)?;
Ok(self.remove_ho(&result, value))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
let mut count: i64 = 0;
let out = "";
let tree = splitnum(self, value).ok_or_else(|| {
N2WError::Type("object of type 'NoneType' has no len()".into())
})?;
let (mut words, num) = match self.clean_counted(tree, &mut count) {
Node::Leaf(t, n) => (t, n),
Node::List(_) => return Err(N2WError::Type("clean did not reduce".into())),
};
words = self.remove_ho(&words, value);
let in_list = |ks: &[i64]| ks.iter().any(|k| num == BigInt::from(*k));
if in_list(&ORD_SUFFIX_K) {
words = format!("da{}k", words);
}
if in_list(&ORD_NO_K) {
words = format!("da{}", words);
}
if num.is_one() {
words = "dahuluk".to_string();
}
if in_list(&ORD_HUNDREDS_K) {
words = format!("dah{}k", words);
}
if in_list(&ORD_HUNDREDS_NO_K) {
words = format!("dah{}", words);
}
let words_split: Vec<String> = words.split_whitespace().map(|s| s.to_string()).collect();
if words_split.len() >= 3 && num < BigInt::from(100) {
let first_word = format!("da{}", words_split[0]);
let second_word = words_split[1..].join(" ");
if second_word.contains("haat") || second_word.contains("neen") {
words = format!("{} {}", first_word, second_word);
} else {
words = format!("{} {}k", first_word, second_word);
}
}
let first = words_split
.first()
.ok_or_else(|| N2WError::Index("list index out of range".into()))?;
let word_first = format!("dah{}", first);
if word_first == "dahatus" && words_split.len() >= 3 {
let word_second = words_split[1..].join(" ");
if word_second.contains("haat") || word_second.contains("neen") {
words = format!("{} {}", word_first, word_second);
} else {
words = format!("{} {}k", word_first, word_second);
}
}
if num.to_string().chars().count() > 3 {
let last_is = |w: &str| words_split.last().map(|s| s.as_str() == w).unwrap_or(false);
if last_is("haat") || last_is("neen") {
words = format!("da{}", words);
} else {
words = format!("da{}k", words);
}
}
Ok(self.title(&format!("{}{}", out, words)))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
Ok(format!("{}º", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
return Ok(format!("{} antes Kristu", self.to_cardinal(&value.abs())?));
}
self.to_cardinal(value)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
match value.as_whole_int() {
Some(i) => self.to_ordinal(&i),
None => Err(N2WError::Type(
"Cannot treat float as ordinal.".into(),
)),
}
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
match value.as_whole_int() {
Some(i) => {
if i.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
repr_str
)));
}
Ok(format!("{}º", repr_str))
}
None => Err(N2WError::Type(
"Cannot treat float as ordinal.".into(),
)),
}
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
let negative = match value {
FloatValue::Float { value, .. } => *value < 0.0,
FloatValue::Decimal { value, .. } => value.is_negative(),
};
if negative {
let abs = match value {
FloatValue::Float { value, precision } => FloatValue::Float {
value: value.abs(),
precision: *precision,
},
FloatValue::Decimal { value, precision } => FloatValue::Decimal {
value: value.abs(),
precision: *precision,
},
};
Ok(format!(
"{} antes Kristu",
self.cardinal_float_entry(&abs, None)?
))
} else {
self.cardinal_float_entry(value, None)
}
}
fn to_cardinal_float(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
let result =
crate::floatpath::default_to_cardinal_float(self, value, precision_override)?;
let (value_str, gt_100) = match value {
FloatValue::Float { value, .. } => (format!("{}", value), *value > 100.0),
FloatValue::Decimal { value, .. } => {
(format!("{}", value), value > &BigDecimal::from(100))
}
};
Ok(self.remove_ho_value(&result, &value_str, gt_100))
}
fn lang_name(&self) -> &str {
"Num2Word_TET"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
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 decimal_val: BigDecimal = match val {
CurrencyValue::Int(i) => BigDecimal::from(i.clone()),
CurrencyValue::Decimal { value, .. } => value.clone(),
};
let scaled = decimal_context_round(&(&decimal_val * BigDecimal::from(100)));
if scaled.abs() >= *decimal_prec_limit() {
return Err(N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.DivisionImpossible'>]".to_string(),
});
}
let has_fractional_cents = &scaled - scaled.with_scale(0) != BigDecimal::zero();
let is_integer_input = matches!(val, CurrencyValue::Int(_));
let (left, right, is_negative) = parse_currency_parts(val, false, false, 100);
let right = right.as_bigint_and_exponent().0;
let forms = self.currency_forms.get(currency).ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
currency,
self.lang_name()
))
})?;
let cr1 = &forms.unit;
let cr2 = &forms.subunit;
let minus_str = if is_negative {
format!("{} ", self.negword().trim())
} else {
String::new()
};
let money_str = self.money_verbose(&left, currency)?;
if is_integer_input {
let curr_name = self.pluralize(&left, cr1)?;
return Ok(format!("{}{} {}", minus_str, curr_name, money_str));
}
let cents_str = if has_fractional_cents && right.is_positive() {
self.cardinal_from_decimal(&BigDecimal::new(right.clone(), 2))?
} else if cents {
self.cents_verbose(&right, currency)?
} else {
self.cents_terse(&right, currency)?
};
if right.is_zero() {
Ok(format!(
"{}{} {}",
minus_str,
self.pluralize(&left, cr1)?,
money_str
))
} else if has_fractional_cents {
Ok(format!(
"{}{} {} {} {}",
minus_str,
self.pluralize(&left, cr1)?,
money_str,
cents_str,
self.pluralize(&BigInt::one(), cr2)?
))
} else {
Ok(format!(
"{}{} {} {} {}",
minus_str,
self.pluralize(&left, cr1)?,
money_str,
self.pluralize(&right, cr2)?,
cents_str
))
}
}
}