use crate::Result;
use crate::currency::vocab::IT 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] = [
"", "uno", "due", "tre", "quattro", "cinque", "sei", "sette", "otto", "nove",
];
const TEENS: [&str; 10] = [
"dieci",
"undici",
"dodici",
"tredici",
"quattordici",
"quindici",
"sedici",
"diciassette",
"diciotto",
"diciannove",
];
const TENS: [&str; 10] = [
"",
"",
"venti",
"trenta",
"quaranta",
"cinquanta",
"sessanta",
"settanta",
"ottanta",
"novanta",
];
const HUNDREDS: [&str; 10] = [
"",
"cento",
"duecento",
"trecento",
"quattrocento",
"cinquecento",
"seicento",
"settecento",
"ottocento",
"novecento",
];
const TENS_STEM: [&str; 10] = [
"", "", "vent", "trent", "quarant", "cinquant", "sessant", "settant", "ottant", "novant",
];
const ZERO: &str = "zero";
const NEGATIVE: &str = "meno";
const DECIMAL_SEP: &str = "virgola";
const THOUSAND_SINGULAR: &str = "mille";
const THOUSAND_PLURAL_SUFFIX: &str = "mila";
const SCALE_PREFIXES: &[&str] = &[
"m", "b", "tr", "quadr", "quint", "sest", "sett", "ott", "nov", "dec",
];
pub const CARDINAL_MAX: Max = Max::long_scale(SCALE_PREFIXES.len() as u32);
pub const ORDINAL_MAX: Max = Max::long_scale(SCALE_PREFIXES.len() as u32);
pub const CURRENCY_MAX: Max = Max::long_scale(SCALE_PREFIXES.len() as u32);
const ORDINAL_ONES: [&str; 11] = [
"", "primo", "secondo", "terzo", "quarto", "quinto", "sesto", "settimo", "ottavo", "nono",
"decimo",
];
const ORDINAL_SUFFIX: &str = "esimo";
fn build_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 result = String::new();
if hundreds > 0 {
let word = HUNDREDS[hundreds];
if tens == 8 || (tens == 0 && ones == 8) {
result.push_str(&word[..word.len() - 1]);
} else {
result.push_str(word);
}
}
if tens == 0 && ones == 0 {
} else if tens == 1 {
result.push_str(TEENS[ones]);
} else if tens >= 2 {
if ones == 1 || ones == 8 {
result.push_str(TENS_STEM[tens]);
result.push_str(ONES[ones]);
} else if ones == 3 {
result.push_str(TENS[tens]);
result.push_str("tré");
} else {
result.push_str(TENS[tens]);
result.push_str(ONES[ones]);
}
} else if ones == 3 && hundreds > 0 {
result.push_str("tré");
} else {
result.push_str(ONES[ones]);
}
result
}
fn build_segment_for_scale(n: u32) -> String {
if n == 1 {
return "un".to_string();
}
build_segment(n)
}
fn build_thousands(n: u32) -> String {
match n {
0 => String::new(),
1 => THOUSAND_SINGULAR.to_string(),
_ => build_segment_unaccented(n) + THOUSAND_PLURAL_SUFFIX,
}
}
fn build_segment_unaccented(n: u32) -> String {
let mut word = build_segment(n);
if n % 10 == 3 && n != 3 && n % 100 != 13 {
word.pop();
word.push('e');
}
word
}
fn scale_word(scale_index: usize, plural: bool) -> String {
if scale_index < 2 {
return String::new();
}
let prefix_index = (scale_index - 2) / 2;
let is_iardo = (scale_index - 2) % 2 == 1;
let Some(prefix) = SCALE_PREFIXES.get(prefix_index) else {
return String::new();
};
let suffix = match (is_iardo, plural) {
(true, false) => "iliardo",
(true, true) => "iliardi",
(false, false) => "ilione",
(false, true) => "ilioni",
};
format!("{prefix}{suffix}")
}
fn integer_to_words(n: &BigUint) -> 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);
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
if remainder == 0 {
return build_thousands(thousands);
}
let tail = if remainder == 3 {
"tré".to_string()
} else {
build_segment(remainder)
};
return build_thousands(thousands) + &tail;
}
}
build_large_number_words(n)
}
fn build_large_number_words(n: &BigUint) -> String {
let groups = split_groups(n, 1000);
let mut parts: Vec<String> = Vec::new();
for (scale_index, &segment) in groups.iter().enumerate().rev() {
if segment == 0 {
continue;
}
if scale_index >= 2 {
parts.push(format!(
"{} {}",
build_segment_for_scale(segment),
scale_word(scale_index, segment != 1)
));
} else if scale_index == 1 {
parts.push(build_thousands(segment));
} else {
parts.push(build_segment(segment));
}
}
join_parts_with_connector(&parts)
}
fn join_parts_with_connector(parts: &[String]) -> String {
let Some((last, init)) = parts.split_last() else {
return String::new();
};
if init.is_empty() {
return last.clone();
}
let mut result = init.join(" ");
if last.contains(' ') {
result.push(' ');
} else {
result.push_str(" e ");
}
result.push_str(last);
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.as_deref() {
result.push(' ');
result.push_str(DECIMAL_SEP);
result.push(' ');
result.push_str(&decimal_part_to_words(fraction));
}
Ok(result)
}
fn cardinal_to_ordinal(cardinal: &str) -> String {
if let Some(stem) = cardinal.strip_suffix('é') {
return format!("{stem}e{ORDINAL_SUFFIX}");
}
if cardinal.ends_with("sei") {
return format!("{cardinal}{ORDINAL_SUFFIX}");
}
if let Some(stem) = cardinal.strip_suffix("mila") {
return format!("{stem}mill{ORDINAL_SUFFIX}");
}
if let Some(stem) = cardinal.strip_suffix("mille") {
return format!("{stem}mill{ORDINAL_SUFFIX}");
}
let mut chars = cardinal.chars();
if chars
.next_back()
.is_some_and(|last| "aeiouà èìòù".contains(last))
{
return format!("{}{ORDINAL_SUFFIX}", chars.as_str());
}
format!("{cardinal}{ORDINAL_SUFFIX}")
}
fn integer_to_ordinal(n: &BigUint) -> String {
if let Some(small) = n.to_u64().filter(|&small| small <= 10) {
return ORDINAL_ONES[small as usize].to_string();
}
if (n % 1_000_000u32).is_zero() {
return build_scale_ordinal(n);
}
cardinal_to_ordinal(&integer_to_words(n))
}
fn build_scale_ordinal(n: &BigUint) -> String {
let groups = split_groups(n, 1000);
let scale_index = groups
.iter()
.enumerate()
.skip(2)
.find(|&(_, &g)| g != 0)
.map_or(2, |(i, _)| i);
let segment = groups[scale_index];
let divisor = BigUint::from(1000u32).pow(scale_index as u32);
let higher = n - BigUint::from(segment) * divisor;
let multiplier = if segment == 1 {
String::new()
} else {
build_segment_unaccented(segment)
};
let mut scale = scale_word(scale_index, false);
scale.pop();
let ordinal = format!("{multiplier}{scale}{ORDINAL_SUFFIX}");
if higher.is_zero() {
ordinal
} else {
format!("{} {ordinal}", integer_to_words(&higher))
}
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
Ok(integer_to_ordinal(&n))
}
options! {
pub struct CurrencyOptions {
and: bool = true;
currency: Currency = Currency::EUR, 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 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("un");
} else {
result.push_str(&integer_to_words(&money.major));
}
if money.major >= BigUint::from(1_000_000u32) && (&money.major % 1_000_000u32).is_zero() {
result.push_str(" di ");
} else {
result.push(' ');
}
result.push_str(words.major_at(0));
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push_str(if options.and { " e " } else { " " });
}
if money.minor == 1 {
result.push_str("un");
} 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: "it-IT",
name: "Italian (Italy)",
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)),
};