use crate::base::{Lang, N2WError, Result};
use crate::currency::{default_to_currency, CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{FromPrimitive, One, Signed, Zero};
use std::collections::HashMap;
const ZERO: &str = "zero";
const NEGWORD: &str = "minus";
const ONES: [&str; 10] = [
"", "jeden", "dwa", "trzy", "cztery", "pięć", "sześć", "siedem", "osiem", "dziewięć",
];
const ONES_ORDINALS: [(&str, &str); 20] = [
("", ""), ("pierwszy", "pierwszo"),
("drugi", "dwu"),
("trzeci", "trzy"),
("czwarty", "cztero"),
("piąty", "pięcio"),
("szósty", "sześcio"),
("siódmy", "siedmio"),
("ósmy", "ośmio"),
("dziewiąty", "dziewięcio"),
("dziesiąty", "dziesięcio"),
("jedenasty", "jedenasto"),
("dwunasty", "dwunasto"),
("trzynasty", "trzynasto"),
("czternasty", "czternasto"),
("piętnasty", "piętnasto"),
("szesnasty", "szesnasto"),
("siedemnasty", "siedemnasto"),
("osiemnasty", "osiemnasto"),
("dziewiętnasty", "dziewiętnasto"),
];
const TENS: [&str; 10] = [
"dziesięć",
"jedenaście",
"dwanaście",
"trzynaście",
"czternaście",
"piętnaście",
"szesnaście",
"siedemnaście",
"osiemnaście",
"dziewiętnaście",
];
const TWENTIES: [&str; 10] = [
"",
"",
"dwadzieścia",
"trzydzieści",
"czterdzieści",
"pięćdziesiąt",
"sześćdziesiąt",
"siedemdziesiąt",
"osiemdziesiąt",
"dziewięćdziesiąt",
];
const TWENTIES_ORDINALS: [(&str, &str); 10] = [
("", ""), ("", ""), ("dwudziesty", "dwudziesto"),
("trzydziesty", "trzydziesto"), ("czterdziesty", "czterdziesto"),
("pięćdziesiąty", "pięćdziesięcio"),
("sześćdziesiąty", "sześćdziesięcio"),
("siedemdziesiąty", "siedemdziesięcio"),
("osiemdziesiąty", "osiemdziesięcio"),
("dziewięćdziesiąty", "dziewięćdziesięcio"),
];
const HUNDREDS: [&str; 10] = [
"",
"sto",
"dwieście",
"trzysta",
"czterysta",
"pięćset",
"sześćset",
"siedemset",
"osiemset",
"dziewięćset",
];
const HUNDREDS_ORDINALS: [(&str, &str); 10] = [
("", ""), ("setny", "stu"),
("dwusetny", "dwustu"),
("trzysetny", "trzystu"),
("czterysetny", "czterystu"),
("pięćsetny", "pięćset"), ("sześćsetny", "sześćset"),
("siedemsetny", "siedemset"),
("osiemsetny", "osiemset"), ("dziewięćsetny", "dziewięćset"),
];
const PREFIXES_ORDINAL: [&str; 4] = [
"",
"tysięczny",
"milionowy",
"miliardowy", ];
const PREFIXES: [&str; 10] = [
"mi", "bi", "try", "kwadry", "kwinty", "seksty", "septy", "okty", "nony", "decy",
];
const SUFFIXES: [&str; 2] = ["lion", "liard"];
fn value_error(msg: String) -> N2WError {
N2WError::Value(msg)
}
fn index_error(msg: &str) -> N2WError {
N2WError::Index(msg.to_string())
}
fn key_error(key: String) -> N2WError {
N2WError::Key(key)
}
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 hundreds_at(n3: usize, level: usize) -> Result<&'static str> {
if level != 0 {
return Err(index_error("tuple index out of range"));
}
HUNDREDS
.get(n3)
.filter(|_| n3 >= 1)
.copied()
.ok_or_else(|| key_error(n3.to_string()))
}
fn hundreds_ordinals_at(n3: usize, level: usize) -> Result<&'static str> {
let t = HUNDREDS_ORDINALS
.get(n3)
.filter(|_| n3 >= 1)
.ok_or_else(|| key_error(n3.to_string()))?;
match level {
0 => Ok(t.0),
1 => Ok(t.1),
_ => Err(index_error("tuple index out of range")),
}
}
fn ones_ordinals_at(k: usize, level: usize) -> Result<&'static str> {
let t = ONES_ORDINALS
.get(k)
.filter(|_| k >= 1)
.ok_or_else(|| key_error(k.to_string()))?;
match level {
0 => Ok(t.0),
1 => Ok(t.1),
_ => Err(index_error("tuple index out of range")),
}
}
fn twenties_ordinals_at(n2: usize, level: usize) -> Result<&'static str> {
let t = TWENTIES_ORDINALS
.get(n2)
.filter(|_| n2 >= 2)
.ok_or_else(|| key_error(n2.to_string()))?;
match level {
0 => Ok(t.0),
1 => Ok(t.1),
_ => Err(index_error("tuple index out of range")),
}
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
const CENTS: [&str; 3] = ["cent", "centy", "centów"];
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert(
"PLN",
CurrencyForms::new(
&["złoty", "złote", "złotych"],
&["grosz", "grosze", "groszy"],
),
);
m.insert("EUR", CurrencyForms::new(&["euro", "euro", "euro"], &CENTS));
m.insert(
"USD",
CurrencyForms::new(
&[
"dolar amerykański",
"dolary amerykańskie",
"dolarów amerykańskich",
],
&CENTS,
),
);
m.insert(
"GBP",
CurrencyForms::new(
&[
"funt brytyjski",
"funty brytyjskie",
"funtów brytyjskich",
],
&["pens", "pensy", "pensów"],
),
);
m
}
pub struct LangPl {
thousands: HashMap<usize, [String; 3]>,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangPl {
fn default() -> Self {
Self::new()
}
}
impl LangPl {
pub fn new() -> Self {
let mut thousands: HashMap<usize, [String; 3]> = HashMap::new();
thousands.insert(
1,
[
"tysiąc".to_string(),
"tysiące".to_string(),
"tysięcy".to_string(),
],
);
let mut idx = 0usize;
for p in PREFIXES.iter() {
for s in SUFFIXES.iter() {
let name = format!("{}{}", p, s);
thousands.insert(
idx + 2,
[name.clone(), format!("{}y", name), format!("{}ów", name)],
);
idx += 1;
}
}
LangPl {
thousands,
currency_forms: build_currency_forms(),
}
}
fn thousands_at(&self, i: usize) -> Result<&[String; 3]> {
self.thousands
.get(&i)
.ok_or_else(|| key_error(i.to_string()))
}
fn pluralize(&self, n: &BigInt, forms: &[String; 3]) -> String {
let form = if n.is_one() {
0usize
} else {
let m10 = n.mod_floor(&BigInt::from(10));
let m100 = n.mod_floor(&BigInt::from(100));
let chained = m10 < BigInt::from(5) && m10 > BigInt::one();
if chained && (m100 < BigInt::from(10) || m100 > BigInt::from(20)) {
1
} else {
2
}
};
forms[form].clone()
}
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 {
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()) {
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 last_fragment_to_ordinal(
&self,
last: &BigInt,
words: &mut Vec<String>,
level: usize,
) -> Result<()> {
let [n1, n2, n3] = get_digits(last)?;
let last_two = n2 * 10 + n1;
if last_two == 0 {
words.push(hundreds_ordinals_at(n3, level)?.to_string());
} else if level == 1 && last.is_one() {
return Ok(());
} else if last_two < 20 {
if n3 > 0 {
words.push(hundreds_at(n3, level)?.to_string());
}
words.push(ones_ordinals_at(last_two, level)?.to_string());
} else if last_two % 10 == 0 {
if n3 > 0 {
words.push(hundreds_at(n3, level)?.to_string());
}
words.push(twenties_ordinals_at(n2, level)?.to_string());
} else {
if n3 > 0 {
words.push(hundreds_at(n3, 0)?.to_string());
}
words.push(twenties_ordinals_at(n2, 0)?.to_string());
words.push(ones_ordinals_at(n1, 0)?.to_string());
}
Ok(())
}
fn cardinal_from_str(&self, number: &str) -> Result<String> {
let n = number.replace(',', ".");
if let Some(dot) = n.find('.') {
let is_negative = n.starts_with('-');
let (abs_start, dot) = if is_negative { (1usize, dot) } else { (0usize, dot) };
let left = &n[abs_start..dot];
let right = &n[dot + 1..];
let mut decimal_parts: Vec<&str> = Vec::new();
for ch in right.chars() {
if ch == '0' {
decimal_parts.push(ZERO);
} else {
let d = ch.to_digit(10).ok_or_else(|| {
value_error(format!(
"invalid literal for int() with base 10: '{}'",
ch
))
})? as usize;
decimal_parts.push(ONES[d]);
}
}
let decimal_part = decimal_parts.join(" ");
let left_word = self.int2word(&parse_int(left)?)?;
let result = format!("{} {} {}", left_word, self.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..];
Ok(format!("{} {}", NEGWORD, self.int2word(&parse_int(abs_n)?)?))
} else {
self.int2word(&parse_int(&n)?)
}
}
}
}
fn is_plain_int_str(s: &str) -> bool {
let t = s.strip_prefix('-').unwrap_or(s);
!t.is_empty() && t.bytes().all(|b| b.is_ascii_digit())
}
impl Lang for LangPl {
fn default_currency(&self) -> &str {
"EUR"
}
fn default_separator(&self) -> &str {
","
}
fn negword(&self) -> &str {
NEGWORD
}
fn pointword(&self) -> &str {
"przecinek"
}
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> {
if value.is_zero() {
return Ok("zerowy".to_string());
}
let mut words: Vec<String> = Vec::new();
let mut fragments = splitbyx(&value.to_string(), 3)?;
let mut level: usize = 0;
let mut last = fragments
.last()
.ok_or_else(|| index_error("list index out of range"))?
.clone();
while last.is_zero() {
level += 1;
fragments.pop();
last = fragments
.last()
.ok_or_else(|| index_error("list index out of range"))?
.clone();
}
if fragments.len() > 1 {
let pow = BigInt::from(1000).pow(level as u32);
let pre_part = self.int2word(&(value - &last * pow))?;
words.push(pre_part);
}
self.last_fragment_to_ordinal(&last, &mut words, if level == 0 { 0 } else { 1 })?;
let mut output = words.join(" ");
if last.is_one() && level > 0 && !output.is_empty() {
output.push(' ');
}
if level > 0 {
let p = PREFIXES_ORDINAL
.get(level)
.filter(|_| level >= 1)
.copied()
.ok_or_else(|| key_error(level.to_string()))?;
output.push_str(p);
}
Ok(output)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { value: f, .. } => {
if *f == f.trunc() {
let n = BigInt::from_f64(*f).ok_or_else(|| {
value_error(format!("cannot convert float {} to integer", f))
})?;
return self.int2word(&n);
}
self.cardinal_from_str(&format!("{}", f))
}
FloatValue::Decimal { value: d, .. } => {
self.cardinal_from_str(&python_decimal_str(d))
}
}
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if value.as_whole_int().is_none() {
return Err(N2WError::NotImplemented(String::new()));
}
match value {
FloatValue::Float { value, .. } => Err(value_error(format!(
"invalid literal for int() with base 10: '{}'",
value
))),
FloatValue::Decimal { value: d, .. } => {
let s = python_decimal_str(d);
if is_plain_int_str(&s) {
self.to_ordinal(&d.with_scale(0).as_bigint_and_exponent().0)
} else {
Err(value_error(format!(
"invalid literal for int() with base 10: '{}'",
s
)))
}
}
}
}
fn neg_zero_decimal(&self, to: &str) -> Option<Result<String>> {
match to {
"cardinal" | "year" => Some(self.cardinal_from_str("-0.0")),
_ => None,
}
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { negative } => Err(value_error(format!(
"invalid literal for int() with base 10: '{}Infinity'",
if negative { "-" } else { "" }
))),
other => Ok(other),
}
}
fn lang_name(&self) -> &str {
"Num2Word_PL"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
let form = plural_form_index(n);
forms
.get(form)
.cloned()
.ok_or_else(|| index_error("tuple index out of range"))
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
) -> Result<String> {
let separator = separator.unwrap_or(self.default_separator());
if let CurrencyValue::Int(v) = val {
let cr1 = match self.currency_forms.get(currency) {
Some(forms) => &forms.unit,
None => {
return default_to_currency(self, val, currency, cents, 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 > BigInt::one() && money_str.ends_with("jeden") {
if cr1.len() > 1 {
cr1[1].clone()
} else {
cr1.first()
.cloned()
.ok_or_else(|| index_error("tuple index out of range"))?
}
} else {
Lang::pluralize(self, &abs_val, cr1)?
};
return Ok(format!("{} {} {}", minus_str, money_str, currency_str)
.trim()
.to_string());
}
default_to_currency(self, val, currency, cents, separator, adjective)
}
}
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 chained = m10 < BigInt::from(5) && m10 > BigInt::one();
if chained && (m100 < BigInt::from(10) || m100 > BigInt::from(20)) {
1
} else {
2
}
}
#[cfg(test)]
mod tests {
use super::*;
use bigdecimal::BigDecimal;
#[test]
fn plural_rule_matches_cardinal_path() {
let lang = LangPl::new();
let forms = [
"a".to_string(),
"b".to_string(),
"c".to_string(),
];
for n in 0i64..=1000 {
let n = BigInt::from(n);
let via_cardinal = lang.pluralize(&n, &forms);
let via_currency = Lang::pluralize(&lang, &n, &forms).unwrap();
assert_eq!(via_cardinal, via_currency, "n = {}", n);
}
}
#[test]
fn plural_form_buckets() {
let cases: &[(i64, usize)] = &[
(0, 2),
(1, 0),
(2, 1),
(4, 1),
(5, 2),
(11, 2),
(12, 2),
(14, 2),
(21, 2),
(22, 1),
(25, 2),
(100, 2),
(1234, 1),
];
for (n, want) in cases {
assert_eq!(plural_form_index(&BigInt::from(*n)), *want, "n = {}", n);
}
}
fn f(lang: &LangPl, v: f64) -> Result<String> {
lang.to_cardinal_float(&FloatValue::Float { value: v, precision: 0 }, None)
}
fn dec(lang: &LangPl, s: &str) -> Result<String> {
let value: BigDecimal = s.parse().unwrap();
let precision = match s.split_once('.') {
Some((_, frac)) => frac.len() as u32,
None => 0,
};
lang.to_cardinal_float(&FloatValue::Decimal { value, precision }, None)
}
#[test]
fn float_corpus() {
let lang = LangPl::new();
let cases: &[(f64, &str)] = &[
(0.0, "zero"),
(0.5, "zero przecinek pięć"),
(1.0, "jeden"),
(1.5, "jeden przecinek pięć"),
(2.25, "dwa przecinek dwa pięć"),
(3.14, "trzy przecinek jeden cztery"),
(0.01, "zero przecinek zero jeden"),
(0.1, "zero przecinek jeden"),
(0.99, "zero przecinek dziewięć dziewięć"),
(1.01, "jeden przecinek zero jeden"),
(12.34, "dwanaście przecinek trzy cztery"),
(99.99, "dziewięćdziesiąt dziewięć przecinek dziewięć dziewięć"),
(100.5, "sto przecinek pięć"),
(1234.56, "tysiąc dwieście trzydzieści cztery przecinek pięć sześć"),
(-0.5, "minus zero przecinek pięć"),
(-1.5, "minus jeden przecinek pięć"),
(-12.34, "minus dwanaście przecinek trzy cztery"),
(1.005, "jeden przecinek zero zero pięć"),
(2.675, "dwa przecinek sześć siedem pięć"),
];
for (v, want) in cases {
assert_eq!(&f(&lang, *v).unwrap(), want, "v = {}", v);
}
}
#[test]
fn whole_floats_and_negatives() {
let lang = LangPl::new();
assert_eq!(f(&lang, 0.0).unwrap(), "zero");
assert_eq!(f(&lang, 1.0).unwrap(), "jeden");
assert_eq!(f(&lang, 100.0).unwrap(), "sto");
assert_eq!(
f(&lang, 9999.0).unwrap(),
"dziewięć tysięcy dziewięćset dziewięćdziesiąt dziewięć"
);
assert_eq!(f(&lang, 1e16).unwrap(), "dziesięć biliardów");
assert_eq!(f(&lang, 1e21).unwrap(), "tryliard");
assert_eq!(
f(&lang, 1e23).unwrap(),
"dziewięćdziesiąt dziewięć tryliardów dziewięćset dziewięćdziesiąt \
dziewięć trylionów dziewięćset dziewięćdziesiąt dziewięć biliardów \
dziewięćset dziewięćdziesiąt dziewięć bilionów dziewięćset \
dziewięćdziesiąt dziewięć miliardów dziewięćset dziewięćdziesiąt \
jeden milionów sześćset jedenaście tysięcy trzysta \
dziewięćdziesiąt dwa"
);
for v in [-1.0, -5.0, -9999.0] {
match f(&lang, v) {
Err(N2WError::Value(m)) => {
assert_eq!(m, "invalid literal for int() with base 10: '-'")
}
other => panic!("v = {} expected ValueError, got {:?}", v, other),
}
}
}
#[test]
fn decimal_corpus() {
let lang = LangPl::new();
let cases: &[(&str, &str)] = &[
("0.01", "zero przecinek zero jeden"),
("1.10", "jeden przecinek jeden zero"),
("12.345", "dwanaście przecinek trzy cztery pięć"),
(
"98746251323029.99",
"dziewięćdziesiąt osiem bilionów siedemset czterdzieści sześć \
miliardów dwieście pięćdziesiąt jeden milionów trzysta \
dwadzieścia trzy tysiące dwadzieścia dziewięć przecinek \
dziewięć dziewięć",
),
("0.001", "zero przecinek zero zero jeden"),
("5", "pięć"),
("-5", "minus pięć"),
("0", "zero"),
("5.0", "pięć przecinek zero"),
("1.00", "jeden przecinek zero zero"),
("0.0", "zero przecinek zero"),
("0.00", "zero przecinek zero zero"),
("0.99", "zero przecinek dziewięć dziewięć"),
("-12.34", "minus dwanaście przecinek trzy cztery"),
];
for (s, want) in cases {
assert_eq!(&dec(&lang, s).unwrap(), want, "dec = {}", s);
}
}
}