use crate::Result;
use crate::currency::vocab::FR 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] = [
"", "un", "deux", "trois", "quatre", "cinq", "six", "sept", "huit", "neuf",
];
const TEENS: [&str; 10] = [
"dix", "onze", "douze", "treize", "quatorze", "quinze", "seize", "dix-sept", "dix-huit",
"dix-neuf",
];
const TENS: [&str; 7] = [
"",
"",
"vingt",
"trente",
"quarante",
"cinquante",
"soixante",
];
const SCALES: &[&str] = &["million", "billion", "trillion", "quadrillion"];
const SCALES_ARD: &[&str] = &["milliard", "billiard", "trilliard", "quadrilliard"];
pub const CARDINAL_MAX: Max = Max::long_scale(SCALES.len() as u32);
pub const ORDINAL_MAX: Max = Max::long_scale(SCALES.len() as u32);
pub const CURRENCY_MAX: Max = Max::long_scale(SCALES.len() as u32);
const THOUSAND: &str = "mille";
const HUNDRED: &str = "cent";
const ZERO: &str = "zéro";
const NEGATIVE: &str = "moins";
const DECIMAL_SEP: &str = "virgule";
const ORDINAL_SUFFIX: &str = "ième";
const PREMIER: &str = "premier";
struct Segment {
word: String,
ends_with_cents: bool,
ends_with_vingts: bool,
}
fn build_segment(n: u32) -> Segment {
if n == 0 {
return Segment {
word: String::new(),
ends_with_cents: false,
ends_with_vingts: false,
};
}
let tens_ones = n % 100;
let hundreds = (n / 100) as usize;
let mut parts: Vec<String> = Vec::new();
let mut ends_with_cents = false;
let mut ends_with_vingts = false;
if hundreds > 0 {
if hundreds == 1 {
parts.push(HUNDRED.to_string());
} else if tens_ones == 0 {
parts.push(format!("{} {HUNDRED}s", ONES[hundreds]));
ends_with_cents = true;
} else {
parts.push(format!("{} {HUNDRED}", ONES[hundreds]));
}
}
if tens_ones == 0 {
} else if tens_ones < 10 {
parts.push(ONES[tens_ones as usize].to_string());
} else if tens_ones < 20 {
parts.push(TEENS[(tens_ones - 10) as usize].to_string());
} else if tens_ones < 70 {
let t = (tens_ones / 10) as usize;
let o = (tens_ones % 10) as usize;
if o == 0 {
parts.push(TENS[t].to_string());
} else if o == 1 {
parts.push(format!("{} et {}", TENS[t], ONES[1]));
} else {
parts.push(format!("{}-{}", TENS[t], ONES[o]));
}
} else if tens_ones < 80 {
let remainder = tens_ones - 60;
if remainder == 11 {
parts.push("soixante et onze".to_string());
} else {
parts.push(format!("soixante-{}", TEENS[(remainder - 10) as usize]));
}
} else if tens_ones == 80 {
parts.push("quatre-vingts".to_string());
ends_with_vingts = true;
} else {
let remainder = tens_ones - 80;
if remainder < 10 {
parts.push(format!("quatre-vingt-{}", ONES[remainder as usize]));
} else {
parts.push(format!("quatre-vingt-{}", TEENS[(remainder - 10) as usize]));
}
}
Segment {
word: parts.join(" "),
ends_with_cents,
ends_with_vingts,
}
}
fn scale_word(scale_index: usize, segment: u32) -> String {
if scale_index == 1 {
return THOUSAND.to_string();
}
if scale_index % 2 == 0 {
let Some(base_word) = SCALES.get(scale_index / 2 - 1) else {
return String::new();
};
if segment > 1 {
format!("{base_word}s")
} else {
(*base_word).to_string()
}
} else {
let Some(ard_word) = SCALES_ARD.get((scale_index - 1) / 2 - 1) else {
return THOUSAND.to_string();
};
if segment > 1 {
format!("{ard_word}s")
} else {
(*ard_word).to_string()
}
}
}
fn strip_plural_before_mille(segment: Segment) -> String {
let mut word = segment.word;
if segment.ends_with_cents || segment.ends_with_vingts {
word.pop();
}
word
}
fn hyphenate(result: String, with_hyphen: bool) -> String {
if with_hyphen {
result.replace(' ', "-")
} else {
result
}
}
fn integer_to_words(n: &BigUint, with_hyphen: bool, hundred_pairing: bool) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1000 {
return hyphenate(build_segment(small as u32).word, with_hyphen);
}
if hundred_pairing && (1100..=1999).contains(&small) {
let num = small as u32;
let tens_ones = num % 100;
let mut result = format!("{} {HUNDRED}", TEENS[(num / 100 - 10) as usize]);
if tens_ones == 0 {
result.push('s');
} else {
result.push(' ');
result.push_str(&build_segment(tens_ones).word);
}
return hyphenate(result, with_hyphen);
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
let sep = if with_hyphen { '-' } else { ' ' };
let mut result = if thousands == 1 {
THOUSAND.to_string()
} else {
let mut words = strip_plural_before_mille(build_segment(thousands));
words.push(sep);
words.push_str(THOUSAND);
words
};
if remainder > 0 {
result.push(sep);
result.push_str(&build_segment(remainder).word);
}
return hyphenate(result, with_hyphen);
}
}
build_large_number_words(n, with_hyphen)
}
fn build_large_number_words(n: &BigUint, with_hyphen: bool) -> String {
let segments = split_groups(n, 1000);
let mut parts: Vec<String> = Vec::new();
for (scale_index, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
let built = build_segment(segment);
if scale_index == 0 {
parts.push(built.word);
} else if scale_index == 1 {
if segment == 1 {
parts.push(THOUSAND.to_string());
} else {
parts.push(strip_plural_before_mille(built));
parts.push(scale_word(scale_index, segment));
}
} else {
parts.push(built.word);
parts.push(scale_word(scale_index, segment));
}
}
let sep = if with_hyphen { "-" } else { " " };
hyphenate(parts.join(sep), with_hyphen)
}
fn decimal_part_to_words(decimal: &str, with_hyphen: bool) -> String {
let sep = if with_hyphen { "-" } else { " " };
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_str(sep);
}
result.push_str(ZERO);
}
let remainder = &decimal[zeros..];
if !remainder.is_empty() {
if !result.is_empty() {
result.push_str(sep);
}
result.push_str(&integer_to_words(
&digits_to_biguint(remainder),
with_hyphen,
false,
));
}
result
}
options! {
pub struct CardinalOptions {
with_hyphen_separator: bool = false;
hundred_pairing: bool = false;
}
}
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 with_hyphen = options.with_hyphen_separator;
let sep = if with_hyphen { "-" } else { " " };
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push_str(sep);
}
result.push_str(&integer_to_words(
&number.integer,
with_hyphen,
options.hundred_pairing,
));
if let Some(fraction) = &number.fraction {
result.push_str(sep);
result.push_str(DECIMAL_SEP);
result.push_str(sep);
result.push_str(&decimal_part_to_words(fraction, with_hyphen));
}
Ok(result)
}
fn cardinal_to_ordinal(cardinal: &str) -> String {
if cardinal.ends_with("cinq") {
return format!("{cardinal}u{ORDINAL_SUFFIX}");
}
if cardinal.ends_with("neuf") {
let stem = &cardinal[..cardinal.len() - 1];
return format!("{stem}v{ORDINAL_SUFFIX}");
}
const PLURALS: [&str; 10] = [
"cents",
"vingts",
"millions",
"milliards",
"billions",
"billiards",
"trillions",
"trilliards",
"quadrillions",
"quadrilliards",
];
if PLURALS.iter().any(|plural| cardinal.ends_with(plural)) {
let stem = &cardinal[..cardinal.len() - 1];
return format!("{stem}{ORDINAL_SUFFIX}");
}
if let Some(stem) = cardinal.strip_suffix('e') {
return format!("{stem}{ORDINAL_SUFFIX}");
}
format!("{cardinal}{ORDINAL_SUFFIX}")
}
fn integer_to_ordinal(n: &BigUint) -> String {
if n.is_one() {
return PREMIER.to_string();
}
let cardinal = integer_to_words(n, false, false);
if let Some(rest) = cardinal.strip_prefix("un ") {
if !rest.contains(' ') {
return cardinal_to_ordinal(rest);
}
}
cardinal_to_ordinal(&cardinal)
}
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 {
result.push_str(&integer_to_words(&money.major, false, false));
let unit = if money.major <= BigUint::from(1u32) {
words.major_at(0)
} else {
words.major_at(1)
};
if money.major >= BigUint::from(1_000_000u32) && (&money.major % 1_000_000u32).is_zero() {
if unit.starts_with(['a', 'e', 'i', 'o', 'u', 'é', 'è', 'ê']) {
result.push_str(" d'");
} else {
result.push_str(" de ");
}
} else {
result.push(' ');
}
result.push_str(unit);
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push_str(if options.and { " et " } else { " " });
}
result.push_str(&integer_to_words(&BigUint::from(money.minor), false, false));
result.push(' ');
result.push_str(words.minor_at(if money.minor == 1 { 0 } else { 1 }));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "fr-FR",
name: "French (France)",
kind: Kind::Full,
cardinal: Some(form!(with_options to_cardinal_with, CardinalOptions, CARDINAL_MAX)),
ordinal: Some(form!(plain to_ordinal, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};