use crate::currency::vocab::ZH_HANT as CURRENCY_VOCAB;
use crate::currency::{Currency, assert_minor_unit};
use crate::language::{Kind, Language, form};
use crate::max::Max;
use crate::number::{Number, parse_cardinal, parse_currency, parse_ordinal};
use crate::options::options;
use crate::{Error, Result};
use num_bigint::BigUint;
use num_traits::ToPrimitive;
struct Numerals {
ones: [&'static str; 10],
ten: &'static str,
hundred: &'static str,
thousand: &'static str,
}
const COMMON: Numerals = Numerals {
ones: ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"],
ten: "十",
hundred: "百",
thousand: "千",
};
const FORMAL: Numerals = Numerals {
ones: ["零", "壹", "貳", "參", "肆", "伍", "陸", "柒", "捌", "玖"],
ten: "拾",
hundred: "佰",
thousand: "仟",
};
const fn numerals(formal: bool) -> &'static Numerals {
if formal { &FORMAL } else { &COMMON }
}
const WAN_WORD: &str = "萬"; const YI_WORD: &str = "億";
pub const CARDINAL_MAX: Max = Max::bounded(16);
pub const ORDINAL_MAX: Max = Max::bounded(16);
pub const CURRENCY_MAX: Max = Max::bounded(16);
const ZERO: &str = "零";
const NEGATIVE: &str = "負";
const DECIMAL_SEP: &str = "點";
const ORDINAL_PREFIX: &str = "第";
const ZHENG_FORMAL: &str = "整";
fn below_ceiling(n: &BigUint, max: Max) -> Result<u64> {
n.to_u64().ok_or(Error::OutOfRange { max })
}
fn convert_below_wan(value: u64, v: &Numerals) -> String {
if value == 0 {
return String::new();
}
let mut parts = String::new();
let mut needs_zero = false;
let thousands_val = value / 1000;
let thousands_remainder = value % 1000;
if thousands_val > 0 {
parts.push_str(v.ones[thousands_val as usize]);
parts.push_str(v.thousand);
needs_zero = thousands_remainder > 0 && thousands_remainder < 100;
}
let hundreds_val = thousands_remainder / 100;
let hundreds_remainder = thousands_remainder % 100;
if hundreds_val > 0 {
if needs_zero {
parts.push_str(ZERO);
}
parts.push_str(v.ones[hundreds_val as usize]);
parts.push_str(v.hundred);
needs_zero = hundreds_remainder > 0 && hundreds_remainder < 10;
} else if thousands_val > 0 && hundreds_remainder > 0 {
needs_zero = true;
}
let tens_val = hundreds_remainder / 10;
let ones_val = hundreds_remainder % 10;
if tens_val > 0 {
if needs_zero {
parts.push_str(ZERO);
needs_zero = false;
}
parts.push_str(v.ones[tens_val as usize]);
parts.push_str(v.ten);
} else if (hundreds_val > 0 || thousands_val > 0) && ones_val > 0 {
needs_zero = true;
}
if ones_val > 0 {
if needs_zero {
parts.push_str(ZERO);
}
parts.push_str(v.ones[ones_val as usize]);
}
parts
}
fn convert_below_yi(value: u64, v: &Numerals) -> String {
if value == 0 {
return String::new();
}
let mut parts = String::new();
if value >= 10_000 {
let wan_value = value / 10_000;
let wan_remainder = value % 10_000;
parts.push_str(&convert_below_wan(wan_value, v));
parts.push_str(WAN_WORD);
if wan_remainder > 0 {
let wan_ends_with_zero = wan_value % 10 == 0;
let remainder_misses_thousands = wan_remainder < 1000;
if wan_ends_with_zero || remainder_misses_thousands {
parts.push_str(ZERO);
}
parts.push_str(&convert_below_wan(wan_remainder, v));
}
} else {
parts.push_str(&convert_below_wan(value, v));
}
parts
}
fn integer_to_words(n: u64, formal: bool) -> String {
if n == 0 {
return ZERO.to_string();
}
let v = numerals(formal);
let mut parts = String::new();
if n >= 100_000_000 {
let yi_value = n / 100_000_000;
let yi_remainder = n % 100_000_000;
parts.push_str(&convert_below_yi(yi_value, v));
parts.push_str(YI_WORD);
if yi_remainder > 0 {
if yi_remainder < 10_000_000 {
parts.push_str(ZERO);
}
parts.push_str(&convert_below_yi(yi_remainder, v));
}
} else {
parts.push_str(&convert_below_yi(n, v));
}
parts
}
fn decimal_digits_to_words(decimal: &str, formal: bool) -> String {
let v = numerals(formal);
let mut words = String::new();
for digit in decimal.bytes() {
words.push_str(v.ones[usize::from(digit - b'0')]);
}
words
}
options! {
pub struct CardinalOptions {
formal: bool = true;
}
}
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, None)?;
let integer = below_ceiling(&number.integer, CARDINAL_MAX)?;
let mut result = String::new();
if number.negative {
result.push_str(NEGATIVE);
}
result.push_str(&integer_to_words(integer, options.formal));
if let Some(fraction) = &number.fraction {
result.push_str(DECIMAL_SEP);
result.push_str(&decimal_digits_to_words(fraction, options.formal));
}
Ok(result)
}
fn integer_to_ordinal(n: u64, formal: bool) -> String {
format!("{ORDINAL_PREFIX}{}", integer_to_words(n, formal))
}
options! {
pub struct OrdinalOptions {
formal: bool = true;
}
}
pub fn to_ordinal<'a>(value: impl Into<Number<'a>>) -> Result<String> {
to_ordinal_with(value, &OrdinalOptions::default())
}
pub fn to_ordinal_with<'a>(
value: impl Into<Number<'a>>,
options: &OrdinalOptions,
) -> Result<String> {
let n = parse_ordinal(&value.into())?;
ORDINAL_MAX.check(&n, None)?;
let n = below_ceiling(&n, ORDINAL_MAX)?;
Ok(integer_to_ordinal(n, options.formal))
}
options! {
pub struct CurrencyOptions {
formal: bool = true;
currency: Currency = Currency::TWD, 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 yuan = below_ceiling(&money.major, CURRENCY_MAX)?;
let v = numerals(options.formal);
let yuan_word = words.major_at(if options.formal { 0 } else { 1 });
let jiao = (money.minor / 10) as usize;
let fen = (money.minor % 10) as usize;
let mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
}
if yuan > 0 || money.minor == 0 {
result.push_str(&integer_to_words(yuan, options.formal));
result.push_str(yuan_word);
}
if jiao > 0 {
result.push_str(v.ones[jiao]);
result.push_str(words.minor_at(0));
}
if fen > 0 {
if yuan > 0 && jiao == 0 {
result.push_str(ZERO);
}
result.push_str(v.ones[fen]);
result.push_str(words.minor_at(1));
} else {
result.push_str(ZHENG_FORMAL);
}
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "zh-Hant-TW",
name: "Chinese (Traditional, Taiwan)",
kind: Kind::Full,
cardinal: Some(form!(with_options to_cardinal_with, CardinalOptions, CARDINAL_MAX)),
ordinal: Some(form!(with_options to_ordinal_with, OrdinalOptions, ORDINAL_MAX)),
currency: Some(form!(with_options to_currency_with, CurrencyOptions, CURRENCY_MAX)),
};