use crate::base::{
default_to_cardinal, set_low_numwords, set_mid_numwords, Cards, Kwargs, KwVal, Lang, N2WError,
Result,
};
use crate::currency::{parse_currency_parts, CurrencyForms, CurrencyValue};
use crate::floatpath::{default_to_cardinal_float, FloatValue};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{One, Signed, Zero};
use std::collections::HashMap;
const STUFF_ZERO: u8 = 2;
const ZERO_WORD: &str = "零";
const ONE_WORD: &str = "一";
const TEN_WORD: &str = "十";
const CURRENCY_FLOATS: [&str; 2] = ["毫", "仙"];
fn bare(s: &str) -> CurrencyForms {
CurrencyForms::new(&[s], &[])
}
fn build_currency_forms() -> HashMap<&'static str, CurrencyForms> {
let mut m: HashMap<&'static str, CurrencyForms> = HashMap::new();
m.insert("XXX", bare("元")); m.insert("CNY", bare("人民幣"));
m.insert("NTD", bare("新台幣"));
m.insert("HKD", bare("港幣"));
m.insert("MOP", bare("澳門幣"));
m.insert("SGD", bare("新加坡元"));
m.insert("MYR", bare("馬來西亞令吉"));
m.insert("USD", bare("美元"));
m.insert("EUR", bare("歐元"));
m.insert("GBP", bare("英鎊"));
m.insert("JPY", bare("日元"));
m.insert("CHF", bare("瑞士法郎"));
m.insert("CAD", bare("加元"));
m.insert("AUD", bare("澳幣"));
m.insert("NZD", bare("紐西蘭元"));
m.insert("THB", bare("泰銖"));
m.insert("KRW", bare("韓元"));
m.insert("EUR", bare("歐羅")); m.insert("JPY", bare("日圓")); m.insert("CAD", bare("加元")); m.insert("AUD", bare("澳元")); m.insert("KRW", bare("韓圜"));
m
}
pub struct LangZhHk {
cards: Cards,
maxval: BigInt,
currency_forms: HashMap<&'static str, CurrencyForms>,
}
impl Default for LangZhHk {
fn default() -> Self {
Self::new()
}
}
impl LangZhHk {
pub fn new() -> Self {
let mut high: Vec<&str> = vec![
"萬", "億", "兆", "京", "垓", "秭", "穣", "溝", "澗", "正", "載", "極", "恆河沙", "阿僧祇", "那由他", "不可思議", "無量", "不可說", ];
high.reverse();
let mut cards = Cards::new();
let max = 4 * high.len() as u32;
let mut n = max;
for word in high.iter() {
if n == 0 {
break;
}
cards.insert(BigInt::from(10u8).pow(n), *word);
n -= 4;
}
set_mid_numwords(&mut cards, &[(1000, "千"), (100, "百"), (10, "十")]);
set_low_numwords(
&mut cards,
&[
"九", "八", "七", "六", "五", "四", "三", "二", "一", ZERO_WORD,
],
);
let maxval = cards.highest().cloned().unwrap_or_else(BigInt::zero) * BigInt::from(1000);
LangZhHk {
cards,
maxval,
currency_forms: build_currency_forms(),
}
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
Ok(())
}
fn zh_to_cap_plain(&self, value: &str) -> String {
let one_ten = format!("{}{}", ONE_WORD, TEN_WORD);
if value.starts_with(&one_ten) {
value.chars().skip(1).collect()
} else {
value.to_string()
}
}
fn kw_cardinal(
&self,
value: &BigInt,
stuff_zero: i64,
reading: Reading,
prefer: Option<&[String]>,
) -> Result<String> {
let zero = select_alt(&ZERO_ALTS, prefer);
let mut cards = self.cards.clone();
if zero != ZERO_WORD {
cards.insert(BigInt::zero(), zero);
}
let eng = HkKwEngine {
base: self,
cards,
stuff_zero,
zero_word: zero,
};
let out = default_to_cardinal(&eng, value)?.replace(' ', "");
Ok(match reading {
Reading::Capital => apply_cap_map(&out, &HK_CAP_MAP),
_ => self.zh_to_cap_plain(&out),
})
}
fn currency_string(&self, code: &str) -> Result<&str> {
self.currency_forms
.get(code)
.map(|f| f.unit[0].as_str())
.ok_or_else(|| {
N2WError::NotImplemented(format!(
"Currency code \"{}\" not implemented for \"{}\"",
code,
self.lang_name()
))
})
}
fn card(&self, d: u32) -> Result<&str> {
self.cards
.get(&BigInt::from(d))
.ok_or_else(|| N2WError::Key(format!("{}", d)))
}
fn to_currency_float(&self, value: &BigInt) -> Result<Vec<&str>> {
let mut out: Vec<&str> = Vec::new();
if !value.is_positive() {
return Ok(out);
}
let cents = format!("{:0>2}", value.to_string());
let digits: Vec<char> = cents.chars().collect();
let d0 = py_int_digit(digits[0])?;
let d1 = py_int_digit(digits[1])?;
out.push(self.card(d0)?);
if d0 > 0 {
out.push(CURRENCY_FLOATS[0]);
}
if d1 > 0 {
out.push(self.card(d1)?);
out.push(CURRENCY_FLOATS[1]);
}
Ok(out)
}
}
fn py_int_digit(ch: char) -> Result<u32> {
ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch))
})
}
fn py_len_str(n: &BigInt) -> i64 {
n.to_string().len() as i64
}
fn zh_merge(
l: (&str, &BigInt),
r: (&str, &BigInt),
stuff_zero: i64,
zero_word: &str,
) -> (String, BigInt) {
let (ltext, lnum) = l;
let (rtext, rnum) = r;
if lnum.is_one() && rnum < &BigInt::from(10) {
return (rtext.to_string(), rnum.clone());
}
let with_zero = || (format!("{}{}{}", ltext, zero_word, rtext), lnum + rnum);
let no_zero = || (format!("{}{}", ltext, rtext), lnum + rnum);
let ldigits = py_len_str(lnum);
let rdigits = py_len_str(rnum);
if ldigits - rdigits > 1 {
match stuff_zero {
1 => return with_zero(),
2 => {
if rdigits % 4 != 0 {
return with_zero();
}
return no_zero();
}
3 => return no_zero(),
_ => {}
}
} else if rnum > lnum {
return (format!("{}{}", ltext, rtext), lnum * rnum);
}
no_zero()
}
#[derive(Clone, Copy, PartialEq)]
enum Reading {
Plain,
True,
Capital,
}
fn parse_reading(kw: &Kwargs) -> Reading {
match kw.get("reading") {
Some(KwVal::Bool(true)) => Reading::True,
Some(KwVal::Str(s)) if s == "capital" => Reading::Capital,
_ => Reading::Plain,
}
}
fn parse_prefer(kw: &Kwargs) -> Result<Option<Vec<String>>> {
match kw.get("prefer") {
None | Some(KwVal::None) => Ok(None),
Some(KwVal::List(l)) => Ok(Some(l.clone())),
Some(KwVal::Str(s)) => Ok(Some(s.chars().map(|c| c.to_string()).collect())),
Some(_) => Err(N2WError::Fallback("kwargs".into())),
}
}
fn parse_stuff_zero(kw: &Kwargs) -> i64 {
match kw.get("stuff_zero") {
None => 2,
Some(KwVal::Int(i)) => *i,
Some(KwVal::Bool(b)) => *b as i64,
Some(_) => 0,
}
}
fn parse_counter(kw: &Kwargs) -> Result<&str> {
match kw.get("counter") {
None => Ok(""),
Some(KwVal::Str(s)) => Ok(s),
Some(_) => Err(N2WError::Fallback("kwargs".into())),
}
}
fn select_alt<'a>(alts: &[&'a str], prefer: Option<&[String]>) -> &'a str {
if let Some(p) = prefer {
let hits: Vec<&str> = alts
.iter()
.copied()
.filter(|a| p.iter().any(|x| x == a))
.collect();
if hits.len() == 1 {
return hits[0];
}
}
alts[0]
}
fn apply_cap_map(value: &str, map: &[(&str, &str)]) -> String {
let mut out = value.to_string();
for (plain, cap) in map {
if out.contains(plain) {
out = out.replace(plain, cap);
}
}
out
}
const HK_CAP_MAP: [(&str, &str); 16] = [
("千", "仟"),
("百", "佰"),
("十", "拾"),
("九", "玖"),
("八", "捌"),
("七", "柒"),
("六", "陸"),
("五", "伍"),
("四", "肆"),
("三", "叁"),
("二", "貳"),
("一", "壹"),
("元", "圓"),
("正", "整"),
("毫", "角"),
("仙", "分"),
];
const ZERO_ALTS: [&str; 2] = ["零", "〇"];
const YEAR_PREFIX_ALTS: [&str; 2] = ["公元", "西元"];
const HK_DIGITS: [&str; 10] = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
struct HkKwEngine<'a> {
base: &'a LangZhHk,
cards: Cards,
stuff_zero: i64,
zero_word: &'static str,
}
impl Lang for HkKwEngine<'_> {
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
self.base.maxval()
}
fn negword(&self) -> &str {
self.base.negword()
}
fn pointword(&self) -> &str {
self.base.pointword()
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
zh_merge(l, r, self.stuff_zero, self.zero_word)
}
}
fn py_float_str(value: f64, precision: u32) -> String {
let a = value.abs();
let mag = if a != 0.0 && (a >= 1e16 || a < 1e-4) {
let s = format!("{:e}", a);
let (mant, exp) = s.split_once('e').expect("LowerExp always emits an e");
let exp: i32 = exp.parse().expect("exponent is a small int");
format!(
"{}e{}{:02}",
mant,
if exp < 0 { '-' } else { '+' },
exp.abs()
)
} else {
format!("{:.*}", precision as usize, a)
};
if value.is_sign_negative() {
format!("-{}", mag)
} else {
mag
}
}
fn py_value_str(value: &FloatValue) -> String {
match value {
FloatValue::Float { value, precision } => py_float_str(*value, *precision),
FloatValue::Decimal { value, .. } => crate::strnum::python_decimal_str(value),
}
}
impl Lang for LangZhHk {
fn default_currency(&self) -> &str {
"XXX"
}
fn default_separator(&self) -> &str {
""
}
fn cards(&self) -> &Cards {
&self.cards
}
fn maxval(&self) -> &BigInt {
&self.maxval
}
fn negword(&self) -> &str {
"負"
}
fn pointword(&self) -> &str {
"點"
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
zh_merge(l, r, STUFF_ZERO as i64, ZERO_WORD)
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let out = default_to_cardinal(self, value)?.replace(' ', "");
Ok(self.zh_to_cap_plain(&out))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
let base = self.to_cardinal(value)?;
Ok(format!("第{}", base))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("第{}", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
let mut out = String::new();
if value.is_negative() {
out.push_str("公元"); out.push_str("前"); }
for ch in value.abs().to_string().chars() {
let d = ch.to_digit(10).ok_or_else(|| {
N2WError::Value(format!("invalid literal for int() with base 10: '{}'", ch))
})?;
let word = self
.cards
.get(&BigInt::from(d))
.ok_or_else(|| N2WError::Key(format!("{}", d)))?;
out.push_str(word);
}
out.push_str("年"); Ok(out)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let out = default_to_cardinal_float(self, value, None)?.replace(' ', "");
Ok(self.zh_to_cap_plain(&out))
}
fn lang_name(&self) -> &str {
"Num2Word_ZH_HK"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.currency_forms.get(code)
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
_cents: bool,
_separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
let (left, right, is_negative) = parse_currency_parts(val, false, false, 100);
let right = right.as_bigint_and_exponent().0;
let cr = self.currency_string(currency)?;
let minus_str = if is_negative { self.negword() } else { "" };
let money_str = self.to_cardinal(&left)?;
let (mut cr_pre, cr_post) = if currency == "XXX" {
(String::new(), cr)
} else {
let xxx = self
.currency_forms
.get("XXX")
.expect("XXX is a class-body literal of Num2Word_ZH.CURRENCY_FORMS");
(cr.to_string(), xxx.unit[0].as_str())
};
let cents_str = self.to_currency_float(&right)?;
for c in [minus_str, money_str.as_str(), cr_post]
.into_iter()
.chain(cents_str)
{
cr_pre.push_str(&self.zh_to_cap_plain(c));
}
Ok(cr_pre)
}
fn to_cheque(&self, val: &BigDecimal, currency: &str) -> Result<String> {
let cr = self.currency_string(currency)?;
let chars: Vec<char> = cr.chars().collect();
if chars.len() != 2 {
return Err(N2WError::Value(if chars.len() < 2 {
format!(
"not enough values to unpack (expected 2, got {})",
chars.len()
)
} else {
"too many values to unpack (expected 2)".to_string()
}));
}
let unit = chars[0];
let divisor = self.currency_precision(currency);
let is_negative = val.is_negative();
let abs_val = val.abs();
let whole = abs_val.with_scale(0).as_bigint_and_exponent().0;
let fraction_str = if divisor > 1 {
let sub = (&abs_val - BigDecimal::from(whole.clone())) * BigDecimal::from(divisor);
let sub = sub.with_scale(0).as_bigint_and_exponent().0;
let digits = divisor.to_string().len() - 1;
format!("{:0>width$}/{}", sub.to_string(), divisor, width = digits)
} else {
String::new()
};
let words = self.money_verbose(&whole, currency)?;
let sign = if is_negative { "MINUS " } else { "" };
let body = if fraction_str.is_empty() {
format!("{} {}", words, unit)
} else {
format!("{} AND {} {}", words, fraction_str, unit)
};
Ok(format!("{}{}", sign, body).to_uppercase())
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
match value.as_whole_int() {
None => Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
py_value_str(value)
))),
Some(i) => {
if i.is_negative() {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
py_value_str(value)
)));
}
Ok(format!("第{}", self.to_cardinal(&i)?))
}
}
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("第{}", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
match value.as_whole_int() {
None => Err(N2WError::Type(format!(
"Cannot treat float {} as year.",
py_value_str(value)
))),
Some(i) => self.to_year(&i),
}
}
fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["stuff_zero", "reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let prefer = parse_prefer(kw)?;
self.kw_cardinal(
value,
parse_stuff_zero(kw),
parse_reading(kw),
prefer.as_deref(),
)
}
fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["counter", "reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let counter = parse_counter(kw)?;
let prefer = parse_prefer(kw)?;
let reading = parse_reading(kw);
self.verify_ordinal(value)?;
let base = self.kw_cardinal(value, 2, reading, prefer.as_deref())?;
Ok(format!("第{}{}", base, counter))
}
fn to_ordinal_num_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["counter", "reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let counter = parse_counter(kw)?;
Ok(format!("第{}{}", value, counter))
}
fn to_year_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let prefer = parse_prefer(kw)?;
let reading = parse_reading(kw);
let p = prefer.as_deref();
let mut out = String::new();
if value.is_negative() {
out.push_str(select_alt(&YEAR_PREFIX_ALTS, p));
out.push_str("前");
} else if reading == Reading::Capital {
out.push_str(select_alt(&YEAR_PREFIX_ALTS, p));
}
for ch in value.abs().to_string().chars() {
let d = ch.to_digit(10).expect("abs(BigInt) is all digits") as usize;
if d == 0 {
out.push_str(select_alt(&ZERO_ALTS, p));
} else {
out.push_str(HK_DIGITS[d]);
}
}
out.push_str("年");
Ok(out)
}
fn to_currency_kw(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
kw: &Kwargs,
) -> Result<String> {
if !kw.only(&["reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
if kw.is_empty() {
return self.to_currency(val, currency, cents, separator, adjective);
}
let prefer = parse_prefer(kw)?;
let reading = parse_reading(kw);
let p = prefer.as_deref();
let capital = reading == Reading::Capital;
let (left, right, is_negative) = parse_currency_parts(val, false, false, 100);
let right = right.as_bigint_and_exponent().0;
let cr = self.currency_string(currency)?;
let minus_str = if is_negative {
self.negword().to_string()
} else {
String::new()
};
let money_str = self.kw_cardinal(&left, 2, reading, p)?;
let (mut out, cr_post) = if currency == "XXX" {
(String::new(), cr.to_string())
} else {
let xxx = self
.currency_forms
.get("XXX")
.expect("XXX is a class-body literal of Num2Word_ZH.CURRENCY_FORMS");
(cr.to_string(), xxx.unit[0].clone())
};
let digit = |d: u32| -> String {
if d == 0 {
select_alt(&ZERO_ALTS, p).to_string()
} else {
HK_DIGITS[d as usize].to_string()
}
};
let cents_num: u32 = right.to_string().parse().unwrap_or(0);
let mut cents_parts: Vec<String> = Vec::new();
if cents_num > 0 {
let d0 = cents_num / 10;
let d1 = cents_num % 10;
if !(d0 == 0 && capital) {
cents_parts.push(digit(d0));
}
if d0 > 0 {
cents_parts.push(CURRENCY_FLOATS[0].to_string());
}
if d1 > 0 {
cents_parts.push(digit(d1));
cents_parts.push(CURRENCY_FLOATS[1].to_string());
}
}
let cheque = if cents_parts.is_empty() && capital {
"正".to_string()
} else {
String::new()
};
for c in std::iter::once(minus_str)
.chain(std::iter::once(money_str))
.chain(std::iter::once(cr_post))
.chain(cents_parts)
.chain(std::iter::once(cheque))
{
let piece = if capital {
apply_cap_map(&c, &HK_CAP_MAP)
} else {
self.zh_to_cap_plain(&c)
};
out.push_str(&piece);
}
Ok(out)
}
fn to_cardinal_float_kw(
&self,
value: &FloatValue,
precision_override: Option<u32>,
kw: &Kwargs,
) -> Result<String> {
if !kw.only(&["stuff_zero", "reading", "prefer"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
if kw.is_empty() {
return self.cardinal_float_entry(value, precision_override);
}
let prefer = parse_prefer(kw)?;
let reading = parse_reading(kw);
if let Some(i) = value.as_whole_int() {
return self.kw_cardinal(&i, parse_stuff_zero(kw), reading, prefer.as_deref());
}
let out = self.to_cardinal_float(value, None)?;
Ok(match reading {
Reading::Capital => apply_cap_map(&out, &HK_CAP_MAP),
_ => self.zh_to_cap_plain(&out),
})
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use std::str::FromStr;
fn f(value: f64, precision: u32) -> String {
LangZhHk::new()
.to_cardinal_float(&FloatValue::Float { value, precision }, None)
.unwrap()
}
fn d(s: &str, precision: u32) -> String {
LangZhHk::new()
.to_cardinal_float(
&FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
},
None,
)
.unwrap()
}
#[test]
fn corpus_cardinal_float() {
let rows: &[(f64, u32, &str)] = &[
(0.5, 1, "零點五"),
(1.5, 1, "一點五"),
(2.25, 2, "二點二五"),
(3.14, 2, "三點一四"),
(0.01, 2, "零點零一"),
(0.1, 1, "零點一"),
(0.99, 2, "零點九九"),
(1.01, 2, "一點零一"),
(12.34, 2, "十二點三四"),
(99.99, 2, "九十九點九九"),
(100.5, 1, "一百點五"),
(1234.56, 2, "一千二百三十四點五六"),
(-0.5, 1, "負零點五"),
(-1.5, 1, "負一點五"),
(-12.34, 2, "負一十二點三四"),
(1.005, 3, "一點零零五"),
(2.675, 3, "二點六七五"),
];
for (value, precision, want) in rows {
assert_eq!(&f(*value, *precision), want, "value {}", value);
}
}
#[test]
fn corpus_cardinal_dec() {
let rows: &[(&str, u32, &str)] = &[
("0.01", 2, "零點零一"),
("1.10", 2, "一點一零"),
("12.345", 3, "十二點三四五"),
(
"98746251323029.99",
2,
"九十八兆七千四百六十二億五千一百三十二萬三千零二十九點九九",
),
("0.001", 3, "零點零零一"),
];
for (value, precision, want) in rows {
assert_eq!(&d(value, precision.to_owned()), want, "value {}", value);
}
}
#[test]
fn precision_override_is_dropped() {
let out = LangZhHk::new()
.to_cardinal_float(
&FloatValue::Float {
value: 0.5,
precision: 1,
},
Some(5),
)
.unwrap();
assert_eq!(out, "零點五");
}
#[test]
fn leading_one_ten_strip_through_float_path() {
assert_eq!(f(11.5, 1), "十一點五");
assert_eq!(f(10.5, 1), "十點五");
}
}