use crate::Result;
use crate::currency::vocab::NL 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::{ToPrimitive, Zero};
const ONES: [&str; 10] = [
"", "een", "twee", "drie", "vier", "vijf", "zes", "zeven", "acht", "negen",
];
const TEENS: [&str; 10] = [
"tien",
"elf",
"twaalf",
"dertien",
"veertien",
"vijftien",
"zestien",
"zeventien",
"achttien",
"negentien",
];
const TENS: [&str; 10] = [
"", "", "twintig", "dertig", "veertig", "vijftig", "zestig", "zeventig", "tachtig", "negentig",
];
const HUNDRED: &str = "honderd";
const SCALES: &[&str] = &[
"duizend",
"miljoen",
"miljard",
"biljoen",
"biljard",
"triljoen",
"triljard",
"quadriljoen",
"quadriljard",
];
const SCALE_ORDINAL: &[&str] = &[
"",
"duizendste",
"miljoenste",
"miljardste",
"biljoenste",
"biljardste",
"triljoenste",
];
pub const CARDINAL_MAX: Max = Max::western(SCALES.len() as u32);
pub const ORDINAL_MAX: Max = Max::western(SCALE_ORDINAL.len() as u32 - 1);
pub const CURRENCY_MAX: Max = Max::western(SCALES.len() as u32);
const ZERO: &str = "nul";
const NEGATIVE: &str = "min";
const DECIMAL_SEP: &str = "komma";
fn build_segment(n: u32, with_and: bool) -> 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 tens_ones = (n % 100) as usize;
let mut result = String::new();
if hundreds > 0 {
if hundreds > 1 {
result.push_str(ONES[hundreds]);
}
result.push_str(HUNDRED);
}
if tens_ones == 0 {
} else if tens_ones < 10 {
if hundreds > 0 && with_and {
result.push_str("en");
}
result.push_str(ONES[tens_ones]);
} else if tens_ones < 20 {
if hundreds > 0 && with_and && tens_ones < 13 {
result.push_str("en");
}
result.push_str(TEENS[ones]);
} else if ones == 0 {
result.push_str(TENS[tens]);
} else {
let ones_word = ONES[ones];
result.push_str(ones_word);
result.push_str(if ones_word.ends_with('e') {
"ën"
} else {
"en"
});
result.push_str(TENS[tens]);
}
result
}
#[derive(Clone, Copy)]
struct Opts {
accent_one: bool,
include_optional_and: bool,
no_hundred_pairing: bool,
}
fn apply_accent(word: String, accent_one: bool) -> String {
if !accent_one || !word.contains("een") {
return word;
}
let is_word = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
let bytes = word.as_bytes();
let mut out = String::with_capacity(word.len() + 4);
let mut last = 0;
let mut i = 0;
while i + 3 <= bytes.len() {
if &bytes[i..i + 3] == b"een"
&& (i == 0 || !is_word(bytes[i - 1]))
&& (i + 3 == bytes.len() || !is_word(bytes[i + 3]))
{
out.push_str(&word[last..i]);
out.push_str("één");
i += 3;
last = i;
} else {
i += 1;
}
}
out.push_str(&word[last..]);
out
}
fn push_after_scale(result: &mut String, word: &str, value: u32, include_optional_and: bool) {
if include_optional_and && value < 13 {
result.push_str(" en ");
} else {
result.push(' ');
}
result.push_str(word);
}
fn integer_to_words(n: &BigUint, options: Opts) -> String {
if n.is_zero() {
return ZERO.to_string();
}
let with_and = options.include_optional_and;
if let Some(small) = n.to_u64() {
if small < 1000 {
return apply_accent(build_segment(small as u32, with_and), options.accent_one);
}
if !options.no_hundred_pairing && (1100..10000).contains(&small) {
let high = (small / 100) as u32;
let low = (small % 100) as u32;
if high % 10 != 0 {
let mut result = build_segment(high, with_and);
result.push_str(HUNDRED);
if low > 0 {
push_after_scale(&mut result, &build_segment(low, with_and), low, with_and);
}
return apply_accent(result, options.accent_one);
}
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
let mut result = if thousands == 1 {
String::new()
} else {
build_segment(thousands, with_and)
};
result.push_str(SCALES[0]);
if remainder > 0 {
push_after_scale(
&mut result,
&build_segment(remainder, with_and),
remainder,
with_and,
);
}
return apply_accent(result, options.accent_one);
}
}
apply_accent(build_large_number_words(n, with_and), options.accent_one)
}
fn build_large_number_words(n: &BigUint, include_optional_and: bool) -> String {
let segments = split_groups(n, 1000);
let mut result = String::new();
let mut prev_was_scale = false;
for (i, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
if i == 0 {
let word = build_segment(segment, include_optional_and);
if result.is_empty() {
result = word;
} else {
push_after_scale(
&mut result,
&word,
segment,
prev_was_scale && include_optional_and,
);
}
prev_was_scale = false;
} else if i == 1 {
if !result.is_empty() {
result.push(' ');
}
if segment != 1 {
result.push_str(&build_segment(segment, include_optional_and));
}
result.push_str(SCALES[0]);
prev_was_scale = true;
} else {
if !result.is_empty() {
result.push(' ');
}
if segment == 1 {
result.push_str("een");
} else {
result.push_str(&build_segment(segment, include_optional_and));
}
result.push(' ');
result.push_str(SCALES[i - 1]);
prev_was_scale = true;
}
}
result
}
fn decimal_part_to_words(fraction: &str, options: Opts) -> 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),
Opts {
no_hundred_pairing: true,
..options
},
));
}
result
}
options! {
pub struct CardinalOptions {
accent_one: bool = true;
include_optional_and: bool = false;
no_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 opts = Opts {
accent_one: options.accent_one,
include_optional_and: options.include_optional_and,
no_hundred_pairing: options.no_hundred_pairing,
};
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&number.integer, opts));
if let Some(fraction) = &number.fraction {
result.push(' ');
result.push_str(DECIMAL_SEP);
result.push(' ');
result.push_str(&decimal_part_to_words(fraction, opts));
}
Ok(result)
}
const ORDINAL_ONES: [&str; 10] = [
"", "eerste", "tweede", "derde", "vierde", "vijfde", "zesde", "zevende", "achtste", "negende",
];
fn small_cardinal_to_ordinal(cardinal_word: String, n: u32) -> String {
if (1..=9).contains(&n) {
return ORDINAL_ONES[n as usize].to_string();
}
let mut result = cardinal_word;
if n < 20 {
result.push_str(if result.ends_with('d') { "e" } else { "de" });
} else {
result.push_str("ste");
}
result
}
fn build_ordinal_segment(n: u32) -> String {
if n == 0 {
return String::new();
}
let hundreds = (n / 100) as usize;
let tens_ones = n % 100;
if hundreds == 0 {
return small_cardinal_to_ordinal(build_segment(n, false), n);
}
let mut result = String::new();
if hundreds > 1 {
result.push_str(ONES[hundreds]);
}
result.push_str(HUNDRED);
if tens_ones == 0 {
result.push_str("ste");
return result;
}
result.push(' ');
result.push_str(&small_cardinal_to_ordinal(
build_segment(tens_ones, false),
tens_ones,
));
result
}
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 result = String::new();
for (i, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
if !result.is_empty() {
result.push(' ');
}
if i == lowest_non_zero {
if i == 0 {
result.push_str(&build_ordinal_segment(segment));
} else if segment == 1 {
if i > 1 {
result.push_str("een ");
}
result.push_str(SCALE_ORDINAL[i]);
} else {
result.push_str(&build_segment(segment, false));
if i > 1 {
result.push(' ');
}
result.push_str(SCALE_ORDINAL[i]);
}
} else if i == 1 {
if segment != 1 {
result.push_str(&build_segment(segment, false));
}
result.push_str(SCALES[0]);
} else {
if segment == 1 {
result.push_str("een");
} else {
result.push_str(&build_segment(segment, false));
}
result.push(' ');
result.push_str(SCALES[i - 1]);
}
}
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 < 10 {
return Ok(ORDINAL_ONES[small as usize].to_string());
}
if small < 100 {
let small = small as u32;
return Ok(small_cardinal_to_ordinal(
build_segment(small, false),
small,
));
}
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;
}
}
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(0)));
}
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
let opts = Opts {
accent_one: true,
include_optional_and: false,
no_hundred_pairing: true,
};
if has_major {
result.push_str(&integer_to_words(&money.major, opts));
result.push(' ');
result.push_str(words.major_at(0));
}
if has_minor {
if has_major {
result.push_str(if options.and { " en " } else { " " });
}
result.push_str(&integer_to_words(&BigUint::from(money.minor), opts));
result.push(' ');
result.push_str(words.minor_at(0));
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "nl-NL",
name: "Dutch (Netherlands)",
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)),
};