use crate::Result;
use crate::currency::vocab::CS as CURRENCY_VOCAB;
use crate::currency::{Currency, assert_minor_unit};
use crate::language::{Kind, Language, form};
use crate::max::Max;
use crate::number::{
Number, digits_to_biguint, parse_cardinal, parse_currency, parse_ordinal, split_groups,
};
use crate::options::options;
use num_bigint::BigUint;
use num_traits::{One, ToPrimitive, Zero};
const ONES: [&str; 10] = [
"", "jedna", "dva", "tři", "čtyři", "pět", "šest", "sedm", "osm", "devět",
];
const TEENS: [&str; 10] = [
"deset",
"jedenáct",
"dvanáct",
"třináct",
"čtrnáct",
"patnáct",
"šestnáct",
"sedmnáct",
"osmnáct",
"devatenáct",
];
const TENS: [&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 SCALE_FORMS: [[&str; 3]; 9] = [
["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"],
];
pub const CARDINAL_MAX: Max = Max::western(SCALE_FORMS.len() as u32);
pub const ORDINAL_MAX: Max = Max::western(SCALE_FORMS.len() as u32);
pub const CURRENCY_MAX: Max = Max::western(SCALE_FORMS.len() as u32);
const ZERO: &str = "nula";
const NEGATIVE: &str = "mínus";
const ORDINAL_ONES: [&str; 10] = [
"", "první", "druhý", "třetí", "čtvrtý", "pátý", "šestý", "sedmý", "osmý", "devátý",
];
const ORDINAL_TEENS: [&str; 10] = [
"desátý",
"jedenáctý",
"dvanáctý",
"třináctý",
"čtrnáctý",
"patnáctý",
"šestnáctý",
"sedmnáctý",
"osmnáctý",
"devatenáctý",
];
const ORDINAL_TENS: [&str; 10] = [
"",
"",
"dvacátý",
"třicátý",
"čtyřicátý",
"padesátý",
"šedesátý",
"sedmdesátý",
"osmdesátý",
"devadesátý",
];
const ORDINAL_HUNDREDS: [&str; 10] = [
"",
"stý",
"dvoustý",
"třístý",
"čtyřstý",
"pětistý",
"šestistý",
"sedmistý",
"osmistý",
"devítistý",
];
const ORDINAL_SCALES: [&str; 4] = ["tisící", "miliontý", "miliardtý", "biliontý"];
fn plural_index_tail(last_two: u32) -> usize {
if (2..=4).contains(&(last_two % 10)) && !(10..=20).contains(&last_two) {
1
} else {
2
}
}
fn plural_index(n: u32) -> usize {
if n == 1 {
0
} else {
plural_index_tail(n % 100)
}
}
fn plural_index_big(n: &BigUint) -> usize {
if n.is_one() {
0
} else {
plural_index_tail((n % 100u32).to_u32().unwrap_or(0))
}
}
fn pluralize<'a>(n: u32, forms: &[&'a str; 3]) -> &'a str {
forms[plural_index(n)]
}
fn build_segment(n: u32) -> String {
if n == 0 {
return String::new();
}
let ones = (n % 10) as usize;
let tens = ((n / 10) % 10) as usize;
let hundreds = (n / 100) as usize;
let mut parts: Vec<&str> = Vec::with_capacity(3);
if hundreds > 0 {
parts.push(HUNDREDS[hundreds]);
}
if tens == 1 {
parts.push(TEENS[ones]);
} else if tens >= 2 {
parts.push(TENS[tens]);
if ones > 0 {
parts.push(ONES[ones]);
}
} else if ones > 0 {
parts.push(ONES[ones]);
}
parts.join(" ")
}
fn decimal_separator(integer: &BigUint) -> &'static str {
match integer.to_u64() {
Some(0 | 1) => "celá",
Some(2..=4) => "celé",
_ => "celých",
}
}
fn integer_to_words(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_segment(small as u32);
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
let scale_word = pluralize(thousands, &SCALE_FORMS[0]);
let mut result = if thousands == 1 {
scale_word.to_string()
} else {
format!("{} {scale_word}", build_segment(thousands))
};
if remainder > 0 {
result.push(' ');
result.push_str(&build_segment(remainder));
}
return result;
}
}
build_large_number_words(n)
}
fn build_large_number_words(n: &BigUint) -> String {
let segments = split_groups(n, 1000);
let mut result = String::new();
for (i, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
if !result.is_empty() {
result.push(' ');
}
if i == 0 {
result.push_str(&build_segment(segment));
} else {
let scale_word = pluralize(segment, &SCALE_FORMS[i - 1]);
if segment == 1 {
result.push_str(scale_word);
} else {
result.push_str(&build_segment(segment));
result.push(' ');
result.push_str(scale_word);
}
}
}
result
}
fn decimal_part_to_words(decimal: &str) -> String {
let mut result = String::new();
let zeros = decimal.bytes().take_while(|&b| b == b'0').count();
for _ in 0..zeros {
if !result.is_empty() {
result.push(' ');
}
result.push_str(ZERO);
}
let remainder = &decimal[zeros..];
if !remainder.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(&integer_to_words(&digits_to_biguint(remainder)));
}
result
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let number = parse_cardinal(&value.into())?;
CARDINAL_MAX.check(&number.integer, number.fraction.as_deref())?;
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&number.integer));
if let Some(fraction) = &number.fraction {
result.push(' ');
result.push_str(decimal_separator(&number.integer));
result.push(' ');
result.push_str(&decimal_part_to_words(fraction));
}
Ok(result)
}
fn build_ordinal_tens_ones(n: u32) -> String {
if n == 0 {
return String::new();
}
let ones = (n % 10) as usize;
let tens = (n / 10) as usize;
if tens == 0 {
return ORDINAL_ONES[ones].to_string();
}
if tens == 1 {
return ORDINAL_TEENS[ones].to_string();
}
if ones == 0 {
return ORDINAL_TENS[tens].to_string();
}
format!("{} {}", TENS[tens], ORDINAL_ONES[ones])
}
fn build_ordinal_segment(n: u32) -> String {
if n < 100 {
return build_ordinal_tens_ones(n);
}
let hundreds = (n / 100) as usize;
let remainder = n % 100;
if remainder == 0 {
return ORDINAL_HUNDREDS[hundreds].to_string();
}
format!(
"{} {}",
HUNDREDS[hundreds],
build_ordinal_tens_ones(remainder)
)
}
fn integer_to_ordinal(n: &BigUint) -> String {
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_ordinal_segment(small as u32);
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
if remainder == 0 {
if thousands == 1 {
return ORDINAL_SCALES[0].to_string();
}
return format!("{} {}", build_segment(thousands), ORDINAL_SCALES[0]);
}
let scale_word = pluralize(thousands, &SCALE_FORMS[0]);
let thousands_word = if thousands == 1 {
String::new()
} else {
format!("{} ", build_segment(thousands))
};
return format!(
"{thousands_word}{scale_word} {}",
build_ordinal_segment(remainder)
);
}
}
build_large_ordinal(n)
}
fn ordinal_scale(i: usize) -> &'static str {
ORDINAL_SCALES
.get(i - 1)
.copied()
.unwrap_or(SCALE_FORMS[i - 1][0])
}
fn build_large_ordinal(n: &BigUint) -> String {
let segments = split_groups(n, 1000);
let lowest_non_zero = segments.iter().position(|&s| s != 0).unwrap_or(0);
let mut result = String::new();
for (i, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
if !result.is_empty() {
result.push(' ');
}
if i == 0 {
result.push_str(&build_ordinal_segment(segment));
continue;
}
if i == lowest_non_zero {
if segment == 1 {
result.push_str(ordinal_scale(i));
} else {
result.push_str(&build_segment(segment));
result.push(' ');
result.push_str(ordinal_scale(i));
}
} else {
let scale_word = pluralize(segment, &SCALE_FORMS[i - 1]);
if segment == 1 {
result.push_str(scale_word);
} else {
result.push_str(&build_segment(segment));
result.push(' ');
result.push_str(scale_word);
}
}
}
result
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
Ok(integer_to_ordinal(&n))
}
options! {
pub struct CurrencyOptions {
currency: Currency = Currency::CZK, table = CURRENCY_VOCAB;
}
}
pub fn to_currency<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_currency_with(value, &CurrencyOptions::default())
}
pub fn to_currency_with<'a>(
value: impl Into<Number<'a>>,
options: &CurrencyOptions,
) -> Result<String> {
let words = CURRENCY_VOCAB.words(options.currency)?;
let money = parse_currency(&value.into(), options.currency.minor_unit_digits())?;
CURRENCY_MAX.check(&money.major, None)?;
assert_minor_unit(money.minor, options.currency)?;
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
if !money.major.is_zero() || money.minor == 0 {
result.push_str(&integer_to_words(&money.major));
result.push(' ');
result.push_str(words.major_at(plural_index_big(&money.major)));
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push(' ');
}
result.push_str(&integer_to_words(&BigUint::from(money.minor)));
result.push(' ');
result.push_str(words.minor_at(plural_index(money.minor)));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "cs-CZ",
name: "Czech (Czechia)",
kind: Kind::Full,
cardinal: Some(form!(plain to_cardinal, CARDINAL_MAX)),
ordinal: Some(form!(plain to_ordinal, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};