use crate::base::{Lang, N2WError, Result};
use crate::currency::{parse_currency_parts, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, python_decimal_str, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{Signed, ToPrimitive, Zero};
const ZERO: &str = "nol";
const DECIMAL_SEPARATOR: &str = "koma";
const MINUS_SIGN: &str = "min";
const BASE: [Option<&str>; 10] = [
None, Some("satu"),
Some("dua"),
Some("tiga"),
Some("empat"),
Some("lima"),
Some("enam"),
Some("tujuh"),
Some("delapan"),
Some("sembilan"),
];
fn tens_to(exp: usize) -> Option<&'static str> {
Some(match exp {
3 => "ribu",
6 => "juta",
9 => "miliar",
12 => "triliun",
15 => "kuadriliun",
18 => "kuantiliun",
21 => "sekstiliun",
24 => "septiliun",
27 => "oktiliun",
30 => "noniliun",
33 => "desiliun",
_ => return None,
})
}
fn maxval() -> BigInt {
BigInt::from(10u8).pow(36)
}
fn negord_type_error() -> N2WError {
N2WError::Type("not all arguments converted during string formatting".to_string())
}
fn base_of(c: char) -> Option<&'static str> {
BASE[c.to_digit(10).expect("digit from str(int)") as usize]
}
fn ratus(c: char) -> Vec<String> {
match c {
'1' => vec!["seratus".to_string()],
'0' => vec![],
_ => {
let mut v: Vec<String> = base_of(c).into_iter().map(str::to_string).collect();
v.push("ratus".to_string());
v
}
}
}
fn puluh(pair: &[char]) -> Vec<String> {
let (a, b) = (pair[0], pair[1]);
if a == '1' {
match b {
'0' => vec!["sepuluh".to_string()],
'1' => vec!["sebelas".to_string()],
_ => {
let mut v: Vec<String> = base_of(b).into_iter().map(str::to_string).collect();
v.push("belas".to_string());
v
}
}
} else if a == '0' {
base_of(b).into_iter().map(str::to_string).collect()
} else {
let mut v: Vec<String> = base_of(a).into_iter().map(str::to_string).collect();
v.push("puluh".to_string());
v.extend(base_of(b).into_iter().map(str::to_string));
v
}
}
fn spell_float(frac: &str) -> String {
let mut parts: Vec<String> = vec![String::new(), DECIMAL_SEPARATOR.to_string()];
for c in frac.chars() {
if c == '0' {
parts.push(ZERO.to_string());
} else {
parts.extend(base_of(c).map(str::to_string));
}
}
parts.join(" ")
}
fn split_by_3(number: &str) -> Vec<String> {
let chars: Vec<char> = number.chars().collect();
let length = chars.len();
let mut blocks: Vec<String> = Vec::new();
if length < 3 {
blocks.push(number.to_string());
} else {
let len_of_first_block = length % 3;
if len_of_first_block > 0 {
blocks.push(chars[0..len_of_first_block].iter().collect());
}
let mut i = len_of_first_block;
while i < length {
blocks.push(chars[i..(i + 3).min(length)].iter().collect());
i += 3;
}
}
blocks
}
fn spell(blocks: Vec<String>) -> Vec<(String, Vec<String>)> {
let mut word_blocks: Vec<(String, Vec<String>)> = Vec::new();
let first = &blocks[0];
let fchars: Vec<char> = first.chars().collect();
let spelling: Vec<String> = if fchars.len() == 1 {
if fchars[0] == '0' {
vec![ZERO.to_string()]
} else {
base_of(fchars[0]).into_iter().map(str::to_string).collect()
}
} else if fchars.len() == 2 {
puluh(&fchars)
} else {
let mut v = ratus(fchars[0]);
v.extend(puluh(&fchars[1..3]));
v
};
word_blocks.push((first.clone(), spelling));
for block in blocks.iter().skip(1) {
let bchars: Vec<char> = block.chars().collect();
let mut v = ratus(bchars[0]);
v.extend(puluh(&bchars[1..3]));
word_blocks.push((block.clone(), v));
}
word_blocks
}
fn join(word_blocks: &[(String, Vec<String>)]) -> Result<String> {
let mut word_list: Vec<String> = Vec::new();
let length = word_blocks.len() - 1;
let mut start = 0usize;
if length == 1 && word_blocks[0].0 == "1" {
word_list.push("seribu".to_string());
start = 1;
}
for i in start..=length {
word_list.extend(word_blocks[i].1.iter().cloned());
if word_blocks[i].1.is_empty() {
continue;
}
if i == length {
break;
}
let key = (length - i) * 3;
let scale = tens_to(key).ok_or_else(|| N2WError::Key(key.to_string()))?;
word_list.push(scale.to_string());
}
Ok(word_list.join(" "))
}
fn py_repr_float(f: f64) -> String {
if f == 0.0 || f.abs() >= 1e-4 {
return format!("{}", f);
}
let s = format!("{:e}", f);
match s.split_once('e') {
Some((mantissa, exp)) => {
let (sign, digits) = match exp.strip_prefix('-') {
Some(d) => ("-", d),
None => ("+", exp.strip_prefix('+').unwrap_or(exp)),
};
format!("{}e{}{:0>2}", mantissa, sign, digits)
}
None => s,
}
}
fn verify_ordinal(value: &BigInt) -> Result<()> {
if value.is_negative() {
return Err(negord_type_error());
}
Ok(())
}
fn verify_ordinal_float(value: &FloatValue) -> Result<()> {
if value.as_whole_int().is_none() {
return Err(negord_type_error());
}
let negative = match value {
FloatValue::Float { value: f, .. } => *f < 0.0,
FloatValue::Decimal { value: d, .. } => d.is_negative(),
};
if negative {
return Err(negord_type_error());
}
Ok(())
}
pub struct LangId;
impl Default for LangId {
fn default() -> Self {
Self::new()
}
}
impl LangId {
pub fn new() -> Self {
LangId
}
}
impl Lang for LangId {
fn python_maxval(&self) -> Option<num_bigint::BigInt> {
Some(num_bigint::BigInt::from(10u32).pow(36))
}
fn default_currency(&self) -> &str {
"IDR"
}
fn default_separator(&self) -> &str {
" "
}
fn negword(&self) -> &str {
MINUS_SIGN
}
fn pointword(&self) -> &str {
"koma"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let max = maxval();
if value >= &max {
return Err(N2WError::Overflow(format!(
"Number is too large to convert to words (abs({}) > {}).",
value, max
)));
}
let minus = if value.is_negative() {
format!("{} ", MINUS_SIGN)
} else {
String::new()
};
let digits = value.abs().to_string();
Ok(format!("{}{}", minus, join(&spell(split_by_3(&digits)))?))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
verify_ordinal(value)?;
let out_word = self.to_cardinal(value)?;
if out_word == "satu" {
return Ok("pertama".to_string());
}
Ok(format!("ke{}", out_word))
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
verify_ordinal(value)?;
Ok(format!("ke-{}", value))
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let max = maxval();
let over = match value {
FloatValue::Float { value: f, .. } => *f >= 1e36,
FloatValue::Decimal { value: d, .. } => *d >= BigDecimal::from(max.clone()),
};
if over {
let signed = match value {
FloatValue::Float { value: f, .. } => format!("{:?}", f),
FloatValue::Decimal { value: d, .. } => format!("{}", d),
};
return Err(N2WError::Overflow(format!(
"Number is too large to convert to words (abs({}) > {}).",
signed, max
)));
}
let minus = match value {
FloatValue::Float { value: f, .. } if *f < 0.0 => format!("{} ", MINUS_SIGN),
FloatValue::Decimal { value: d, .. } if d.is_negative() => {
format!("{} ", MINUS_SIGN)
}
_ => String::new(),
};
let s = match value {
FloatValue::Float { value: f, .. } => format!("{:?}", f.abs()),
FloatValue::Decimal { value: d, .. } => {
let disp = python_decimal_str(d);
disp.strip_prefix('-').unwrap_or(&disp).to_string()
}
};
if let Some(bad) = s.chars().find(|c| *c != '.' && !c.is_ascii_digit()) {
return Err(N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
bad
)));
}
let (int_part, float_word) = match s.split_once('.') {
Some((int_part, frac)) => (int_part, spell_float(frac)),
None => (s.as_str(), String::new()),
};
Ok(format!(
"{}{}{}",
minus,
join(&spell(split_by_3(int_part)))?,
float_word
))
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
verify_ordinal_float(value)?;
let out_word = self.to_cardinal_float(value, None)?;
if out_word == "satu" {
return Ok("pertama".to_string());
}
Ok(format!("ke{}", out_word))
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
verify_ordinal_float(value)?;
Ok(format!("ke-{}", repr_str))
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.to_cardinal_float(value, None)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s) {
Ok(ParsedNumber::NaN) => Err(N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.InvalidOperation'>]".into(),
}),
Ok(ParsedNumber::Inf { negative: true }) => Err(N2WError::Value(
"invalid literal for int() with base 10: 'I'".into(),
)),
other => other,
}
}
fn to_fraction(&self, _numerator: &BigInt, _denominator: &BigInt) -> Result<String> {
Err(N2WError::Attribute(
"'Num2Word_ID' object has no attribute 'to_fraction'".to_string(),
))
}
fn lang_name(&self) -> &str {
"Num2Word_ID"
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
_separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
let has_fractional_cents = match val {
CurrencyValue::Int(_) => false,
CurrencyValue::Decimal { value, .. } => {
let scaled = value * BigDecimal::from(100);
&scaled - scaled.with_scale(0) != BigDecimal::zero()
}
};
let is_integer_input = matches!(val, CurrencyValue::Int(_));
let (left, right, is_negative) =
parse_currency_parts(val, false, has_fractional_cents, 100);
let minus_str = if is_negative { "minus " } else { "" };
let money_str = self.to_cardinal(&left)?;
if is_integer_input || currency == "IDR" {
return Ok(format!("{}{} rupiah", minus_str, money_str));
}
let cents_str = if has_fractional_cents {
let f = right
.to_f64()
.ok_or_else(|| N2WError::Value(format!("cannot represent {} as f64", right)))?;
let repr = py_repr_float(f);
if cents {
if repr.contains('e') {
return Err(N2WError::Value(
"invalid literal for int() with base 10: 'e'".to_string(),
));
}
self.cardinal_from_decimal(&right)?
} else {
repr
}
} else {
let right_int = right.as_bigint_and_exponent().0;
if right_int.is_positive() {
self.to_cardinal(&right_int)?
} else {
String::new()
}
};
if !cents_str.is_empty() {
return Ok(format!(
"{}{} rupiah {} sen",
minus_str, money_str, cents_str
));
}
Ok(format!("{}{} rupiah", minus_str, money_str))
}
fn to_cheque(&self, _val: &BigDecimal, _currency: &str) -> Result<String> {
Err(N2WError::Attribute(
"'Num2Word_ID' object has no attribute 'to_cheque'".to_string(),
))
}
}