use crate::Result;
use crate::currency::vocab::ES 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::{Gender, options};
use num_bigint::BigUint;
use num_traits::{One, ToPrimitive, Zero};
const ONES_MASC: [&str; 10] = [
"", "uno", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve",
];
const ONES_FEM: [&str; 10] = [
"", "una", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve",
];
const ONES_MASC_APOC: [&str; 10] = [
"", "un", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve",
];
const TEENS: [&str; 10] = [
"diez",
"once",
"doce",
"trece",
"catorce",
"quince",
"dieciséis",
"diecisiete",
"dieciocho",
"diecinueve",
];
const TWENTIES_MASC: [&str; 10] = [
"veinte",
"veintiuno",
"veintidós",
"veintitrés",
"veinticuatro",
"veinticinco",
"veintiséis",
"veintisiete",
"veintiocho",
"veintinueve",
];
const TWENTIES_FEM: [&str; 10] = [
"veinte",
"veintiuna",
"veintidós",
"veintitrés",
"veinticuatro",
"veinticinco",
"veintiséis",
"veintisiete",
"veintiocho",
"veintinueve",
];
const TWENTIES_MASC_APOC: [&str; 10] = [
"veinte",
"veintiún",
"veintidós",
"veintitrés",
"veinticuatro",
"veinticinco",
"veintiséis",
"veintisiete",
"veintiocho",
"veintinueve",
];
const TENS: [&str; 10] = [
"",
"",
"",
"treinta",
"cuarenta",
"cincuenta",
"sesenta",
"setenta",
"ochenta",
"noventa",
];
const HUNDREDS_MASC: [&str; 10] = [
"",
"ciento",
"doscientos",
"trescientos",
"cuatrocientos",
"quinientos",
"seiscientos",
"setecientos",
"ochocientos",
"novecientos",
];
const HUNDREDS_FEM: [&str; 10] = [
"",
"ciento",
"doscientas",
"trescientas",
"cuatrocientas",
"quinientas",
"seiscientas",
"setecientas",
"ochocientas",
"novecientas",
];
const SCALES: &[&str] = &["millón", "billón", "trillón", "cuatrillón"];
const SCALES_PLURAL: &[&str] = &["millones", "billones", "trillones", "cuatrillones"];
pub const CARDINAL_MAX: Max = Max::long_scale(SCALES.len() as u32);
pub const ORDINAL_MAX: Max = Max::bounded(9);
pub const CURRENCY_MAX: Max = Max::long_scale(SCALES.len() as u32);
const THOUSAND: &str = "mil";
pub(crate) const ZERO: &str = "cero";
pub(crate) const NEGATIVE: &str = "menos";
pub(crate) const DECIMAL_SEP: &str = "punto";
const ORDINAL_ONES_MASC: [&str; 10] = [
"", "primero", "segundo", "tercero", "cuarto", "quinto", "sexto", "séptimo", "octavo", "noveno",
];
const ORDINAL_ONES_FEM: [&str; 10] = [
"", "primera", "segunda", "tercera", "cuarta", "quinta", "sexta", "séptima", "octava", "novena",
];
const ORDINAL_TEENS_MASC: [&str; 10] = [
"décimo",
"undécimo",
"duodécimo",
"decimotercero",
"decimocuarto",
"decimoquinto",
"decimosexto",
"decimoséptimo",
"decimoctavo",
"decimonoveno",
];
const ORDINAL_TEENS_FEM: [&str; 10] = [
"décima",
"undécima",
"duodécima",
"decimotercera",
"decimocuarta",
"decimoquinta",
"decimosexta",
"decimoséptima",
"decimoctava",
"decimonovena",
];
const ORDINAL_TWENTIES_MASC: [&str; 10] = [
"vigésimo",
"vigesimoprimero",
"vigesimosegundo",
"vigesimotercero",
"vigesimocuarto",
"vigesimoquinto",
"vigesimosexto",
"vigesimoséptimo",
"vigesimoctavo",
"vigesimonoveno",
];
const ORDINAL_TWENTIES_FEM: [&str; 10] = [
"vigésima",
"vigesimoprimera",
"vigesimosegunda",
"vigesimotercera",
"vigesimocuarta",
"vigesimoquinta",
"vigesimosexta",
"vigesimoséptima",
"vigesimoctava",
"vigesimonovena",
];
const ORDINAL_TENS_MASC: [&str; 10] = [
"",
"décimo",
"vigésimo",
"trigésimo",
"cuadragésimo",
"quincuagésimo",
"sexagésimo",
"septuagésimo",
"octogésimo",
"nonagésimo",
];
const ORDINAL_TENS_FEM: [&str; 10] = [
"",
"décima",
"vigésima",
"trigésima",
"cuadragésima",
"quincuagésima",
"sexagésima",
"septuagésima",
"octogésima",
"nonagésima",
];
const ORDINAL_HUNDREDS_MASC: [&str; 10] = [
"",
"centésimo",
"ducentésimo",
"tricentésimo",
"cuadringentésimo",
"quingentésimo",
"sexcentésimo",
"septingentésimo",
"octingentésimo",
"noningentésimo",
];
const ORDINAL_HUNDREDS_FEM: [&str; 10] = [
"",
"centésima",
"ducentésima",
"tricentésima",
"cuadringentésima",
"quingentésima",
"sexcentésima",
"septingentésima",
"octingentésima",
"noningentésima",
];
const FUSED_ONES: [&str; 10] = [
"", "un", "dos", "tres", "cuatro", "cinco", "seis", "siete", "ocho", "nueve",
];
const FUSED_TEENS: [&str; 10] = [
"diez",
"once",
"doce",
"trece",
"catorce",
"quince",
"dieciseis",
"diecisiete",
"dieciocho",
"diecinueve",
];
const FUSED_TWENTIES: [&str; 10] = [
"veinte",
"veintiun",
"veintidos",
"veintitres",
"veinticuatro",
"veinticinco",
"veintiseis",
"veintisiete",
"veintiocho",
"veintinueve",
];
const ORDINAL_THOUSAND_MASC: &str = "milésimo";
const ORDINAL_THOUSAND_FEM: &str = "milésima";
const ORDINAL_MILLION_MASC: &str = "millonésimo";
const ORDINAL_MILLION_FEM: &str = "millonésima";
pub(crate) const CURRENCY_CONNECTOR: &str = "con";
pub(crate) fn build_segment(n: u32, feminine: bool, apocopate: bool) -> String {
if n == 0 {
return String::new();
}
if n == 100 {
return "cien".to_string();
}
let ones = (n % 10) as usize;
let tens = ((n / 10) % 10) as usize;
let hundreds = (n / 100) as usize;
let tens_ones = (n % 100) as usize;
let masculine_ones = if apocopate {
&ONES_MASC_APOC
} else {
&ONES_MASC
};
let masculine_twenties = if apocopate {
&TWENTIES_MASC_APOC
} else {
&TWENTIES_MASC
};
let ones_words = if feminine { &ONES_FEM } else { masculine_ones };
let mut result = String::new();
if hundreds > 0 {
let hundreds_words = if feminine {
&HUNDREDS_FEM
} else {
&HUNDREDS_MASC
};
result.push_str(hundreds_words[hundreds]);
}
if tens_ones > 0 {
if !result.is_empty() {
result.push(' ');
}
if tens_ones < 10 {
result.push_str(ones_words[tens_ones]);
} else if tens_ones < 20 {
result.push_str(TEENS[ones]);
} else if tens_ones < 30 {
let twenties = if feminine {
&TWENTIES_FEM
} else {
masculine_twenties
};
result.push_str(twenties[ones]);
} else if ones == 0 {
result.push_str(TENS[tens]);
} else {
result.push_str(TENS[tens]);
result.push_str(" y ");
result.push_str(ones_words[ones]);
}
}
result
}
fn build_below_million(n: u32, feminine: bool, apocopate: bool) -> String {
if n < 1000 {
return build_segment(n, feminine, apocopate);
}
let thousands = n / 1000;
let remainder = n % 1000;
let mut result = if thousands == 1 {
THOUSAND.to_string()
} else {
format!("{} {THOUSAND}", build_segment(thousands, false, true))
};
if remainder > 0 {
result.push(' ');
result.push_str(&build_segment(remainder, feminine, apocopate));
}
result
}
fn integer_to_words(n: &BigUint, feminine: bool, before_noun: bool) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1_000_000 {
return build_below_million(small as u32, feminine, before_noun);
}
}
build_large_number_words(n, feminine, before_noun)
}
fn build_large_number_words(n: &BigUint, feminine: bool, before_noun: bool) -> String {
let groups = split_groups(n, 1_000_000);
let mut result = String::new();
for (i, &group) in groups.iter().enumerate().skip(1).rev() {
if group == 0 {
continue;
}
if !result.is_empty() {
result.push(' ');
}
result.push_str(&build_below_million(group, false, true));
result.push(' ');
result.push_str(if group == 1 {
SCALES[i - 1]
} else {
SCALES_PLURAL[i - 1]
});
}
if groups[0] > 0 {
result.push(' ');
result.push_str(&build_below_million(groups[0], feminine, before_noun));
}
result
}
fn decimal_part_to_words(decimal: &str, feminine: bool) -> 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),
feminine,
false,
));
}
result
}
options! {
pub struct CardinalOptions {
gender: Gender = Gender::Masculine;
}
}
pub fn to_cardinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_cardinal_with(value, &CardinalOptions::default())
}
pub fn to_cardinal_with<'a>(
value: impl Into<Number<'a>>,
options: &CardinalOptions,
) -> Result<String> {
let number = parse_cardinal(&value.into())?;
CARDINAL_MAX.check(&number.integer, number.fraction.as_deref())?;
let feminine = options.gender == Gender::Feminine;
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&number.integer, feminine, false));
if let Some(fraction) = &number.fraction {
result.push(' ');
result.push_str(DECIMAL_SEP);
result.push(' ');
result.push_str(&decimal_part_to_words(fraction, feminine));
}
Ok(result)
}
fn build_ordinal_segment(n: u32, feminine: bool) -> 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(if feminine {
ORDINAL_HUNDREDS_FEM[hundreds]
} else {
ORDINAL_HUNDREDS_MASC[hundreds]
});
}
if tens == 1 {
parts.push(if feminine {
ORDINAL_TEENS_FEM[ones]
} else {
ORDINAL_TEENS_MASC[ones]
});
} else if tens == 2 {
parts.push(if feminine {
ORDINAL_TWENTIES_FEM[ones]
} else {
ORDINAL_TWENTIES_MASC[ones]
});
} else {
if tens > 0 {
parts.push(if feminine {
ORDINAL_TENS_FEM[tens]
} else {
ORDINAL_TENS_MASC[tens]
});
}
if ones > 0 {
parts.push(if feminine {
ORDINAL_ONES_FEM[ones]
} else {
ORDINAL_ONES_MASC[ones]
});
}
}
parts.join(" ")
}
fn fused_multiplier(n: u32) -> String {
if n == 100 {
return "cien".to_string();
}
let tens_ones = (n % 100) as usize;
let mut result = HUNDREDS_MASC[(n / 100) as usize].to_string();
if tens_ones < 10 {
result.push_str(FUSED_ONES[tens_ones]);
} else if tens_ones < 20 {
result.push_str(FUSED_TEENS[tens_ones - 10]);
} else if tens_ones < 30 {
result.push_str(FUSED_TWENTIES[tens_ones - 20]);
} else {
result.push_str(TENS[tens_ones / 10]);
if tens_ones % 10 > 0 {
result.push('i');
result.push_str(FUSED_ONES[tens_ones % 10]);
}
}
result
}
fn integer_to_ordinal(n: u64, feminine: bool) -> String {
let thousand_word = if feminine {
ORDINAL_THOUSAND_FEM
} else {
ORDINAL_THOUSAND_MASC
};
let million_word = if feminine {
ORDINAL_MILLION_FEM
} else {
ORDINAL_MILLION_MASC
};
if n < 1000 {
return build_ordinal_segment(n as u32, feminine);
}
if n < 1_000_000 {
let thousands = (n / 1000) as u32;
let remainder = (n % 1000) as u32;
let mut result = if thousands == 1 {
thousand_word.to_string()
} else {
format!("{}{thousand_word}", fused_multiplier(thousands))
};
if remainder > 0 {
result.push(' ');
result.push_str(&build_ordinal_segment(remainder, feminine));
}
return result;
}
let millions = (n / 1_000_000) as u32;
let remainder = n % 1_000_000;
let mut result = if millions == 1 {
million_word.to_string()
} else {
format!("{}{million_word}", fused_multiplier(millions))
};
if remainder > 0 {
result.push(' ');
result.push_str(&integer_to_ordinal(remainder, feminine));
}
result
}
options! {
pub struct OrdinalOptions {
gender: Gender = Gender::Masculine;
}
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_ordinal_with(value, &OrdinalOptions::default())
}
pub fn to_ordinal_with<'a>(
value: impl Into<Number<'a>>,
options: &OrdinalOptions,
) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
let n = n
.to_u64()
.expect("below the ordinal ceiling, which fits a u64");
Ok(integer_to_ordinal(n, options.gender == Gender::Feminine))
}
options! {
pub struct CurrencyOptions {
and: bool = true;
currency: Currency = Currency::EUR, table = CURRENCY_VOCAB;
}
}
pub(crate) fn is_round_millions(n: &BigUint) -> bool {
*n >= BigUint::from(1_000_000u32) && (n % 1_000_000u32).is_zero()
}
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 major_feminine = words.major_gender == Some(Gender::Feminine);
let minor_feminine = words.minor_gender == Some(Gender::Feminine);
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
if !money.major.is_zero() || money.minor == 0 {
if money.major.is_one() {
result.push_str(if major_feminine { "una " } else { "un " });
result.push_str(words.major_at(0));
} else {
let preposition = if is_round_millions(&money.major) {
" de "
} else {
" "
};
result.push_str(&integer_to_words(&money.major, major_feminine, true));
result.push_str(preposition);
result.push_str(words.major_at(1));
}
}
if money.minor > 0 {
if !money.major.is_zero() {
if options.and {
result.push(' ');
result.push_str(CURRENCY_CONNECTOR);
}
result.push(' ');
}
if money.minor == 1 {
result.push_str(if minor_feminine { "una " } else { "un " });
result.push_str(words.minor_at(0));
} else {
result.push_str(&integer_to_words(
&BigUint::from(money.minor),
minor_feminine,
true,
));
result.push(' ');
result.push_str(words.minor_at(1));
}
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "es-ES",
name: "European Spanish",
kind: Kind::Full,
cardinal: Some(form!(with_options to_cardinal_with, CardinalOptions, CARDINAL_MAX)),
ordinal: Some(form!(with_options to_ordinal_with, OrdinalOptions, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};