use crate::base::{Lang, N2WError, Result};
use crate::currency::{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;
fn key_error(msg: impl Into<String>) -> N2WError {
N2WError::Key(msg.into())
}
const ZERO_WORD: &str = "nula";
const NEGWORD: &str = "mínus";
const POINTWORD: &str = "čárka";
const ONES: [&str; 10] = [
"", "jedna", "dva", "tři", "čtyři", "pět", "šest", "sedm", "osm", "devět",
];
const TENS: [&str; 10] = [
"deset",
"jedenáct",
"dvanáct",
"třináct",
"čtrnáct",
"patnáct",
"šestnáct",
"sedmnáct",
"osmnáct",
"devatenáct",
];
const TWENTIES: [&str; 10] = [
"",
"",
"dvacet",
"třicet",
"čtyřicet",
"padesát",
"šedesát",
"sedmdesát",
"osmdesát",
"devadesát",
];
const HUNDREDS: [&str; 10] = [
"",
"sto",
"dvě stě",
"tři sta",
"čtyři sta",
"pět set",
"šest set",
"sedm set",
"osm set",
"devět set",
];
const THOUSANDS: [(&str, &str, &str); 11] = [
("", "", ""), ("tisíc", "tisíce", "tisíc"), ("milion", "miliony", "milionů"), ("miliarda", "miliardy", "miliard"), ("bilion", "biliony", "bilionů"), ("biliarda", "biliardy", "biliard"), ("trilion", "triliony", "trilionů"), ("triliarda", "triliardy", "triliard"), ("kvadrilion", "kvadriliony", "kvadrilionů"), ("kvadriliarda", "kvadriliardy", "kvadriliard"), ("quintillion", "quintilliony", "quintillionů"), ];
fn thousands_at(i: usize) -> Result<(&'static str, &'static str, &'static str)> {
THOUSANDS
.get(i)
.filter(|_| i >= 1)
.copied()
.ok_or_else(|| key_error(i.to_string()))
}
const ORDINALS: [(u16, &str); 29] = [
(1, "první"),
(2, "druhý"),
(3, "třetí"),
(4, "čtvrtý"),
(5, "pátý"),
(6, "šestý"),
(7, "sedmý"),
(8, "osmý"),
(9, "devátý"),
(10, "desátý"),
(11, "jedenáctý"),
(12, "dvanáctý"),
(13, "třináctý"),
(14, "čtrnáctý"),
(15, "patnáctý"),
(16, "šestnáctý"),
(17, "sedmnáctý"),
(18, "osmnáctý"),
(19, "devatenáctý"),
(20, "dvacátý"),
(30, "třicátý"),
(40, "čtyřicátý"),
(50, "padesátý"),
(60, "šedesátý"),
(70, "sedmdesátý"),
(80, "osmdesátý"),
(90, "devadesátý"),
(100, "stý"),
(1000, "tisící"),
];
fn splitbyx(n: &str, x: usize) -> Vec<BigInt> {
let chars: Vec<char> = n.chars().collect();
let length = chars.len();
let parse = |i: usize, j: usize| -> BigInt {
let s: String = chars[i..j.min(length)].iter().collect();
BigInt::parse_bytes(s.as_bytes(), 10).unwrap_or_else(BigInt::zero)
};
let mut out: Vec<BigInt> = Vec::new();
if length > x {
let start = length % x;
if start > 0 {
out.push(parse(0, start));
}
let mut i = start;
while i < length {
out.push(parse(i, i + x));
i += x;
}
} else {
out.push(parse(0, length));
}
out
}
fn get_digits(n: &BigInt) -> [usize; 3] {
let s = format!("{:0>3}", n.to_string());
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
}
fn plural_form_index(n: &BigInt) -> usize {
if n.is_one() {
return 0;
}
let m10 = n.mod_floor(&BigInt::from(10));
let m100 = n.mod_floor(&BigInt::from(100));
let paucal = m10 < BigInt::from(5)
&& m10 > BigInt::one()
&& (m100 < BigInt::from(10) || m100 > BigInt::from(20));
if paucal {
1
} else {
2
}
}
fn pluralize<'a>(n: &BigInt, forms: (&'a str, &'a str, &'a str)) -> &'a str {
match plural_form_index(n) {
0 => forms.0,
1 => forms.1,
_ => forms.2,
}
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"CZK",
CurrencyForms::new(
&["koruna", "koruny", "korun"],
&["haléř", "haléře", "haléřů"],
),
);
m.insert(
"EUR",
CurrencyForms::new(
&["euro", "euro", "euro"],
&["centů", "centů", "centů"],
),
);
m.insert(
"USD",
CurrencyForms::new(&["dolar", "dolary", "dolarů"], &["cent", "centy", "centů"]),
);
m
}
fn python_repr_f64(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 neg = f.is_sign_negative();
let s = format!("{:e}", f.abs()); let (mantissa, exp_s) = match s.split_once('e') {
Some(parts) => parts,
None => return s, };
let exp: i32 = exp_s.parse().unwrap_or(0);
let digits: String = mantissa.chars().filter(|c| c.is_ascii_digit()).collect();
let ndigits = digits.len() as i32;
let decpt = exp + 1;
let body = if decpt <= -4 || decpt > 16 {
let mut m = String::new();
m.push_str(&digits[..1]);
if ndigits > 1 {
m.push('.');
m.push_str(&digits[1..]);
}
let e = decpt - 1;
let (esign, eabs) = if e < 0 { ('-', -e) } else { ('+', e) };
format!("{}e{}{:02}", m, esign, eabs)
} else if decpt <= 0 {
format!("0.{}{}", "0".repeat((-decpt) as usize), digits)
} else if decpt >= ndigits {
format!("{}{}.0", digits, "0".repeat((decpt - ndigits) as usize))
} else {
let dp = decpt as usize;
format!("{}.{}", &digits[..dp], &digits[dp..])
};
if neg {
format!("-{}", body)
} else {
body
}
}
fn python_str_decimal(bd: &BigDecimal) -> String {
let (coeff, scale) = bd.as_bigint_and_exponent();
let py_exp: i64 = -scale; let neg = coeff.is_negative(); let int_str = coeff.abs().to_string(); let ndigits = int_str.len() as i64;
let leftdigits = py_exp + ndigits;
let dotplace: i64 = if py_exp <= 0 && leftdigits > -6 {
leftdigits
} else {
1
};
let (intpart, fracpart) = if dotplace <= 0 {
(
"0".to_string(),
format!(".{}{}", "0".repeat((-dotplace) as usize), int_str),
)
} else if dotplace >= ndigits {
(
format!("{}{}", int_str, "0".repeat((dotplace - ndigits) as usize)),
String::new(),
)
} else {
let dp = dotplace as usize;
(int_str[..dp].to_string(), format!(".{}", &int_str[dp..]))
};
let exp = if leftdigits == dotplace {
String::new()
} else {
format!("E{:+}", leftdigits - dotplace)
};
let sign = if neg { "-" } else { "" };
format!("{}{}{}{}", sign, intpart, fracpart, exp)
}
fn parse_int_token(token: &str) -> Result<BigInt> {
BigInt::parse_bytes(token.as_bytes(), 10)
.filter(|_| !token.is_empty() && token.chars().all(|c| c.is_ascii_digit()))
.ok_or_else(|| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
token
))
})
}
pub struct LangCs {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangCs {
fn default() -> Self {
Self::new()
}
}
impl LangCs {
pub fn new() -> Self {
LangCs {
currency_forms: build_currency_forms(),
}
}
fn int2word(&self, n: &BigInt) -> Result<String> {
if n.is_zero() {
return Ok(ZERO_WORD.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 {
words.push(HUNDREDS[n3].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()) {
if n1 == 2 && (i == 3 || i == 5 || i == 7) {
words.push("dvě".to_string());
} else {
words.push(ONES[n1].to_string());
}
}
if i > 0 {
words.push(pluralize(x, thousands_at(i)?).to_string());
}
}
Ok(words.join(" "))
}
}
impl Lang for LangCs {
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 default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
","
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"čárka"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
if value.is_negative() {
let result = self.int2word(&value.abs())?;
Ok(format!("{} {}", NEGWORD, result))
} else {
self.int2word(value)
}
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
for (k, word) in ORDINALS.iter() {
if *value == BigInt::from(*k) {
return Ok((*word).to_string());
}
}
let cardinal = self.to_cardinal(value)?;
Ok(cardinal + "ý")
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
let num = match value {
FloatValue::Float { value, .. } => {
if value.is_nan() {
return Err(N2WError::Value(
"cannot convert float NaN to integer".into(),
));
}
if value.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
<BigInt as num_traits::FromPrimitive>::from_f64(value.trunc())
.ok_or_else(|| N2WError::Value("cannot convert float to integer".into()))?
}
FloatValue::Decimal { value, .. } => {
value.with_scale(0).as_bigint_and_exponent().0
}
};
self.to_ordinal(&num)
}
fn str_to_number(&self, s: &str) -> Result<crate::strnum::ParsedNumber> {
match crate::strnum::python_decimal_parse(s)? {
crate::strnum::ParsedNumber::Inf { .. } => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
other => Ok(other),
}
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let n_raw = match value {
FloatValue::Float { value, .. } => python_repr_f64(*value),
FloatValue::Decimal { value, .. } => python_str_decimal(value),
};
let n = n_raw.replace(',', ".");
if n.contains('.') {
let is_negative = n.starts_with('-');
let abs_n: &str = if is_negative { &n[1..] } else { &n };
let (left, right) = match abs_n.split_once('.') {
Some(pair) => pair,
None => {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
abs_n
)))
}
};
let stripped = right.trim_end_matches('0');
let right_eff = if stripped.is_empty() { "0" } else { stripped };
let mut decimal_parts: Vec<String> = Vec::with_capacity(right_eff.len());
for ch in right_eff.chars() {
if ch == '0' {
decimal_parts.push(ZERO_WORD.to_string());
} else {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch))
})? as usize;
decimal_parts.push(ONES[d].to_string());
}
}
let decimal_part = decimal_parts.join(" ");
let left_int = parse_int_token(left)?;
let result = format!(
"{} {} {}",
self.int2word(&left_int)?,
POINTWORD,
decimal_part
);
if is_negative {
Ok(format!("{} {}", NEGWORD, result))
} else {
Ok(result)
}
} else {
let is_negative = n.starts_with('-');
if is_negative {
let abs_n = &n[1..];
let m = parse_int_token(abs_n)?;
Ok(format!("{} {}", NEGWORD, self.int2word(&m)?))
} else {
let m = parse_int_token(&n)?;
self.int2word(&m)
}
}
}
fn lang_name(&self) -> &str {
"Num2Word_CS"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
forms
.get(plural_form_index(n))
.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 {
if let Some(forms) = self.currency_forms.get(currency) {
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() {
forms.unit.first().cloned()
} else {
forms.unit.get(1).or_else(|| forms.unit.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,
)
}
}