use crate::base::{Lang, N2WError, Result};
use crate::currency::CurrencyValue;
use crate::floatpath::FloatValue;
use crate::strnum::ParsedNumber;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_integer::Integer;
use num_traits::{One, Signed, Zero};
use std::str::FromStr;
const TO_19: [&str; 20] = [
"không",
"một",
"hai",
"ba",
"bốn",
"năm",
"sáu",
"bảy",
"tám",
"chín",
"mười",
"mười một",
"mười hai",
"mười ba",
"mười bốn",
"mười lăm",
"mười sáu",
"mười bảy",
"mười tám",
"mười chín",
];
const TENS: [&str; 8] = [
"hai mươi",
"ba mươi",
"bốn mươi",
"năm mươi",
"sáu mươi",
"bảy mươi",
"tám mươi",
"chín mươi",
];
const DENOM: [&str; 21] = [
"",
"nghìn",
"triệu",
"tỷ",
"nghìn tỷ",
"trăm nghìn tỷ",
"Quintillion",
"Sextillion",
"Septillion",
"Octillion",
"Nonillion",
"Decillion",
"Undecillion",
"Duodecillion",
"Tredecillion",
"Quattuordecillion",
"Sexdecillion",
"Septendecillion",
"Octodecillion",
"Novemdecillion",
"Vigintillion",
];
fn type_error(msg: impl Into<String>) -> N2WError {
N2WError::Type(msg.into())
}
fn missing_attr(name: &str) -> N2WError {
N2WError::Attribute(format!("'Num2Word_VI' object has no attribute '{}'", name))
}
fn div_impossible() -> N2WError {
N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.DivisionImpossible'>]".into(),
}
}
fn py_float_int(n: &BigInt) -> Result<BigInt> {
if n.is_zero() {
return Ok(BigInt::zero());
}
let bits = n.bits();
if bits <= 53 {
return Ok(n.clone());
}
let shift = bits - 53;
let mut hi = n >> shift; let rem = n - (&hi << shift); let half = BigInt::one() << (shift - 1);
if rem > half || (rem == half && hi.is_odd()) {
hi += 1u32;
}
let res = hi << shift;
if res >= (BigInt::one() << 1024u32) {
return Err(N2WError::Overflow(
"int too large to convert to float".into(),
));
}
Ok(res)
}
pub struct LangVi;
impl Default for LangVi {
fn default() -> Self {
Self::new()
}
}
impl LangVi {
pub fn new() -> Self {
LangVi
}
fn convert_nn(&self, val: u32) -> String {
debug_assert!(val < 100, "_convert_nn is only ever called with val < 100");
if val < 20 {
return TO_19[val as usize].to_string();
}
let dcap = TENS[(val / 10 - 2) as usize];
let unit = val % 10;
if unit != 0 {
let a = match unit {
1 => "mốt",
5 => "lăm",
d => TO_19[d as usize],
};
return format!("{} {}", dcap, a);
}
dcap.to_string()
}
fn convert_nnn(&self, val: u32) -> String {
debug_assert!(
val < 1000,
"_convert_nnn is only ever called with val < 1000"
);
let mut word = String::new();
let (mod_, rem) = (val % 100, val / 100);
if rem > 0 {
word = format!("{} trăm", TO_19[rem as usize]);
if mod_ > 0 {
word.push(' ');
}
}
if mod_ > 0 && mod_ < 10 {
if mod_ == 5 {
if !word.is_empty() {
word.push_str("lẻ năm");
} else {
word.push_str("năm");
}
} else if !word.is_empty() {
word.push_str("lẻ ");
word.push_str(&self.convert_nn(mod_));
} else {
word.push_str(&self.convert_nn(mod_));
}
}
if mod_ >= 10 {
word.push_str(&self.convert_nn(mod_));
}
word
}
fn vietnam_number(&self, val: &BigInt) -> Option<String> {
let hundred = BigInt::from(100);
let thousand = BigInt::from(1000);
if *val < hundred {
return Some(self.convert_nn(to_u32(val)));
}
if *val < thousand {
return Some(self.convert_nnn(to_u32(val)));
}
for v in 0..DENOM.len() as u32 {
let dval = thousand.pow(v);
if dval > *val {
let didx = (v - 1) as usize;
let modulus = thousand.pow(v - 1);
let lval = val / &modulus;
let r = val - (&lval * &modulus);
let head = self.convert_nnn(to_u32(&lval));
let mut ret = format!("{} {}", head, DENOM[didx]);
if r <= BigInt::from(99) && r > BigInt::zero() {
ret = format!("{} {} lẻ", head, DENOM[didx]);
}
if r > BigInt::zero() {
ret.push(' ');
ret.push_str(&self.vietnam_number(&r)?);
}
return Some(ret);
}
}
None
}
fn number_to_text(&self, number: &BigInt) -> Result<String> {
let is_negative = number.is_negative();
let number = number.abs();
let rounded = py_float_int(&number)?;
match self.vietnam_number(&rounded) {
Some(start_word) => {
if is_negative {
Ok(format!("âm {}", start_word))
} else {
Ok(start_word)
}
}
None => {
if is_negative {
Err(type_error(
"can only concatenate str (not \"NoneType\") to str",
))
} else {
Err(N2WError::ReturnsNone)
}
}
}
}
fn has_fractional_cents(value: &BigDecimal) -> Result<bool> {
let m = (value * BigDecimal::from(100)).with_prec(28);
if m.abs() >= BigDecimal::from(BigInt::from(10).pow(28)) {
return Err(div_impossible());
}
Ok(&m - m.with_scale(0) != BigDecimal::zero())
}
fn number_to_text_decimal(&self, number: &BigDecimal) -> Result<String> {
let is_negative = number.is_negative();
let number = number.abs();
let as_f64: f64 = number
.to_string()
.parse()
.map_err(|_| type_error("float() argument must be a number"))?;
let formatted = format!("{:.2}", as_f64);
let (int_part, frac_part) = match formatted.split_once('.') {
Some((i, f)) => (i, Some(f)),
None => (formatted.as_str(), None),
};
let int_val = BigInt::from_str(int_part).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
int_part
))
})?;
let start_word = self
.vietnam_number(&int_val)
.ok_or_else(|| type_error("unsupported operand type(s) for +: 'NoneType' and 'str'"))?;
let mut final_result = start_word;
if let Some(frac_part) = frac_part {
let frac_val = u32::from_str(frac_part).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
frac_part
))
})?;
if frac_val > 0 {
final_result = format!("{} phẩy {}", final_result, self.convert_nn(frac_val));
}
}
if is_negative {
final_result = format!("âm {}", final_result);
}
Ok(final_result)
}
fn number_to_text_float(&self, is_negative: bool, magnitude: f64) -> Result<String> {
let formatted = format!("{:.2}", magnitude);
let (int_part, frac_part) = match formatted.split_once('.') {
Some((i, f)) => (i, Some(f)),
None => (formatted.as_str(), None),
};
let int_val = BigInt::from_str(int_part).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
int_part
))
})?;
match self.vietnam_number(&int_val) {
Some(start_word) => {
let mut final_result = start_word;
if let Some(frac_part) = frac_part {
let frac_val = u32::from_str(frac_part).map_err(|_| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
frac_part
))
})?;
if frac_val > 0 {
final_result =
format!("{} phẩy {}", final_result, self.convert_nn(frac_val));
}
}
if is_negative {
final_result = format!("âm {}", final_result);
}
Ok(final_result)
}
None => {
if is_negative {
Err(type_error(
"can only concatenate str (not \"NoneType\") to str",
))
} else {
Err(N2WError::ReturnsNone)
}
}
}
}
fn dong(&self, cardinal: Result<String>) -> Result<String> {
match cardinal {
Ok(s) => Ok(format!("{} đồng", s)),
Err(N2WError::ReturnsNone) => Err(type_error(
"unsupported operand type(s) for +: 'NoneType' and 'str'",
)),
Err(e) => Err(e),
}
}
}
fn to_u32(val: &BigInt) -> u32 {
use num_traits::ToPrimitive;
val.to_u32()
.expect("value is bounded below 1000 by construction")
}
impl Lang for LangVi {
fn default_currency(&self) -> &str {
"VND"
}
fn default_separator(&self) -> &str {
","
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
self.number_to_text(value)
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(format!("thứ {}", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
let concat = |r: Result<String>| -> Result<String> {
match r {
Err(N2WError::ReturnsNone) => Err(type_error(
"can only concatenate str (not \"NoneType\") to str",
)),
other => other,
}
};
if value.is_negative() {
Ok(format!(
"năm {} trước Công nguyên",
concat(self.to_cardinal(&(-value)))?
))
} else {
Ok(format!("năm {}", concat(self.to_cardinal(value))?))
}
}
fn ordinal_num_float_entry(&self, _value: &FloatValue, repr_str: &str) -> Result<String> {
Ok(format!("thứ {}", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
let concat = |r: Result<String>| -> Result<String> {
match r {
Err(N2WError::ReturnsNone) => Err(type_error(
"can only concatenate str (not \"NoneType\") to str",
)),
other => other,
}
};
let (lt_zero, magnitude) = match value {
FloatValue::Float { value, .. } => (*value < 0.0, value.abs()),
FloatValue::Decimal { value, .. } => {
let m: f64 = value.abs().to_string().parse().map_err(|_| {
N2WError::Value("could not convert Decimal to float".into())
})?;
(value.is_negative(), m)
}
};
let cardinal = concat(self.number_to_text_float(false, magnitude))?;
if lt_zero {
Ok(format!("năm {} trước Công nguyên", cardinal))
} else {
Ok(format!("năm {}", cardinal))
}
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
match value {
FloatValue::Float { value, .. } => self.number_to_text_float(*value < 0.0, value.abs()),
FloatValue::Decimal { value, .. } => {
let magnitude: f64 = value.abs().to_string().parse().map_err(|_| {
N2WError::Value("could not convert Decimal to float".into())
})?;
self.number_to_text_float(value.is_negative(), magnitude)
}
}
}
fn str_to_number(&self, _s: &str) -> Result<ParsedNumber> {
Err(missing_attr("str_to_number"))
}
fn to_fraction(&self, _numerator: &BigInt, _denominator: &BigInt) -> Result<String> {
Err(missing_attr("to_fraction"))
}
fn to_currency(
&self,
val: &CurrencyValue,
_currency: &str,
_cents: bool,
_separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
let decimal_val = match val {
CurrencyValue::Int(v) => BigDecimal::from(v.clone()),
CurrencyValue::Decimal { value, .. } => value.clone(),
};
let has_fractional_cents = Self::has_fractional_cents(&decimal_val)?;
if let CurrencyValue::Int(v) = val {
return self.dong(self.to_cardinal(v));
}
if has_fractional_cents {
return Err(missing_attr("to_cardinal_float"));
}
let value = match val {
CurrencyValue::Decimal { value, .. } => value,
CurrencyValue::Int(_) => unreachable!("int branch returned above"),
};
self.dong(self.number_to_text_decimal(value))
}
fn to_cheque(&self, _val: &BigDecimal, _currency: &str) -> Result<String> {
Err(missing_attr("to_cheque"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn repr_precision(f: f64) -> u32 {
let s = format!("{}", f);
match s.split_once('.') {
Some((_, frac)) if !frac.contains('e') => frac.len() as u32,
_ => 0,
}
}
fn float_case(v: f64) -> String {
LangVi::new()
.to_cardinal_float(
&FloatValue::Float {
value: v,
precision: repr_precision(v),
},
None,
)
.unwrap()
}
fn dec_case(s: &str) -> String {
let value = BigDecimal::from_str(s).unwrap();
let precision = s.split_once('.').map_or(0, |(_, f)| f.len() as u32);
LangVi::new()
.to_cardinal_float(&FloatValue::Decimal { value, precision }, None)
.unwrap()
}
#[test]
fn float_cardinals_match_python() {
for (v, want) in [
(0.5, "không phẩy năm mươi"),
(1.5, "một phẩy năm mươi"),
(2.25, "hai phẩy hai mươi lăm"),
(3.14, "ba phẩy mười bốn"),
(0.01, "không phẩy một"),
(0.1, "không phẩy mười"),
(0.99, "không phẩy chín mươi chín"),
(1.01, "một phẩy một"),
(12.34, "mười hai phẩy ba mươi bốn"),
(99.99, "chín mươi chín phẩy chín mươi chín"),
(100.5, "một trăm phẩy năm mươi"),
(1234.56, "một nghìn hai trăm ba mươi bốn phẩy năm mươi sáu"),
(-0.5, "âm không phẩy năm mươi"),
(-1.5, "âm một phẩy năm mươi"),
(-12.34, "âm mười hai phẩy ba mươi bốn"),
(1.005, "một"),
(2.675, "hai phẩy sáu mươi bảy"),
(0.001, "không"),
(-0.001, "âm không"),
] {
assert_eq!(float_case(v), want, "float {}", v);
}
}
#[test]
fn decimal_cardinals_match_python() {
for (s, want) in [
("0.01", "không phẩy một"),
("1.10", "một phẩy mười"),
("12.345", "mười hai phẩy ba mươi lăm"),
("-12.345", "âm mười hai phẩy ba mươi lăm"),
("0.001", "không"),
(
"98746251323029.99",
"chín mươi tám nghìn tỷ bảy trăm bốn mươi sáu tỷ hai trăm \
năm mươi mốt triệu ba trăm hai mươi ba nghìn lẻ hai mươi \
chín phẩy chín mươi tám",
),
] {
assert_eq!(dec_case(s), want, "decimal {}", s);
}
}
#[test]
fn precision_override_is_discarded() {
let got = LangVi::new()
.to_cardinal_float(
&FloatValue::Float {
value: 12.345,
precision: 3,
},
Some(1),
)
.unwrap();
assert_eq!(got, "mười hai phẩy ba mươi lăm");
}
#[test]
fn none_falloff_past_1e60() {
let lang = LangVi::new();
let big = "2".to_string() + &"0".repeat(60) + ".5";
let v = FloatValue::Decimal {
value: BigDecimal::from_str(&big).unwrap(),
precision: 1,
};
assert!(matches!(
lang.to_cardinal_float(&v, None),
Err(N2WError::ReturnsNone)
));
let neg = FloatValue::Decimal {
value: BigDecimal::from_str(&format!("-{}", big)).unwrap(),
precision: 1,
};
assert!(matches!(
lang.to_cardinal_float(&neg, None),
Err(N2WError::Type(_))
));
}
}