use crate::base::{Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::{float2tuple, FloatValue};
use crate::strnum::ParsedNumber;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{FromPrimitive, One, Signed, ToPrimitive, Zero};
use std::collections::HashMap;
const ZERO: &str = "nulle";
const NEGWORD: &str = "mīnus";
const POINTWORD: &str = "komats";
const ONES: [&str; 10] = [
"", "viens", "divi", "trīs", "četri", "pieci", "seši", "septiņi", "astoņi", "deviņi",
];
const TENS: [&str; 10] = [
"desmit",
"vienpadsmit",
"divpadsmit",
"trīspadsmit",
"četrpadsmit",
"piecpadsmit",
"sešpadsmit",
"septiņpadsmit",
"astoņpadsmit",
"deviņpadsmit",
];
const TWENTIES: [&str; 10] = [
"",
"",
"divdesmit",
"trīsdesmit",
"četrdesmit",
"piecdesmit",
"sešdesmit",
"septiņdesmit",
"astoņdesmit",
"deviņdesmit",
];
const HUNDRED: [&str; 3] = ["simts", "simti", "simtu"];
const THOUSANDS: [[&str; 3]; 11] = [
["", "", ""], ["tūkstotis", "tūkstoši", "tūkstošu"],
["miljons", "miljoni", "miljonu"],
["miljards", "miljardi", "miljardu"],
["triljons", "triljoni", "triljonu"],
["kvadriljons", "kvadriljoni", "kvadriljonu"],
["kvintiljons", "kvintiljoni", "kvintiljonu"],
["sikstiljons", "sikstiljoni", "sikstiljonu"], ["septiljons", "septiljoni", "septiljonu"],
["oktiljons", "oktiljoni", "oktiljonu"],
["nontiljons", "nontiljoni", "nontiljonu"], ];
const ORDINALS: [(i64, &str); 29] = [
(1, "pirmais"),
(2, "otrais"),
(3, "trešais"),
(4, "ceturtais"),
(5, "piektais"),
(6, "sestais"),
(7, "septītais"),
(8, "astotais"),
(9, "devītais"),
(10, "desmitais"),
(11, "vienpadsmitais"),
(12, "divpadsmitais"),
(13, "trīspadsmitais"),
(14, "četrpadsmitais"),
(15, "piecpadsmitais"),
(16, "sešpadsmitais"),
(17, "septiņpadsmitais"),
(18, "astoņpadsmitais"),
(19, "deviņpadsmitais"),
(20, "divdesmitais"),
(30, "trīsdesmitais"),
(40, "četrdesmitais"),
(50, "piecdesmitais"),
(60, "sešdesmitais"),
(70, "septiņdesmitais"),
(80, "astoņdesmitais"),
(90, "deviņdesmitais"),
(100, "simtais"),
(1000, "tūkstošais"),
];
const GENERIC_DOLLARS: [&str; 3] = ["dolārs", "dolāri", "dolāru"];
const GENERIC_CENTS: [&str; 3] = ["cents", "centi", "centu"];
const GENERIC_KRONA: [&str; 3] = ["krona", "kronas", "kronu"];
const GENERIC_ERA: [&str; 3] = ["ēre", "ēras", "ēru"];
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("AUD", CurrencyForms::new(&GENERIC_DOLLARS, &GENERIC_CENTS));
m.insert("CAD", CurrencyForms::new(&GENERIC_DOLLARS, &GENERIC_CENTS));
m.insert("EEK", CurrencyForms::new(&GENERIC_KRONA, &GENERIC_CENTS));
m.insert(
"EUR",
CurrencyForms::new(&["eiro", "eiro", "eiro"], &GENERIC_CENTS),
);
m.insert(
"EUR_LEGAL",
CurrencyForms::new(&["euro", "euro", "euro"], &GENERIC_CENTS),
);
m.insert(
"GBP",
CurrencyForms::new(
&["sterliņu mārciņa", "sterliņu mārciņas", "sterliņu mārciņu"],
&["penss", "pensi", "pensu"],
),
);
m.insert(
"LTL",
CurrencyForms::new(&["lits", "liti", "litu"], &GENERIC_CENTS),
);
m.insert(
"LVL",
CurrencyForms::new(
&["lats", "lati", "latu"],
&["santīms", "santīmi", "santīmu"],
),
);
m.insert("USD", CurrencyForms::new(&GENERIC_DOLLARS, &GENERIC_CENTS));
m.insert(
"RUB",
CurrencyForms::new(
&["rublis", "rubļi", "rubļu"],
&["kapeika", "kapeikas", "kapeiku"],
),
);
m.insert("SEK", CurrencyForms::new(&GENERIC_KRONA, &GENERIC_ERA));
m.insert("NOK", CurrencyForms::new(&GENERIC_KRONA, &GENERIC_ERA));
m.insert(
"PLN",
CurrencyForms::new(&["zlots", "zloti", "zlotu"], &["grasis", "graši", "grašu"]),
);
m
}
fn build_currency_adjectives() -> HashMap<&'static str, &'static str> {
[
("AUD", "Austrālijas"),
("CAD", "Kanādas"),
("EEK", "Igaunijas"),
("USD", "ASV"),
("RUB", "Kreivijas"), ("SEK", "Zviedrijas"),
("NOK", "Norvēģijas"),
]
.into_iter()
.collect()
}
fn key_error(key: String) -> N2WError {
N2WError::Key(key)
}
fn splitbyx(n: &str, x: usize) -> Result<Vec<BigInt>> {
let chars: Vec<char> = n.chars().collect();
let length = chars.len();
let slice = |i: usize, j: usize| -> String { chars[i..j.min(length)].iter().collect() };
let parse = |s: String| -> Result<BigInt> {
BigInt::parse_bytes(s.as_bytes(), 10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s))
})
};
let mut out: Vec<BigInt> = Vec::new();
if length > x {
let start = length % x;
if start > 0 {
out.push(parse(slice(0, start))?);
}
let mut i = start;
while i < length {
out.push(parse(slice(i, i + x))?);
i += x;
}
} else {
out.push(parse(n.to_string())?);
}
Ok(out)
}
fn get_digits(n: &BigInt) -> [usize; 3] {
let s = format!("{:03}", n);
let chars: Vec<char> = s.chars().collect();
let tail = &chars[chars.len() - 3..];
let mut a = [0usize; 3];
for (k, c) in tail.iter().rev().enumerate() {
a[k] = c.to_digit(10).unwrap_or(0) as usize;
}
a
}
pub struct LangLv {
currency_forms: HashMap<&'static str, CurrencyForms>,
currency_adjectives: HashMap<&'static str, &'static str>,
}
impl Default for LangLv {
fn default() -> Self {
Self::new()
}
}
impl LangLv {
pub fn new() -> Self {
LangLv {
currency_forms: build_currency_forms(),
currency_adjectives: build_currency_adjectives(),
}
}
fn thousands_at(&self, i: usize) -> Result<&'static [&'static str; 3]> {
if (1..=10).contains(&i) {
Ok(&THOUSANDS[i])
} else {
Err(key_error(i.to_string()))
}
}
fn pluralize(&self, n: &BigInt, forms: &[&str; 3]) -> String {
let m10 = n.mod_floor(&BigInt::from(10));
let m100 = n.mod_floor(&BigInt::from(100));
let form = if m10.is_one() && m100 != BigInt::from(11) {
0usize
} else if !n.is_zero() {
1
} else {
2
};
forms[form].to_string()
}
fn int2word(&self, n: &BigInt) -> Result<String> {
if n.is_zero() {
return Ok(ZERO.to_string());
}
let mut words: Vec<String> = Vec::new();
let chunks = splitbyx(&n.to_string(), 3)?;
let mut i = chunks.len();
for x in chunks.iter() {
i -= 1;
if x.is_zero() {
continue;
}
let [n1, n2, n3] = get_digits(x);
if n3 > 0 {
if n3 == 1 && n2 == 0 && n1 > 0 {
words.push(HUNDRED[2].to_string());
} else if n3 > 1 {
words.push(ONES[n3].to_string());
words.push(HUNDRED[1].to_string());
} else {
words.push(HUNDRED[0].to_string());
}
}
if n2 > 1 {
words.push(TWENTIES[n2].to_string());
}
if n2 == 1 {
words.push(TENS[n1].to_string());
} else if n1 > 0 && !(i > 0 && x.is_one()) {
words.push(ONES[n1].to_string());
}
if i > 0 {
let forms = self.thousands_at(i)?;
words.push(self.pluralize(x, forms));
}
}
Ok(words.join(" "))
}
}
fn trunc_to_bigint(v: &FloatValue) -> Option<BigInt> {
match v {
FloatValue::Float { value, .. } => {
if !value.is_finite() {
return None;
}
BigInt::from_f64(value.trunc())
}
FloatValue::Decimal { value, .. } => {
Some(value.with_scale(0).as_bigint_and_exponent().0)
}
}
}
fn int_value_error(literal: &str) -> N2WError {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
literal
))
}
fn float_no_point_str(f: f64) -> String {
if f.is_nan() {
return "nan".to_string();
}
if f.is_infinite() {
return if f < 0.0 { "-inf" } else { "inf" }.to_string();
}
let s = format!("{:e}", f);
match s.split_once('e') {
Some((m, e)) if !e.starts_with('-') => format!("{}e+{:0>2}", m, e),
Some((m, e)) => format!("{}e-{:0>2}", m, &e[1..]),
None => s,
}
}
impl Lang for LangLv {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
","
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"komats"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let words = self.int2word(&value.abs())?;
Ok(format!("{} {}", NEGWORD, words))
} else {
self.int2word(value)
}
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
if let Some(k) = value.to_i64() {
if let Some((_, word)) = ORDINALS.iter().find(|(key, _)| *key == k) {
return Ok(word.to_string());
}
}
let cardinal = self.to_cardinal(value)?;
match cardinal.strip_suffix('s') {
Some(stem) => Ok(format!("{}ais", stem)),
None => Ok(format!("{}ais", cardinal)),
}
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let precision = value.precision() as usize;
let base_str = if value.is_negative() {
format!("{} ", NEGWORD)
} else {
String::new()
};
let (pre, post) = float2tuple(value);
let left_word = self.int2word(&pre.abs())?;
if precision == 0 {
return Ok(format!("{}{}", base_str, left_word));
}
let post_str = post.to_string();
let right = format!(
"{}{}",
"0".repeat(precision.saturating_sub(post_str.len())),
post_str
);
let leading_zero_count = right.len() - right.trim_start_matches('0').len();
let mut decimal_part = String::new();
for _ in 0..leading_zero_count {
decimal_part.push_str(ZERO);
decimal_part.push(' ');
}
decimal_part.push_str(&self.int2word(&post)?);
Ok(format!(
"{}{} {} {}",
base_str, left_word, POINTWORD, decimal_part
))
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
if value.has_visible_point() {
return self.to_cardinal_float(value, precision_override);
}
match value {
FloatValue::Float { value, .. } => Err(int_value_error(&float_no_point_str(*value))),
FloatValue::Decimal { value, .. } => {
let s = crate::strnum::python_decimal_str(value);
match crate::strnum::python_int_parse(&s) {
Some(i) => self.to_cardinal(&i),
None => Err(int_value_error(&s)),
}
}
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if let FloatValue::Float { value: f, .. } = value {
if f.is_nan() {
return Ok("nan".to_string());
}
if f.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
}
let num = trunc_to_bigint(value).expect("finite after the guards above");
self.to_ordinal(&num)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match crate::strnum::python_decimal_parse(s)? {
ParsedNumber::Inf { negative } => Err(int_value_error(if negative {
"-Infinity"
} else {
"Infinity"
})),
other => Ok(other),
}
}
fn lang_name(&self) -> &str {
"Num2Word_LV"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn currency_adjective(&self, code: &str) -> Option<&str> {
self.currency_adjectives.get(code).copied()
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let m10 = n.mod_floor(&BigInt::from(10));
let m100 = n.mod_floor(&BigInt::from(100));
let form = if m10.is_one() && m100 != BigInt::from(11) {
0usize
} else if !n.is_zero() {
1
} else {
2
};
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> {
if let CurrencyValue::Int(v) = val {
let cr1 = match self.currency_forms.get(currency) {
Some(forms) => &forms.unit,
None => {
return crate::currency::default_to_currency(
self,
val,
currency,
cents,
separator.unwrap_or(self.default_separator()),
adjective,
);
}
};
let minus_str = if v.is_negative() { NEGWORD } else { "" };
let abs_val = v.abs();
let money_str = self.to_cardinal(&abs_val)?;
let currency_str = if abs_val.is_one() {
cr1.first()
} else {
cr1.get(1).or_else(|| cr1.first())
}
.cloned()
.ok_or_else(|| N2WError::Index("tuple index out of range".into()))?;
return Ok(format!("{} {} {}", minus_str, money_str, currency_str)
.trim()
.to_string());
}
crate::currency::default_to_currency(
self,
val,
currency,
cents,
separator.unwrap_or(self.default_separator()),
adjective,
)
}
}