use crate::Result;
use crate::currency::vocab::VI 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] = [
"không", "một", "hai", "ba", "bốn", "năm", "sáu", "bảy", "tám", "chín",
];
const SCALES: &[&str] = &["", "nghìn", "triệu", "tỷ", "nghìn tỷ", "triệu tỷ", "tỷ tỷ"];
pub const CARDINAL_MAX: Max = Max::western(SCALES.len() as u32 - 1);
pub const ORDINAL_MAX: Max = Max::western(SCALES.len() as u32 - 1);
pub const CURRENCY_MAX: Max = Max::western(SCALES.len() as u32 - 1);
const HUNDRED: &str = "trăm";
const ZERO: &str = "không";
const NEGATIVE: &str = "âm";
const DECIMAL_SEP: &str = "phẩy";
const LE: &str = "lẻ";
const ORDINAL_PREFIX: &str = "thứ";
const ORDINAL_ONE: &str = "nhất";
const MOT_FINAL: &str = "mốt"; const LAM: &str = "lăm";
fn build_below_hundred(n: u32) -> String {
if n == 0 {
return ONES[0].to_string();
}
if n < 10 {
return ONES[n as usize].to_string();
}
if n < 20 {
let ones = (n - 10) as usize;
return match ones {
0 => "mười".to_string(),
5 => "mười lăm".to_string(),
_ => format!("mười {}", ONES[ones]),
};
}
let ones = (n % 10) as usize;
let tens = (n / 10) as usize;
let tens_word = format!("{} mươi", ONES[tens]);
match ones {
0 => tens_word,
1 => format!("{tens_word} {MOT_FINAL}"),
5 => format!("{tens_word} {LAM}"),
_ => format!("{tens_word} {}", ONES[ones]),
}
}
fn after_le(n: u32) -> &'static str {
if n == 5 { "năm" } else { ONES[n as usize] }
}
fn build_segment(n: u32) -> String {
if n == 0 {
return String::new();
}
let hundreds = (n / 100) as usize;
let remainder = n % 100;
let mut result = String::new();
if hundreds > 0 {
result = format!("{} {HUNDRED}", ONES[hundreds]);
}
if remainder > 0 {
if remainder < 10 {
if result.is_empty() {
result = ONES[remainder as usize].to_string();
} else {
result.push(' ');
result.push_str(LE);
result.push(' ');
result.push_str(after_le(remainder));
}
} else {
if !result.is_empty() {
result.push(' ');
}
result.push_str(&build_below_hundred(remainder));
}
}
result
}
fn build_le_segment(n: u32) -> String {
if n == 0 {
return String::new();
}
if n < 10 {
return format!("{LE} {}", after_le(n));
}
format!("{LE} {}", build_below_hundred(n))
}
fn integer_to_words(n: &BigUint) -> String {
if n.is_zero() {
return ZERO.to_string();
}
if let Some(small) = n.to_u64() {
if small < 100 {
return build_below_hundred(small as u32);
}
if small < 1000 {
return build_segment(small as u32);
}
if small < 1_000_000 {
let thousands = (small / 1000) as u32;
let remainder = (small % 1000) as u32;
let mut result = format!("{} {}", build_segment(thousands), SCALES[1]);
if remainder == 0 {
return result;
}
result.push(' ');
if remainder < 100 {
result.push_str(&build_le_segment(remainder));
} else {
result.push_str(&build_segment(remainder));
}
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<String> = Vec::new();
for (i, &segment) in segments.iter().enumerate().rev() {
if segment == 0 {
continue;
}
let words = build_segment(segment);
if i > 0 {
parts.push(format!("{words} {}", SCALES[i]));
} else {
parts.push(words);
}
}
if parts.is_empty() {
return ZERO.to_string();
}
if parts.len() == 1 {
return parts.remove(0);
}
let last_segment = segments[0];
if (1..=99).contains(&last_segment) {
parts.pop();
parts.push(build_le_segment(last_segment));
}
parts.join(" ")
}
fn decimal_part_to_words(decimal: &str) -> String {
let mut parts: Vec<String> = Vec::new();
let zeros = decimal.bytes().take_while(|&b| b == b'0').count();
for _ in 0..zeros {
parts.push(ZERO.to_string());
}
let remainder = &decimal[zeros..];
if !remainder.is_empty() {
parts.push(integer_to_words(&digits_to_biguint(remainder)));
}
parts.join(" ")
}
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 integer_to_ordinal(n: &BigUint) -> String {
if n.is_one() {
return format!("{ORDINAL_PREFIX} {ORDINAL_ONE}");
}
format!("{ORDINAL_PREFIX} {}", integer_to_words(n))
}
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 {
currency: Currency = Currency::VND, 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 mut result = String::new();
if money.negative {
result.push_str(NEGATIVE);
result.push(' ');
}
result.push_str(&integer_to_words(&money.major));
result.push(' ');
result.push_str(words.major_at(0));
Ok(result)
}
pub static LANGUAGE: Language = Language {
code: "vi-VN",
name: "Vietnamese (Vietnam)",
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)),
};