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::{FromPrimitive, One, Signed, Zero};
use std::collections::HashMap;
const ZERO: &str = "nula";
const NEGWORD: &str = "mínus";
const POINTWORD: &str = "celých";
const ONES: [(&str, &str, &[usize]); 10] = [
("", "", &[]), ("jeden", "jeden", &[]),
("dva", "dve", &[1, 3, 5, 7, 9]),
("tri", "tri", &[]),
("štyri", "štyri", &[]),
("päť", "päť", &[]),
("šesť", "šesť", &[]),
("sedem", "sedem", &[]),
("osem", "osem", &[]),
("deväť", "deväť", &[]),
];
const TENS: [&str; 10] = [
"desať",
"jedenásť",
"dvanásť",
"trinásť",
"štrnásť",
"pätnásť",
"šestnásť",
"sedemnásť",
"osemnásť",
"devätnásť",
];
const TWENTIES: [&str; 10] = [
"",
"",
"dvadsať",
"tridsať",
"štyridsať",
"päťdesiat",
"šesťdesiat",
"sedemdesiat",
"osemdesiat",
"deväťdesiat",
];
const HUNDREDS: [&str; 10] = [
"",
"sto",
"dvesto",
"tristo",
"štyristo",
"päťsto",
"šesťsto",
"sedemsto",
"osemsto",
"deväťsto",
];
const THOUSANDS: [[&str; 3]; 11] = [
["", "", ""], ["tisíc", "tisíc", "tisíc"], ["milión", "milióny", "miliónov"], ["miliarda", "miliardy", "miliárd"], ["bilión", "bilióny", "biliónov"], ["biliarda", "biliardy", "biliárd"], ["trilión", "trilióny", "triliónov"], ["triliarda", "triliardy", "triliárd"], ["kvadrilión", "kvadrilióny", "kvadriliónov"], ["kvadriliarda", "kvadriliardy", "kvadriliárd"], ["kvintilión", "kvintillióny", "kvintiliónov"], ];
const ORDINALS: [(u32, &str); 29] = [
(1, "prvý"),
(2, "druhý"),
(3, "tretí"),
(4, "štvrtý"),
(5, "piaty"),
(6, "šiesty"),
(7, "siedmy"),
(8, "ôsmy"),
(9, "deviaty"),
(10, "desiaty"),
(11, "jedenásty"),
(12, "dvanásty"),
(13, "trinásty"),
(14, "štrnásty"),
(15, "pätnásty"),
(16, "šestnásty"),
(17, "sedemnásty"),
(18, "osemnásty"),
(19, "devätnásty"),
(20, "dvadsiaty"),
(30, "tridsiaty"),
(40, "štyridsiaty"),
(50, "päťdesiaty"),
(60, "šesťdesiaty"),
(70, "sedemdesiaty"),
(80, "osemdesiaty"),
(90, "deväťdesiaty"),
(100, "stý"),
(1000, "tisíci"),
];
fn key_error(key: String) -> N2WError {
N2WError::Key(key)
}
fn value_error(msg: String) -> N2WError {
N2WError::Value(msg)
}
fn parse_int(s: &str) -> Result<BigInt> {
BigInt::parse_bytes(s.as_bytes(), 10)
.ok_or_else(|| value_error(format!("invalid literal for int() with base 10: '{}'", s)))
}
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 mut out: Vec<BigInt> = Vec::new();
if length > x {
let start = length % x;
if start > 0 {
out.push(parse_int(&slice(0, start))?);
}
let mut i = start;
while i < length {
out.push(parse_int(&slice(i, i + x))?);
i += x;
}
} else {
out.push(parse_int(n)?);
}
Ok(out)
}
fn fmt_03(n: &BigInt) -> String {
let mag = n.abs().to_string();
if n.is_negative() {
let pad = 3usize.saturating_sub(1 + mag.len());
format!("-{}{}", "0".repeat(pad), mag)
} else {
let pad = 3usize.saturating_sub(mag.len());
format!("{}{}", "0".repeat(pad), mag)
}
}
fn get_digits(n: &BigInt) -> Result<[usize; 3]> {
let s = fmt_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).ok_or_else(|| {
value_error(format!("invalid literal for int() with base 10: '{}'", c))
})? as usize;
}
Ok(a)
}
fn plural_form_index(n: &BigInt) -> usize {
if n.is_one() {
0
} else if n.is_positive() && n < &BigInt::from(5) {
1
} else {
2
}
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const CENTS: [&str; 3] = ["cent", "centy", "centov"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"EUR",
CurrencyForms::new(&["euro", "eurá", "eur"], &CENTS),
);
m.insert(
"CZK",
CurrencyForms::new(
&["koruna", "koruny", "korún"],
&["halier", "haliere", "halierov"],
),
);
m.insert(
"USD",
CurrencyForms::new(&["dolár", "doláre", "dolárov"], &CENTS),
);
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)
}
pub struct LangSk {
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangSk {
fn default() -> Self {
Self::new()
}
}
impl LangSk {
pub fn new() -> Self {
LangSk {
currency_forms: build_currency_forms(),
}
}
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: &[&'static str; 3]) -> &'static str {
let form = if n.is_one() {
0usize
} else if n.is_positive() && n < &BigInt::from(5) {
1
} else {
2
};
forms[form]
}
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)?;
let mut word_chunk: Vec<&'static str> = Vec::new();
if n3 > 0 {
word_chunk.push(HUNDREDS[n3]);
}
if n2 > 1 {
word_chunk.push(TWENTIES[n2]);
}
if n2 == 1 {
word_chunk.push(TENS[n1]);
} else if n1 > 0 && !(i > 0 && x.is_one()) {
if n2 == 0 && n3 == 0 && ONES[n1].2.contains(&i) {
word_chunk.push(ONES[n1].1);
} else {
word_chunk.push(ONES[n1].0);
}
}
if i > 0 {
word_chunk.push(self.pluralize(x, self.thousands_at(i)?));
}
words.push(word_chunk.join(" "));
}
Ok(words.join(" "))
}
}
impl Lang for LangSk {
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> {
let num = match value {
FloatValue::Float { value, .. } => {
BigInt::from_f64(value.trunc()).unwrap_or_else(BigInt::zero)
}
FloatValue::Decimal { value, .. } => {
value.with_scale(0).as_bigint_and_exponent().0
}
};
self.to_ordinal(&num)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
let parsed = python_decimal_parse(s)?;
Ok(parsed)
}
fn inf_result(&self, _negative: bool, to: &str) -> Result<String> {
match to {
"cardinal" | "year" => Err(N2WError::Value(
"invalid literal for int() with base 10: 'Infinity'".into(),
)),
_ => Err(N2WError::Overflow(
"cannot convert Infinity to integer".into(),
)),
}
}
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
","
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"celých"
}
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(format!("{}ý", cardinal))
}
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(value_error(format!(
"invalid literal for int() with base 10: '{}'",
abs_n
)))
}
};
let leading_zero_count = right.chars().take_while(|c| *c == '0').count();
let right_int = parse_int(right)?;
let right_word = self.int2word(&right_int)?;
let mut decimal_part = String::new();
for _ in 0..leading_zero_count {
decimal_part.push_str(ZERO);
decimal_part.push(' ');
}
decimal_part.push_str(&right_word);
let left_int = parse_int(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(abs_n)?;
Ok(format!("{} {}", NEGWORD, self.int2word(&m)?))
} else {
let m = parse_int(&n)?;
self.int2word(&m)
}
}
}
fn lang_name(&self) -> &str {
"Num2Word_SK"
}
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,
)
}
}