use crate::Result;
use crate::currency::vocab::ES as CURRENCY_VOCAB;
use crate::currency::{Currency, assert_minor_unit};
use crate::es_es::{
CURRENCY_CONNECTOR, DECIMAL_SEP, NEGATIVE, ZERO, build_segment, is_round_millions,
};
use crate::language::{Kind, Language, form};
use crate::max::Max;
use crate::number::{Number, digits_to_biguint, parse_cardinal, parse_currency, split_groups};
use crate::options::{Gender, options};
use num_bigint::BigUint;
use num_traits::{One, ToPrimitive, Zero};
pub use crate::es_es::{ORDINAL_MAX, OrdinalOptions, to_ordinal, to_ordinal_with};
const SCALES: &[&str] = &[
"mil",
"millón",
"billón",
"trillón",
"cuatrillón",
"quintillón",
];
const SCALES_PLURAL: &[&str] = &[
"mil",
"millones",
"billones",
"trillones",
"cuatrillones",
"quintillones",
];
pub const CARDINAL_MAX: Max = Max::western(SCALES.len() as u32);
pub const CURRENCY_MAX: Max = Max::western(SCALES.len() as u32);
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 < 1000 {
return build_segment(small as u32, feminine, before_noun);
}
}
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, feminine, before_noun));
} else if i == 1 {
if segment != 1 {
result.push_str(&build_segment(segment, false, true));
result.push(' ');
}
result.push_str(SCALES[0]);
} else {
let scale_index = i - 1; result.push_str(&build_segment(segment, false, true));
result.push(' ');
result.push_str(if segment == 1 {
SCALES[scale_index]
} else {
SCALES_PLURAL[scale_index]
});
}
}
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)
}
options! {
pub struct CurrencyOptions {
and: bool = true;
currency: Currency = Currency::USD, 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 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-US",
name: "Spanish (United States)",
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)),
};