use crate::base::{Lang, N2WError, Result};
use crate::floatpath::FloatValue;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{Signed, ToPrimitive};
const ZERO: &str = "nolla";
const MINUS_PREFIX_WORD: &str = "minus ";
const FLOAT_INFIX_WORD: &str = " comma ";
const CARDINAL_WORDS: [&str; 20] = [
ZERO,
"ün",
"duos",
"trais",
"quatter",
"tschinch",
"ses",
"set",
"ot",
"nouv",
"desch",
"ündesch",
"dudesch",
"traidesch",
"quattordesch",
"quindesch",
"saidesch",
"deschset",
"deschdot",
"deschnouv",
];
const ORDINAL_WORDS: [&str; 21] = [
ZERO,
"prüm",
"seguond",
"terz",
"quart",
"tschinchavel",
"sesavel",
"settavel",
"ottavel",
"nouvavel",
"deschavel",
"ündeschavel",
"dudeschavel",
"traideschavel",
"quattordeschavel",
"quindeschavel",
"saideschavel",
"deschsettavel",
"deschdottavel",
"deschnouvavel",
"vainchavel",
];
fn str_tens(tens: &BigInt) -> Option<&'static str> {
match tens.to_u32()? {
2 => Some("vainche"),
3 => Some("trenta"),
4 => Some("quaranta"),
5 => Some("tschinquanta"),
6 => Some("sesanta"),
7 => Some("settanta"),
8 => Some("ottanta"),
9 => Some("novanta"),
_ => None,
}
}
const EXPONENT_PREFIXES: [&str; 11] = [
ZERO, "m", "b", "tr", "quadr", "quint", "sest", "sett", "ott", "nov", "dec",
];
fn attribute_error(msg: &str) -> N2WError {
N2WError::Attribute(msg.to_string())
}
fn parse_int(s: &str) -> Result<BigInt> {
s.parse::<BigInt>()
.map_err(|_| N2WError::Value(format!("invalid literal for int() with base 10: '{}'", s)))
}
fn float_repr_has_point(f: f64) -> bool {
f.is_finite() && (f == 0.0 || (f.abs() >= 1e-4 && f.abs() < 1e16))
}
fn f64_trunc_to_bigint(f: f64) -> Result<BigInt> {
if f.is_nan() {
return Err(N2WError::Value(
"cannot convert float NaN to integer".to_string(),
));
}
num_traits::FromPrimitive::from_f64(f.trunc())
.ok_or_else(|| N2WError::Overflow("cannot convert float infinity to integer".to_string()))
}
fn card_word(i: &BigInt) -> Result<&'static str> {
i.to_usize()
.and_then(|i| CARDINAL_WORDS.get(i).copied())
.ok_or_else(|| N2WError::Index("list index out of range".to_string()))
}
fn ord_word(i: &BigInt) -> Result<&'static str> {
i.to_usize()
.and_then(|i| ORDINAL_WORDS.get(i).copied())
.ok_or_else(|| N2WError::Index("list index out of range".to_string()))
}
fn exponent_prefix(i: usize) -> Result<&'static str> {
EXPONENT_PREFIXES
.get(i)
.copied()
.ok_or_else(|| N2WError::Index("list index out of range".to_string()))
}
fn phonetic_contraction(s: &str) -> String {
s.replace("aün", "ün") .replace("eün", "ün") .replace("aot", "ot") .replace("eot", "ot") .replace("vainche_", "vainch") .replace('_', "")
}
fn adapt_hundred(s: &str) -> String {
s.replace("duostschient", "duatschient")
.replace("traistschient", "trajatschient")
.replace("eün", "edün")
.replace("eot", "edot")
}
fn adapt_thousand(s: &str) -> String {
s.replace("duosmilli", "duamilli")
.replace("traismilli", "trajamilli")
.replace("eün", "edün")
.replace("eot", "edot")
}
fn adapt_milliard(s: &str) -> String {
let s = format!(" {} ", s);
s.replace(" e ün", " ed ün").replace(" e ot", " ed ot")
}
fn exponent_length_to_string(exponent_length: usize) -> Result<String> {
let prefix = exponent_prefix(exponent_length / 6)?;
if exponent_length % 6 == 0 {
Ok(format!("{}illiun", prefix))
} else {
Ok(format!("{}illiard", prefix))
}
}
fn omitt_if_zero(number_to_string: &str) -> String {
if number_to_string == ZERO {
String::new()
} else {
number_to_string.to_string()
}
}
fn empty_if_zero(number_to_string: &str) -> String {
if number_to_string == ZERO {
"_".to_string()
} else {
number_to_string.to_string()
}
}
pub struct LangRmVallader;
impl LangRmVallader {
pub fn new() -> Self {
LangRmVallader
}
fn tens_to_cardinal(&self, number: &BigInt) -> Result<String> {
let tens = number / BigInt::from(10);
let units = number % BigInt::from(10);
let prefix = match str_tens(&tens) {
Some(p) => p.to_string(),
None => {
let word = card_word(&tens)?;
let mut chars: Vec<char> = word.chars().collect();
chars.pop();
format!("{}anta", chars.into_iter().collect::<String>())
}
};
let postfix = empty_if_zero(card_word(&units)?);
Ok(phonetic_contraction(&format!("{}{}", prefix, postfix)))
}
fn hundreds_to_cardinal(&self, number: &BigInt) -> Result<String> {
let hundreds = number / BigInt::from(100);
let tens = number % BigInt::from(100);
let mut prefix = "tschient".to_string();
if hundreds != BigInt::from(1) {
prefix = format!("{}tschient", card_word(&hundreds)?);
}
let postfix = omitt_if_zero(&self.to_cardinal(&tens)?);
let mut infix = "";
let t = tens.to_u32().unwrap_or(u32::MAX);
if (t > 0 && t <= 13) || matches!(t, 15 | 16 | 20 | 30) {
infix = "e";
}
Ok(adapt_hundred(&format!("{}{}{}", prefix, infix, postfix)))
}
fn thousands_to_cardinal(&self, number: &BigInt) -> Result<String> {
let thousands = number / BigInt::from(1000);
let hundreds = number % BigInt::from(1000);
let mut prefix = "milli".to_string();
if thousands != BigInt::from(1) {
prefix = format!("{}milli", self.to_cardinal(&thousands)?);
}
let postfix = omitt_if_zero(&self.to_cardinal(&hundreds)?);
let mut infix = "";
if hundreds <= BigInt::from(100) && !postfix.is_empty() {
infix = "e";
}
Ok(adapt_thousand(&format!("{}{}{}", prefix, infix, postfix)))
}
fn big_number_to_cardinal(&self, number: &BigInt) -> Result<String> {
let digits: Vec<char> = number.to_string().chars().collect();
let length = digits.len();
if length >= 66 {
return Err(N2WError::NotImplemented(
"The given number is too large.".to_string(),
));
}
let predigits = if length % 3 == 0 { 3 } else { length % 3 };
let multiplier: String = digits[..predigits].iter().collect();
let exponent: String = digits[predigits..].iter().collect();
let mut infix = exponent_length_to_string(exponent.chars().count())?;
let prefix;
if multiplier == "1" {
prefix = "ün ".to_string();
} else {
prefix = self.to_cardinal(&parse_int(&multiplier)?)?;
infix = format!(" {}s", infix);
}
let exponent_is_all_zero = !exponent.is_empty() && exponent.chars().all(|c| c == '0');
let postfix;
if !exponent_is_all_zero {
let exponent_str = &exponent;
postfix = self.to_cardinal(&parse_int(exponent_str)?)?;
if exponent_str.starts_with("000") {
infix.push_str(" e ");
} else {
infix.push(' ');
}
} else {
postfix = String::new();
}
Ok(adapt_milliard(&format!("{}{}{}", prefix, infix, postfix))
.trim()
.to_string())
}
fn float_to_words(&self, float_number: f64, precision: u32) -> Result<String> {
let pre = f64_trunc_to_bigint(float_number)?;
let prefix = self.to_cardinal(&pre)?;
if !float_repr_has_point(float_number) {
return Err(N2WError::Index("list index out of range".to_string()));
}
let repr = format!("{:.*}", precision as usize, float_number);
let float_part = repr.split_once('.').map(|(_, f)| f).unwrap_or("");
let mut words: Vec<String> = Vec::with_capacity(float_part.len());
for c in float_part.chars() {
let d = c.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", c))
})?;
words.push(self.to_cardinal(&BigInt::from(d))?);
}
Ok(format!("{}{}{}", prefix, FLOAT_INFIX_WORD, words.join(" ")))
}
fn float_to_cardinal(&self, value: f64, precision: u32) -> Result<String> {
if value < 0.0 {
return Ok(format!(
"{}{}",
MINUS_PREFIX_WORD,
self.float_to_words(-value, precision)?
));
}
self.float_to_words(value, precision)
}
fn decimal_to_cardinal(&self, value: &BigDecimal) -> Result<String> {
if value.is_negative() {
let inner = self.decimal_to_cardinal(&(-value.clone()))?;
return Ok(format!("{}{}", MINUS_PREFIX_WORD, inner));
}
if *value < BigDecimal::from(1_000_000i64) {
return Err(N2WError::Type(
"list indices must be integers or slices, not decimal.Decimal".to_string(),
));
}
let s = crate::strnum::python_decimal_str(value);
let digits: Vec<char> = s.chars().collect();
let length = digits.len();
if length >= 66 {
return Err(N2WError::NotImplemented(
"The given number is too large.".to_string(),
));
}
if s.contains('.') {
let predigits = if length % 3 == 0 { 3 } else { length % 3 };
let exponent: String = digits[predigits..].iter().collect();
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
exponent
)));
}
let n = parse_int(&s)?;
self.big_number_to_cardinal(&n)
}
fn float_ordinal(&self, f: f64, precision: u32) -> Result<String> {
if f < 0.0 {
return Ok(format!(
"{}{}",
MINUS_PREFIX_WORD,
self.float_ordinal(-f, precision)?
));
}
if f.fract() != 0.0 {
let prefix = self.to_ordinal(&f64_trunc_to_bigint(f)?)?;
if !float_repr_has_point(f) {
return Err(N2WError::Index("list index out of range".to_string()));
}
let repr = format!("{:.*}", precision as usize, f);
let float_part = repr.split_once('.').map(|(_, x)| x).unwrap_or("");
let mut words: Vec<String> = Vec::with_capacity(float_part.len());
for c in float_part.chars() {
let d = c.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", c))
})?;
words.push(self.to_cardinal(&BigInt::from(d))?);
}
return Ok(format!("{}{}{}", prefix, FLOAT_INFIX_WORD, words.join(" ")));
}
if f <= 20.0 {
return Err(N2WError::Type(
"list indices must be integers or slices, not float".to_string(),
));
}
let cardinal = self.float_to_cardinal(f, precision)?;
let suffix = if cardinal.chars().next_back() == Some('a') {
"vel"
} else if cardinal.ends_with("set") || cardinal.ends_with("ot") {
"tavel"
} else {
"avel"
};
Ok(format!("{}{}", cardinal, suffix))
}
fn decimal_ordinal(&self, value: &BigDecimal) -> Result<String> {
if value.is_negative() {
return Ok(format!(
"{}{}",
MINUS_PREFIX_WORD,
self.decimal_ordinal(&(-value.clone()))?
));
}
if !value.is_integer() {
let pre = value.with_scale(0).as_bigint_and_exponent().0;
let prefix = self.to_ordinal(&pre)?;
let s = crate::strnum::python_decimal_str(value);
let float_part = match s.split_once('.') {
Some((_, frac)) => frac.to_string(),
None => return Err(N2WError::Index("list index out of range".to_string())),
};
let mut words: Vec<String> = Vec::with_capacity(float_part.len());
for c in float_part.chars() {
let d = c.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", c))
})?;
words.push(self.to_cardinal(&BigInt::from(d))?);
}
return Ok(format!("{}{}{}", prefix, FLOAT_INFIX_WORD, words.join(" ")));
}
if *value <= BigDecimal::from(20) {
return Err(N2WError::Type(
"list indices must be integers or slices, not decimal.Decimal".to_string(),
));
}
let cardinal = self.decimal_to_cardinal(value)?;
let suffix = if cardinal.chars().next_back() == Some('a') {
"vel"
} else if cardinal.ends_with("set") || cardinal.ends_with("ot") {
"tavel"
} else {
"avel"
};
Ok(format!("{}{}", cardinal, suffix))
}
}
impl Default for LangRmVallader {
fn default() -> Self {
LangRmVallader::new()
}
}
impl Lang for LangRmVallader {
fn to_currency(
&self,
_val: &crate::currency::CurrencyValue,
_currency: &str,
_cents: bool,
_separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
Err(N2WError::Attribute(format!(
"'{}' object has no attribute 'to_currency'",
"Num2Word_RM_VALLADER",
)))
}
fn to_cheque(&self, _val: &bigdecimal::BigDecimal, _currency: &str) -> Result<String> {
Err(N2WError::Attribute(format!(
"'{}' object has no attribute 'to_cheque'",
"Num2Word_RM_VALLADER",
)))
}
fn to_cardinal(&self, number: &BigInt) -> Result<String> {
if number.is_negative() {
return Ok(format!(
"{}{}",
MINUS_PREFIX_WORD,
self.to_cardinal(&(-number))?
));
}
if *number < BigInt::from(20) {
Ok(card_word(number)?.to_string())
} else if *number < BigInt::from(100) {
self.tens_to_cardinal(number)
} else if *number < BigInt::from(1000) {
self.hundreds_to_cardinal(number)
} else if *number < BigInt::from(1_000_000) {
self.thousands_to_cardinal(number)
} else {
self.big_number_to_cardinal(number)
}
}
fn to_ordinal(&self, number: &BigInt) -> Result<String> {
if number.is_negative() {
return Ok(format!(
"{}{}",
MINUS_PREFIX_WORD,
self.to_ordinal(&(-number))?
));
}
if *number <= BigInt::from(20) {
return Ok(ord_word(number)?.to_string());
}
let cardinal = self.to_cardinal(number)?;
let suffix = if cardinal.chars().next_back() == Some('a') {
"vel"
} else if cardinal.ends_with("set") || cardinal.ends_with("ot") {
"tavel"
} else {
"avel"
};
Ok(cardinal + suffix)
}
fn to_ordinal_num(&self, _value: &BigInt) -> Result<String> {
Err(attribute_error(
"'Num2Word_RM_VALLADER' object has no attribute 'to_ordinal_num'",
))
}
fn to_year(&self, _value: &BigInt) -> Result<String> {
Err(attribute_error(
"'Num2Word_RM_VALLADER' object has no attribute 'to_year'",
))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { value, precision } => self.float_to_cardinal(*value, *precision),
FloatValue::Decimal { value, .. } => self.decimal_to_cardinal(value),
}
}
fn neg_zero_decimal(&self, to: &str) -> Option<Result<String>> {
let zero = BigDecimal::from(0i64);
Some(match to {
"cardinal" => self.decimal_to_cardinal(&zero),
"ordinal" => self.decimal_ordinal(&zero),
"ordinal_num" => Err(attribute_error(
"'Num2Word_RM_VALLADER' object has no attribute 'to_ordinal_num'",
)),
"year" => Err(attribute_error(
"'Num2Word_RM_VALLADER' object has no attribute 'to_year'",
)),
_ => self.decimal_to_cardinal(&zero),
})
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { value, precision } => self.float_to_cardinal(*value, *precision),
FloatValue::Decimal { value, .. } => self.decimal_to_cardinal(value),
}
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
match value {
FloatValue::Float { value, precision } => self.float_ordinal(*value, *precision),
FloatValue::Decimal { value, .. } => self.decimal_ordinal(value),
}
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, _repr_str: &str) -> Result<String> {
Err(attribute_error(
"'Num2Word_RM_VALLADER' object has no attribute 'to_ordinal_num'",
))
}
fn year_float_entry(&self, _value: &FloatValue) -> Result<String> {
Err(attribute_error(
"'Num2Word_RM_VALLADER' object has no attribute 'to_year'",
))
}
fn str_to_number(&self, _s: &str) -> Result<crate::strnum::ParsedNumber> {
Err(attribute_error(
"'Num2Word_RM_VALLADER' object has no attribute 'str_to_number'",
))
}
fn to_fraction(&self, _numerator: &BigInt, _denominator: &BigInt) -> Result<String> {
Err(attribute_error(
"'Num2Word_RM_VALLADER' object has no attribute 'to_fraction'",
))
}
}