use crate::Result;
use crate::currency::vocab::DE 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] = [
"", "eins", "zwei", "drei", "vier", "fünf", "sechs", "sieben", "acht", "neun",
];
const EIN: &str = "ein";
const TEENS: [&str; 10] = [
"zehn",
"elf",
"zwölf",
"dreizehn",
"vierzehn",
"fünfzehn",
"sechzehn",
"siebzehn",
"achtzehn",
"neunzehn",
];
const TENS: [&str; 10] = [
"", "", "zwanzig", "dreißig", "vierzig", "fünfzig", "sechzig", "siebzig", "achtzig", "neunzig",
];
const SCALES: &[&str] = &[
"tausend",
"Million",
"Milliarde",
"Billion",
"Billiarde",
"Trillion",
"Trilliarde",
"Quadrillion",
"Quadrilliarde",
];
const SCALES_PLURAL: &[&str] = &[
"tausend",
"Millionen",
"Milliarden",
"Billionen",
"Billiarden",
"Trillionen",
"Trilliarden",
"Quadrillionen",
"Quadrilliarden",
];
pub const CARDINAL_MAX: Max = Max::western(SCALES.len() as u32);
pub const ORDINAL_MAX: Max = Max::western(SCALES.len() as u32);
pub const CURRENCY_MAX: Max = Max::western(SCALES.len() as u32);
const HUNDRED: &str = "hundert";
const ZERO: &str = "null";
const NEGATIVE: &str = "minus";
const DECIMAL_SEP: &str = "komma";
const ORDINAL_ONES: [&str; 10] = [
"", "erste", "zweite", "dritte", "vierte", "fünfte", "sechste", "siebte", "achte", "neunte",
];
const ORDINAL_TEENS: [&str; 10] = [
"zehnte",
"elfte",
"zwölfte",
"dreizehnte",
"vierzehnte",
"fünfzehnte",
"sechzehnte",
"siebzehnte",
"achtzehnte",
"neunzehnte",
];
const ORDINAL_TENS: [&str; 10] = [
"",
"",
"zwanzigste",
"dreißigste",
"vierzigste",
"fünfzigste",
"sechzigste",
"siebzigste",
"achtzigste",
"neunzigste",
];
const ORDINAL_SUFFIX: &str = "ste";
const ORDINAL_SCALE_STEMS: &[&str] = &[
"tausend",
"million",
"milliard",
"billion",
"billiard",
"trillion",
"trilliard",
"quadrillion",
"quadrilliard",
];
fn push_hundreds(result: &mut String, hundreds: usize) {
if hundreds > 0 {
result.push_str(if hundreds == 1 { EIN } else { ONES[hundreds] });
result.push_str(HUNDRED);
}
}
fn push_tens_ones(result: &mut String, tens: usize, ones: usize, lone_one: &str) {
if tens == 1 {
result.push_str(TEENS[ones]);
} else if tens >= 2 && ones > 0 {
result.push_str(if ones == 1 { EIN } else { ONES[ones] });
result.push_str("und");
result.push_str(TENS[tens]);
} else if tens >= 2 {
result.push_str(TENS[tens]);
} else if ones == 1 {
result.push_str(lone_one);
} else if ones > 0 {
result.push_str(ONES[ones]);
}
}
fn build_segment(n: u32) -> String {
let mut result = String::new();
let n = n as usize;
push_hundreds(&mut result, n / 100);
push_tens_ones(&mut result, (n / 10) % 10, n % 10, ONES[1]);
result
}
fn build_segment_for_thousand(n: u32) -> String {
let mut result = String::new();
let n = n as usize;
push_hundreds(&mut result, n / 100);
push_tens_ones(&mut result, (n / 10) % 10, n % 10, EIN);
result
}
struct Part {
words: String,
is_scale: bool,
}
fn join_german_parts(parts: &[Part]) -> String {
if parts.is_empty() {
return ZERO.to_string();
}
let mut result = String::new();
for (i, part) in parts.iter().enumerate() {
if i > 0 && (part.is_scale || parts[i - 1].is_scale) {
result.push(' ');
}
result.push_str(&part.words);
}
result
}
fn push_large_scale(parts: &mut Vec<Part>, segment: u32, scale_index: usize) {
let (words, scale_word) = if segment == 1 {
("eine".to_string(), SCALES[scale_index - 1])
} else {
(build_segment(segment), SCALES_PLURAL[scale_index - 1])
};
parts.push(Part {
words,
is_scale: false,
});
parts.push(Part {
words: scale_word.to_string(),
is_scale: true,
});
}
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 mut result = build_segment_for_thousand((small / 1000) as u32);
result.push_str(SCALES[0]);
result.push_str(&build_segment((small % 1000) as u32));
return result;
}
}
build_large_number_words(n)
}
fn build_large_number_words(n: &BigUint) -> String {
let segments = split_groups(n, 1000);
let mut parts: Vec<Part> = Vec::new();
for (scale_index, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
match scale_index {
0 => parts.push(Part {
words: build_segment(segment),
is_scale: false,
}),
1 => parts.push(Part {
words: build_segment_for_thousand(segment) + SCALES[0],
is_scale: false,
}),
_ => push_large_scale(&mut parts, segment, scale_index),
}
}
join_german_parts(&parts)
}
fn decimal_part_to_words(fraction: &str) -> String {
let mut result = String::new();
let digits = fraction.trim_start_matches('0');
for _ in 0..fraction.len() - digits.len() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(ZERO);
}
if !digits.is_empty() {
if !result.is_empty() {
result.push(' ');
}
result.push_str(&integer_to_words(&digits_to_biguint(digits)));
}
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 {
let mut result = String::new();
let n = n as usize;
let ones = n % 10;
let tens = (n / 10) % 10;
push_hundreds(&mut result, n / 100);
if tens == 1 {
result.push_str(ORDINAL_TEENS[ones]);
} else if tens >= 2 && ones > 0 {
result.push_str(if ones == 1 { EIN } else { ONES[ones] });
result.push_str("und");
result.push_str(ORDINAL_TENS[tens]);
} else if tens >= 2 {
result.push_str(ORDINAL_TENS[tens]);
} else if ones > 0 {
result.push_str(ORDINAL_ONES[ones]);
} else if n > 0 {
result.push_str(ORDINAL_SUFFIX);
}
result
}
fn integer_to_ordinal(n: &BigUint) -> String {
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_ordinal_segment(small as u32);
}
if small < 1_000_000 {
let remainder = (small % 1000) as u32;
let mut result = build_segment_for_thousand((small / 1000) as u32);
result.push_str(SCALES[0]);
if remainder == 0 {
result.push_str(ORDINAL_SUFFIX);
} else {
result.push_str(&build_ordinal_segment(remainder));
}
return result;
}
}
build_large_ordinal(n)
}
fn build_large_ordinal(n: &BigUint) -> String {
let segments = split_groups(n, 1000);
let lowest_non_zero = segments.iter().position(|&s| s != 0).unwrap_or(0);
let mut parts: Vec<Part> = Vec::new();
for (scale_index, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
let is_lowest = scale_index == lowest_non_zero;
if scale_index == 0 {
parts.push(Part {
words: build_ordinal_segment(segment),
is_scale: false,
});
} else if scale_index == 1 {
let mut words = build_segment_for_thousand(segment) + SCALES[0];
if is_lowest {
words.push_str(ORDINAL_SUFFIX);
}
parts.push(Part {
words,
is_scale: false,
});
} else if is_lowest {
let mut words = if segment == 1 && parts.is_empty() {
String::new()
} else {
build_segment_for_thousand(segment)
};
words.push_str(ORDINAL_SCALE_STEMS[scale_index - 1]);
words.push_str(ORDINAL_SUFFIX);
parts.push(Part {
words,
is_scale: false,
});
} else {
push_large_scale(&mut parts, segment, scale_index);
}
}
join_german_parts(&parts)
}
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;
}
}
fn count_before_noun(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 1000 {
return build_segment_for_thousand(small as u32);
}
if small < 1_000_000 {
let mut result = build_segment_for_thousand((small / 1000) as u32);
result.push_str(SCALES[0]);
result.push_str(&build_segment_for_thousand((small % 1000) as u32));
return result;
}
}
let mut words = integer_to_words(n);
if (n % 100u32).is_one() {
words.pop();
}
words
}
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(&count_before_noun(&money.major));
result.push(' ');
result.push_str(words.major_at(0));
}
if money.minor > 0 {
if !money.major.is_zero() {
result.push_str(if options.and { " und " } else { " " });
}
result.push_str(&count_before_noun(&BigUint::from(money.minor)));
result.push(' ');
result.push_str(words.minor_at(0));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "de-DE",
name: "German (Germany)",
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)),
};