use crate::Result;
use crate::currency::vocab::PT_PT 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] = [
"", "um", "dois", "três", "quatro", "cinco", "seis", "sete", "oito", "nove",
];
const TEENS: [&str; 10] = [
"dez",
"onze",
"doze",
"treze",
"catorze",
"quinze",
"dezasseis",
"dezassete",
"dezoito",
"dezanove",
];
const TENS: [&str; 10] = [
"",
"",
"vinte",
"trinta",
"quarenta",
"cinquenta",
"sessenta",
"setenta",
"oitenta",
"noventa",
];
const HUNDREDS: [&str; 10] = [
"",
"cento",
"duzentos",
"trezentos",
"quatrocentos",
"quinhentos",
"seiscentos",
"setecentos",
"oitocentos",
"novecentos",
];
const THOUSAND: &str = "mil";
const ZERO: &str = "zero";
const NEGATIVE: &str = "menos";
const DECIMAL_SEP: &str = "vírgula";
const ORDINAL_ONES: [&str; 10] = [
"", "primeiro", "segundo", "terceiro", "quarto", "quinto", "sexto", "sétimo", "oitavo", "nono",
];
const ORDINAL_TEENS: [&str; 10] = [
"décimo",
"décimo primeiro",
"décimo segundo",
"décimo terceiro",
"décimo quarto",
"décimo quinto",
"décimo sexto",
"décimo sétimo",
"décimo oitavo",
"décimo nono",
];
const ORDINAL_TENS: [&str; 10] = [
"",
"",
"vigésimo",
"trigésimo",
"quadragésimo",
"quinquagésimo",
"sexagésimo",
"septuagésimo",
"octogésimo",
"nonagésimo",
];
const ORDINAL_HUNDREDS: [&str; 10] = [
"",
"centésimo",
"ducentésimo",
"tricentésimo",
"quadringentésimo",
"quingentésimo",
"sexcentésimo",
"septingentésimo",
"octingentésimo",
"nongentésimo",
];
const SCALES_SINGULAR: &[&str] = &["milhão", "bilião", "trilião", "quatrilião"];
const SCALES_PLURAL: &[&str] = &["milhões", "biliões", "triliões", "quatriliões"];
const SCALE_ORDINAL: &[&str] = &[
"",
"milésimo",
"milionésimo",
"mil milionésimo",
"bilionésimo",
"mil bilionésimo",
"trilionésimo",
];
pub const CARDINAL_MAX: Max = Max::long_scale(SCALES_SINGULAR.len() as u32);
pub const ORDINAL_MAX: Max = Max::western(SCALE_ORDINAL.len() as u32 - 1);
pub const CURRENCY_MAX: Max = Max::long_scale(SCALES_SINGULAR.len() as u32);
struct Segment {
word: String,
is_exact_hundred: bool,
starts_with_hundreds: bool,
}
impl Segment {
fn takes_e(&self) -> bool {
!self.starts_with_hundreds || self.is_exact_hundred
}
}
fn build_segment(n: u32) -> Segment {
if n == 0 {
return Segment {
word: String::new(),
is_exact_hundred: false,
starts_with_hundreds: false,
};
}
if n == 100 {
return Segment {
word: "cem".to_string(),
is_exact_hundred: true,
starts_with_hundreds: true,
};
}
let ones = (n % 10) as usize;
let tens = ((n / 10) % 10) as usize;
let hundreds = (n / 100) as usize;
let mut word = String::new();
if hundreds > 0 {
word.push_str(HUNDREDS[hundreds]);
}
let tens_ones = if tens == 1 {
TEENS[ones].to_string()
} else if tens >= 2 {
if ones > 0 {
format!("{} e {}", TENS[tens], ONES[ones])
} else {
TENS[tens].to_string()
}
} else if ones > 0 {
ONES[ones].to_string()
} else {
String::new()
};
if !tens_ones.is_empty() {
if !word.is_empty() {
word.push_str(" e ");
}
word.push_str(&tens_ones);
}
Segment {
word,
is_exact_hundred: hundreds > 0 && tens == 0 && ones == 0,
starts_with_hundreds: n >= 100,
}
}
fn integer_to_words(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1_000_000 {
return build_group(small as u32);
}
}
build_large_number_words(n)
}
fn build_group(n: u32) -> String {
if n < 1000 {
return build_segment(n).word;
}
let thousands = n / 1000;
let remainder = n % 1000;
let mut result = if thousands == 1 {
THOUSAND.to_string()
} else {
format!("{} {THOUSAND}", build_segment(thousands).word)
};
if remainder > 0 {
let segment = build_segment(remainder);
result.push_str(if segment.takes_e() { " e " } else { " " });
result.push_str(&segment.word);
}
result
}
fn build_large_number_words(n: &BigUint) -> String {
let groups = split_groups(n, 1_000_000);
let lowest = groups.iter().position(|&g| g != 0).unwrap_or(0);
let mut result = String::new();
for (i, &group) in groups.iter().enumerate().rev() {
if group == 0 {
continue;
}
if !result.is_empty() {
let upper = group / 1000;
let lower = group % 1000;
let single = if upper == 0 {
lower
} else if lower == 0 {
upper
} else {
0
};
let takes_e = i == lowest && single > 0 && (single < 100 || single % 100 == 0);
result.push_str(if takes_e { " e " } else { " " });
}
if i == 0 {
result.push_str(&build_group(group));
} else if group == 1 {
result.push_str("um ");
result.push_str(SCALES_SINGULAR[i - 1]);
} else {
result.push_str(&build_group(group));
result.push(' ');
result.push_str(SCALES_PLURAL[i - 1]);
}
}
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_SEP);
result.push(' ');
result.push_str(&decimal_part_to_words(fraction));
}
Ok(result)
}
fn build_ordinal_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(ORDINAL_HUNDREDS[hundreds]);
}
if tens == 1 {
parts.push(ORDINAL_TEENS[ones]);
} else if tens >= 2 {
parts.push(ORDINAL_TENS[tens]);
if ones > 0 {
parts.push(ORDINAL_ONES[ones]);
}
} else if ones > 0 {
parts.push(ORDINAL_ONES[ones]);
}
parts.join(" ")
}
fn build_ordinal_group(n: u32) -> String {
if n < 1000 {
return build_ordinal_segment(n);
}
let thousands = n / 1000;
let remainder = n % 1000;
let words = if thousands == 1 {
SCALE_ORDINAL[1].to_string()
} else {
format!("{} {}", build_ordinal_segment(thousands), SCALE_ORDINAL[1])
};
if remainder == 0 {
words
} else {
format!("{words} {}", build_ordinal_segment(remainder))
}
}
fn build_large_ordinal(n: &BigUint) -> String {
let segments = split_groups(n, 1000);
let lowest = segments.iter().position(|&s| s != 0).unwrap_or(0);
let mut i = segments.len() - 1;
let mut first = segments[i];
if i >= 3 && i % 2 == 1 && segments[i - 1] != 0 {
i -= 1;
first = first * 1000 + segments[i];
} else if i == lowest {
return if first == 1 {
SCALE_ORDINAL[i].to_string()
} else {
format!("{} {}", build_ordinal_segment(first), SCALE_ORDINAL[i])
};
}
let mut result = SCALE_ORDINAL[i].to_string();
if first != 1 {
result = format!("{} {result}", build_group(first));
}
while i > lowest {
i -= 1;
let mut value = segments[i];
if i >= 3 && i % 2 == 1 {
i -= 1;
value = value * 1000 + segments[i];
}
if value == 0 {
continue;
}
result.push(' ');
if i == 0 {
result.push_str(&build_ordinal_segment(value));
} else if value == 1 {
result.push_str(SCALE_ORDINAL[i]);
} else {
result.push_str(&build_ordinal_group(value));
result.push(' ');
result.push_str(SCALE_ORDINAL[i]);
}
}
result
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
if let Some(small) = n.to_u64() {
if small < 1000 {
return Ok(build_ordinal_segment(small as u32));
}
}
Ok(build_large_ordinal(&n))
}
options! {
pub struct CurrencyOptions {
and: bool = true;
currency: Currency = Currency::EUR, table = CURRENCY_VOCAB;
}
}
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 has_major = !money.major.is_zero();
let has_minor = money.minor > 0;
if !has_major && !has_minor {
return Ok(format!("{ZERO} {}", words.major_at(1)));
}
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
if has_major {
let preposition = if is_round_millions(&money.major) {
" de "
} else {
" "
};
result.push_str(&integer_to_words(&money.major));
result.push_str(preposition);
result.push_str(words.major_at(if money.major.is_one() { 0 } else { 1 }));
}
if has_minor {
if has_major {
result.push_str(if options.and { " e " } else { " " });
}
result.push_str(&integer_to_words(&BigUint::from(money.minor)));
result.push(' ');
result.push_str(words.minor_at(if money.minor == 1 { 0 } else { 1 }));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "pt-PT",
name: "European Portuguese",
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)),
};