use crate::base::{KwVal, Kwargs, Lang, N2WError, Result};
use crate::currency::CurrencyValue;
use crate::floatpath::FloatValue;
use crate::strnum::{python_decimal_parse, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::{FromPrimitive, Signed, ToPrimitive, Zero};
const ARABIC_ONES: [&str; 20] = [
"",
"واحد",
"اثنان",
"ثلاثة",
"أربعة",
"خمسة",
"ستة",
"سبعة",
"ثمانية",
"تسعة",
"عشرة",
"أحد عشر",
"اثنا عشر",
"ثلاثة عشر",
"أربعة عشر",
"خمسة عشر",
"ستة عشر",
"سبعة عشر",
"ثمانية عشر",
"تسعة عشر",
];
const ARABIC_FEMININE_ONES: [&str; 20] = [
"",
"إحدى",
"اثنتان",
"ثلاث",
"أربع",
"خمس",
"ست",
"سبع",
"ثمان",
"تسع",
"عشر",
"إحدى عشرة",
"اثنتا عشرة",
"ثلاث عشرة",
"أربع عشرة",
"خمس عشرة",
"ست عشرة",
"سبع عشرة",
"ثماني عشرة",
"تسع عشرة",
];
const ARABIC_TENS: [&str; 8] = [
"عشرون",
"ثلاثون",
"أربعون",
"خمسون",
"ستون",
"سبعون",
"ثمانون",
"تسعون",
];
const ARABIC_HUNDREDS: [&str; 10] = [
"",
"مائة",
"مئتان",
"ثلاثمائة",
"أربعمائة",
"خمسمائة",
"ستمائة",
"سبعمائة",
"ثمانمائة",
"تسعمائة",
];
const ARABIC_APPENDED_TWOS: [&str; 17] = [
"مئتا",
"ألفا",
"مليونا",
"مليارا",
"تريليونا",
"كوادريليونا",
"كوينتليونا",
"سكستيليونا",
"سبتيليونا",
"أوكتيليونا ",
"نونيليونا",
"ديسيليونا",
"أندسيليونا",
"دوديسيليونا",
"تريديسيليونا",
"كوادريسيليونا",
"كوينتينيليونا",
];
const ARABIC_TWOS: [&str; 17] = [
"مئتان",
"ألفان",
"مليونان",
"ملياران",
"تريليونان",
"كوادريليونان",
"كوينتليونان",
"سكستيليونان",
"سبتيليونان",
"أوكتيليونان ",
"نونيليونان ",
"ديسيليونان",
"أندسيليونان",
"دوديسيليونان",
"تريديسيليونان",
"كوادريسيليونان",
"كوينتينيليونان",
];
const ARABIC_GROUP: [&str; 17] = [
"مائة",
"ألف",
"مليون",
"مليار",
"تريليون",
"كوادريليون",
"كوينتليون",
"سكستيليون",
"سبتيليون",
"أوكتيليون",
"نونيليون",
"ديسيليون",
"أندسيليون",
"دوديسيليون",
"تريديسيليون",
"كوادريسيليون",
"كوينتينيليون",
];
const ARABIC_APPENDED_GROUP: [&str; 17] = [
"",
"ألفاً",
"مليوناً",
"ملياراً",
"تريليوناً",
"كوادريليوناً",
"كوينتليوناً",
"سكستيليوناً",
"سبتيليوناً",
"أوكتيليوناً",
"نونيليوناً",
"ديسيليوناً",
"أندسيليوناً",
"دوديسيليوناً",
"تريديسيليوناً",
"كوادريسيليوناً",
"كوينتينيليوناً",
];
const ARABIC_PLURAL_GROUPS: [&str; 17] = [
"",
"آلاف",
"ملايين",
"مليارات",
"تريليونات",
"كوادريليونات",
"كوينتليونات",
"سكستيليونات",
"سبتيليونات",
"أوكتيليونات",
"نونيليونات",
"ديسيليونات",
"أندسيليونات",
"دوديسيليونات",
"تريديسيليونات",
"كوادريسيليونات",
"كوينتينيليونات",
];
const AR_ORDINALS_DEF: [(&str, &str); 20] = [
("", ""), ("الأول", "الأولى"),
("الثاني", "الثانية"),
("الثالث", "الثالثة"),
("الرابع", "الرابعة"),
("الخامس", "الخامسة"),
("السادس", "السادسة"),
("السابع", "السابعة"),
("الثامن", "الثامنة"),
("التاسع", "التاسعة"),
("العاشر", "العاشرة"),
("الحادي عشر", "الحادية عشرة"),
("الثاني عشر", "الثانية عشرة"),
("الثالث عشر", "الثالثة عشرة"),
("الرابع عشر", "الرابعة عشرة"),
("الخامس عشر", "الخامسة عشرة"),
("السادس عشر", "السادسة عشرة"),
("السابع عشر", "السابعة عشرة"),
("الثامن عشر", "الثامنة عشرة"),
("التاسع عشر", "التاسعة عشرة"),
];
const AR_TENS_DEF: [&str; 8] = [
"العشرون",
"الثلاثون",
"الأربعون",
"الخمسون",
"الستون",
"السبعون",
"الثمانون",
"التسعون",
];
const AR_HUNDREDS_DEF: [&str; 9] = [
"المائة",
"المئتان",
"الثلاثمائة",
"الأربعمائة",
"الخمسمائة",
"الستمائة",
"السبعمائة",
"الثمانمائة",
"التسعمائة",
];
const AR_AFTER_HUNDRED: [&str; 9] = [
"بعد المائة",
"بعد المئتين",
"بعد الثلاثمائة",
"بعد الأربعمائة",
"بعد الخمسمائة",
"بعد الستمائة",
"بعد السبعمائة",
"بعد الثمانمائة",
"بعد التسعمائة",
];
const AR_STANDALONE_DUAL: [(&str, &str, &str); 6] = [
("مئتا", "مئتان", "مئتين"),
("ألفا", "ألفان", "ألفين"),
("مليونا", "مليونان", "مليونين"),
("مليارا", "ملياران", "مليارين"),
("تريليونا", "تريليونان", "تريليونين"),
("كوادريليونا", "كوادريليونان", "كوادريليونين"),
];
const AR_DUAL_NOMINATIVE_TO_OBLIQUE: [(&str, &str); 6] = [
("كوادريليونان", "كوادريليونين"),
("تريليونان", "تريليونين"),
("مليونان", "مليونين"),
("ملياران", "مليارين"),
("ألفان", "ألفين"),
("مئتان", "مئتين"),
];
const ZERO_WORD: &str = "صفر";
const MINUS: &str = "سالب ";
const DEFAULT_PREC: usize = 28;
fn maxval() -> BigInt {
BigInt::from(10u8).pow(51)
}
fn assertion_error(what: &str) -> N2WError {
N2WError::Assertion(what.to_string())
}
fn ndigits(x: &BigInt) -> usize {
if x.is_zero() {
1
} else {
x.to_string().len()
}
}
fn is_two_times_power_of_ten(x: &BigInt) -> bool {
let s = x.to_string();
let mut it = s.chars();
it.next() == Some('2') && it.all(|c| c == '0')
}
fn round_half_even_sig(x: &BigInt, prec: usize) -> BigInt {
let d = ndigits(x);
if d <= prec {
return x.clone();
}
let k = d - prec;
let p = BigInt::from(10u8).pow(k as u32);
let q = x / &p;
let r = x - &q * &p;
let half = &p / 2u8;
let bump = r > half || (r == half && (&q % 2u8) != BigInt::from(0u8));
if bump {
(q + 1u8) * p
} else {
q * p
}
}
fn py_lstrip_charset(s: &str, chars: &str) -> String {
if chars.is_empty() {
return s.to_string();
}
let set: Vec<char> = chars.chars().collect();
let mut it = s.char_indices();
let mut cut = s.len();
loop {
match it.next() {
Some((i, c)) => {
if !set.contains(&c) {
cut = i;
break;
}
}
None => break,
}
}
s[cut..].to_string()
}
fn digit_feminine_status(digit: usize, group_level: i32, fem_name: bool) -> &'static str {
if group_level == -1 {
ARABIC_FEMININE_ONES[digit]
} else if group_level == 0 {
if fem_name {
ARABIC_FEMININE_ONES[digit]
} else {
ARABIC_ONES[digit]
}
} else {
ARABIC_ONES[digit]
}
}
fn process_arabic_group(
group_number: u32,
group_level: i32,
integer_value: &BigInt,
fem_name: bool,
) -> Result<String> {
let tens = group_number % 100;
let hundreds_int = (group_number / 100) as usize;
let mut ret_val = String::new();
if hundreds_int > 0 {
if tens == 0 && hundreds_int == 2 {
ret_val = ARABIC_APPENDED_TWOS[0].to_string();
} else {
ret_val = ARABIC_HUNDREDS[hundreds_int].to_string();
if !ret_val.is_empty() && tens != 0 {
ret_val.push_str(" و");
}
}
}
if tens > 0 {
if tens < 20 {
if group_level >= ARABIC_TWOS.len() as i32 {
return Err(assertion_error("int(group_level) < len(self.arabicTwos)"));
}
if tens == 2 && hundreds_int == 0 && group_level > 0 {
if is_two_times_power_of_ten(integer_value)
&& *integer_value > BigInt::from(10u8)
&& (ndigits(integer_value) - 1) % 3 == 0
{
ret_val = ARABIC_APPENDED_TWOS[group_level as usize].to_string();
} else {
ret_val = ARABIC_TWOS[group_level as usize].to_string();
}
} else {
if tens == 1 && group_level > 0 {
ret_val.push_str(ARABIC_GROUP[group_level as usize]);
} else {
ret_val.push_str(digit_feminine_status(tens as usize, group_level, fem_name));
}
}
} else {
let ones = tens % 10;
let t = (tens / 10 - 2) as usize;
if ones > 0 {
ret_val.push_str(digit_feminine_status(ones as usize, group_level, fem_name));
}
if !ret_val.is_empty() && ones != 0 {
ret_val.push_str(" و");
}
ret_val.push_str(ARABIC_TENS[t]);
}
}
Ok(ret_val)
}
fn convert(n: &BigInt, fem_name: bool) -> Result<String> {
debug_assert!(!n.is_negative());
let integer_value = n.clone();
if n.is_zero() {
return Ok(ZERO_WORD.to_string());
}
let thousand = BigInt::from(1000u16);
let mut ret_val = String::new();
let mut group: i32 = 0;
let mut prec = DEFAULT_PREC;
let mut temp = n.clone();
while temp > BigInt::zero() {
let q_int = &temp / &thousand;
if ndigits(&q_int) > prec {
prec = ndigits(&temp);
}
let number_to_process = (&temp % &thousand).to_u32().expect("0..=999");
temp = round_half_even_sig(&temp, prec) / &thousand;
let group_description =
process_arabic_group(number_to_process, group, &integer_value, fem_name)?;
if !group_description.is_empty() {
if group > 0 {
if !ret_val.is_empty() {
ret_val = format!("و{}", ret_val);
}
if number_to_process != 2 && number_to_process != 1 {
if group >= ARABIC_GROUP.len() as i32 {
return Err(assertion_error("group < len(self.arabicGroup)"));
}
if number_to_process % 100 != 1 {
if (3..=10).contains(&number_to_process) {
ret_val =
format!("{} {}", ARABIC_PLURAL_GROUPS[group as usize], ret_val);
} else if !ret_val.is_empty() {
ret_val =
format!("{} {}", ARABIC_APPENDED_GROUP[group as usize], ret_val);
} else {
ret_val = format!("{} {}", ARABIC_GROUP[group as usize], ret_val);
}
} else {
ret_val = format!("{} {}", ARABIC_GROUP[group as usize], ret_val);
}
}
}
ret_val = format!("{} {}", group_description, ret_val);
}
group += 1;
}
Ok(ret_val)
}
fn validate_number(number: &BigInt) -> Result<()> {
let mx = maxval();
if *number >= mx {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
number, mx
)));
}
Ok(())
}
fn cardinal_int_case(value: &BigInt, is_oblique: bool) -> Result<String> {
validate_number(value)?;
let minus = if value.is_negative() { MINUS } else { "" };
let abs = if value.is_negative() {
-value
} else {
value.clone()
};
let out = format!("{}{}", minus, convert(&abs, false)?.trim());
Ok(finish_cardinal_case(out, minus, is_oblique))
}
fn to_ordinal_impl(number: &BigInt, feminine: bool, prefix: &str) -> Result<String> {
let pick = |pair: &(&'static str, &'static str)| if feminine { pair.1 } else { pair.0 };
let one = BigInt::from(1u8);
let nineteen = BigInt::from(19u8);
let twenty = BigInt::from(20u8);
let ninety_nine = BigInt::from(99u8);
let hundred = BigInt::from(100u8);
let nine_ninety_nine = BigInt::from(999u16);
if *number >= one && *number <= nineteen {
let i = number.to_usize().expect("1..=19");
return Ok(pick(&AR_ORDINALS_DEF[i]).to_string());
}
if *number >= twenty && *number <= ninety_nine {
let n = number.to_u32().expect("20..=99");
let tens = (n / 10) * 10;
let ones = n % 10;
if ones == 0 {
return Ok(AR_TENS_DEF[(tens / 10 - 2) as usize].to_string());
}
let ones_form = pick(&AR_ORDINALS_DEF[ones as usize]);
return Ok(format!(
"{} و{}",
ones_form,
AR_TENS_DEF[(tens / 10 - 2) as usize]
));
}
if *number >= hundred && *number <= nine_ninety_nine {
let n = number.to_u32().expect("100..=999");
let hundreds = (n / 100) * 100;
let remainder = n % 100;
if remainder == 0 {
return Ok(AR_HUNDREDS_DEF[(hundreds / 100 - 1) as usize].to_string());
}
let inner = to_ordinal_impl(&BigInt::from(remainder), feminine, "")?;
return Ok(format!(
"{} {}",
inner,
AR_AFTER_HUNDRED[(hundreds / 100 - 1) as usize]
));
}
let fem_name = *number < hundred; let abs = if number.is_negative() {
-number
} else {
number.clone()
};
let conv = convert(&abs, fem_name)?;
if !prefix.is_empty() && !abs.is_zero() {
return Ok(format!("{} {}", prefix, conv).trim().to_string());
}
Ok(conv.trim().to_string())
}
fn float_int_trunc(value: &FloatValue) -> Result<BigInt> {
match value {
FloatValue::Float { value: x, .. } => {
if x.is_nan() {
return Err(N2WError::Value(
"cannot convert float NaN to integer".into(),
));
}
if x.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
Ok(BigInt::from_f64(x.trunc()).expect("finite f64"))
}
FloatValue::Decimal { value: d, .. } => Ok(d.with_scale(0).as_bigint_and_exponent().0),
}
}
fn is_oblique_case(case: Option<&KwVal>) -> bool {
match case {
Some(KwVal::Str(s)) => {
let l = s.to_lowercase();
matches!(
l.as_str(),
"a" | "accusative"
| "نصب"
| "منصوب"
| "g"
| "genitive"
| "جر"
| "مجرور"
| "o"
| "oblique"
)
}
_ => false,
}
}
fn finish_cardinal_case(out: String, minus: &str, is_oblique: bool) -> String {
let bare = py_lstrip_charset(&out, minus).trim().to_string();
for (construct, nominative, oblique) in AR_STANDALONE_DUAL.iter() {
if bare == *construct {
let form = if is_oblique { oblique } else { nominative };
return format!("{}{}", minus, form).trim().to_string();
}
}
if is_oblique {
let mut out = out;
for (nom, obl) in AR_DUAL_NOMINATIVE_TO_OBLIQUE.iter() {
out = out.replace(nom, obl);
}
return out;
}
out
}
fn kwval_display(v: &KwVal) -> String {
match v {
KwVal::Str(s) => s.clone(),
KwVal::Bool(b) => if *b { "True" } else { "False" }.to_string(),
KwVal::Int(i) => i.to_string(),
KwVal::None => "None".to_string(),
KwVal::List(l) => {
let items: Vec<String> = l.iter().map(|s| format!("'{}'", s)).collect();
format!("[{}]", items.join(", "))
}
}
}
struct CurrencyPrefs {
unit: &'static [&'static str; 4],
subunit: &'static [&'static str; 4],
part_precision: usize,
}
const CURRENCY_SR: ([&str; 4], [&str; 4]) = (
["ريال", "ريالان", "ريالات", "ريالاً"],
["هللة", "هللتان", "هللات", "هللة"],
);
const CURRENCY_EGP: ([&str; 4], [&str; 4]) = (
["جنيه", "جنيهان", "جنيهات", "جنيهاً"],
["قرش", "قرشان", "قروش", "قرش"],
);
const CURRENCY_KWD: ([&str; 4], [&str; 4]) = (
["دينار", "ديناران", "دينارات", "ديناراً"],
["فلس", "فلسان", "فلس", "فلس"],
);
const CURRENCY_LBP: ([&str; 4], [&str; 4]) = (
["ليرة", "ليرتان", "ليرات", "ليرة"],
["قرش", "قرشان", "قروش", "قرش"],
);
const CURRENCY_YER: ([&str; 4], [&str; 4]) = (
["ريال", "ريالان", "ريالات", "ريالاً"],
["فلس", "فلسان", "فلس", "فلس"],
);
const CURRENCY_USD: ([&str; 4], [&str; 4]) = (
["دولار", "دولارين", "دولارات", "دولاراً"],
["سنت", "سنتان", "سنتا", "سنتاٌ"],
);
const CURRENCY_TND: ([&str; 4], [&str; 4]) = (
["دينار", "ديناران", "دينارات", "ديناراً"],
["مليماً", "ميلمان", "مليمات", "مليم"],
);
fn set_currency_prefer(currency: &str) -> CurrencyPrefs {
let (unit, subunit, part_precision) = match currency {
"TND" => (&CURRENCY_TND.0, &CURRENCY_TND.1, 3),
"EGP" => (&CURRENCY_EGP.0, &CURRENCY_EGP.1, 2),
"KWD" => (&CURRENCY_KWD.0, &CURRENCY_KWD.1, 2),
"LBP" => (&CURRENCY_LBP.0, &CURRENCY_LBP.1, 2),
"YER" => (&CURRENCY_YER.0, &CURRENCY_YER.1, 2),
"USD" => (&CURRENCY_USD.0, &CURRENCY_USD.1, 2),
_ => (&CURRENCY_SR.0, &CURRENCY_SR.1, 2),
};
CurrencyPrefs {
unit,
subunit,
part_precision,
}
}
fn to_str_int(n: &BigInt) -> String {
n.to_string()
}
fn to_str_float(x: f64) -> String {
let integer = x.trunc();
let int_big = BigInt::from_f64(integer).unwrap_or_else(BigInt::zero);
if integer == x {
return int_big.to_string();
}
let decimal = py_round_half_even_f64((x - integer) * 1e9);
let padded = format!("{:0>9}", decimal);
format!("{}.{}", int_big, padded.trim_end_matches('0'))
}
fn py_round_half_even_f64(v: f64) -> i64 {
let fl = v.floor();
let base = fl as i64;
let frac = v - fl;
if frac > 0.5 {
base + 1
} else if frac < 0.5 {
base
} else if base % 2 == 0 {
base
} else {
base + 1
}
}
fn decimal_value(decimal_part: &str, part_precision: usize) -> String {
let n = decimal_part.chars().count();
if part_precision != n {
let mut b = decimal_part.to_string();
for _ in 0..part_precision.saturating_sub(n) {
b.push('0');
}
let len = b.chars().count();
let dec = if len <= part_precision {
len
} else {
part_precision
};
b.chars().take(dec).collect()
} else {
decimal_part.to_string()
}
}
fn number_str_is_zero(number: &str) -> bool {
number.chars().all(|c| c == '0' || c == '.')
}
fn convert_groups_currency(integer_value: &BigInt, fem_name: bool) -> Result<String> {
let thousand = BigInt::from(1000u16);
let mut ret_val = String::new();
let mut group: i32 = 0;
let mut temp = integer_value.clone();
while temp > BigInt::zero() {
let q_int = &temp / &thousand;
let prec = if ndigits(&q_int) > DEFAULT_PREC {
ndigits(&temp)
} else {
DEFAULT_PREC
};
let number_to_process = (&temp % &thousand).to_u32().expect("0..=999");
temp = round_half_even_sig(&temp, prec) / &thousand;
let group_description =
process_arabic_group(number_to_process, group, integer_value, fem_name)?;
if !group_description.is_empty() {
if group > 0 {
if !ret_val.is_empty() {
ret_val = format!("و{}", ret_val);
}
if number_to_process != 2 && number_to_process != 1 {
if group >= ARABIC_GROUP.len() as i32 {
return Err(assertion_error("group < len(self.arabicGroup)"));
}
if number_to_process % 100 != 1 {
if (3..=10).contains(&number_to_process) {
ret_val =
format!("{} {}", ARABIC_PLURAL_GROUPS[group as usize], ret_val);
} else if !ret_val.is_empty() {
ret_val =
format!("{} {}", ARABIC_APPENDED_GROUP[group as usize], ret_val);
} else {
ret_val = format!("{} {}", ARABIC_GROUP[group as usize], ret_val);
}
} else {
ret_val = format!("{} {}", ARABIC_GROUP[group as usize], ret_val);
}
}
}
ret_val = format!("{} {}", group_description, ret_val);
}
group += 1;
}
Ok(ret_val)
}
fn convert_currency(number: &str, prefs: &CurrencyPrefs) -> Result<String> {
let mut splits = number.splitn(2, '.');
let int_str = splits.next().unwrap_or("0");
let frac = splits.next();
let integer_value = BigInt::parse_bytes(int_str.as_bytes(), 10).ok_or_else(|| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
int_str
))
})?;
let decimal_value_num: u32 = match frac {
Some(f) => decimal_value(f, prefs.part_precision)
.parse()
.map_err(|_| N2WError::Value("invalid literal for int() with base 10".into()))?,
None => 0,
};
if number_str_is_zero(number) {
return Ok(ZERO_WORD.to_string());
}
let fem_name = false;
let decimal_string = process_arabic_group(decimal_value_num, -1, &integer_value, fem_name)?;
let ret_val = convert_groups_currency(&integer_value, fem_name)?;
let mut formatted = ret_val;
if !integer_value.is_zero() {
let remaining100 = (&integer_value % 100u8).to_u32().expect("0..=99");
if remaining100 == 0 {
formatted.push_str(prefs.unit[0]);
} else if remaining100 == 1 {
formatted.push_str(prefs.unit[0]);
} else if remaining100 == 2 {
if integer_value == BigInt::from(2u8) {
formatted.push_str(prefs.unit[1]);
} else {
formatted.push_str(prefs.unit[0]);
}
} else if (3..=10).contains(&remaining100) {
formatted.push_str(prefs.unit[2]);
} else if (11..=99).contains(&remaining100) {
formatted.push_str(prefs.unit[3]);
}
}
if decimal_value_num != 0 {
formatted = formatted.trim_end().to_string();
formatted.push_str(" و");
formatted.push_str(&decimal_string);
formatted.push(' ');
let remaining100 = decimal_value_num % 100;
if remaining100 == 0 {
formatted.push_str(prefs.subunit[0]);
} else if remaining100 == 1 {
formatted.push_str(prefs.subunit[0]);
} else if remaining100 == 2 {
formatted.push_str(prefs.subunit[1]);
} else if (3..=10).contains(&remaining100) {
formatted.push_str(prefs.subunit[2]);
} else if (11..=99).contains(&remaining100) {
formatted.push_str(prefs.subunit[3]);
}
}
Ok(formatted)
}
fn validate_number_float(x: f64) -> bool {
if !x.is_finite() {
return x > 0.0;
}
if x < 4.5e15 {
return false;
}
BigInt::from_f64(x.trunc())
.map(|i| i >= maxval())
.unwrap_or(false)
}
fn py_repr_f64_big(x: f64) -> String {
if !x.is_finite() {
return format!("{}", x);
}
let s = format!("{:e}", x);
match s.split_once('e') {
Some((mant, exp)) if !exp.starts_with('-') => format!("{}e+{}", mant, exp),
_ => s,
}
}
fn py_str_decimal(d: &BigDecimal) -> String {
let (mant, scale) = d.as_bigint_and_exponent();
let exponent = -scale;
let neg = mant.is_negative();
let digits = if neg {
(-&mant).to_string()
} else {
mant.to_string()
};
let ndig = digits.len() as i64;
let adjusted = exponent + ndig - 1;
let sign = if neg { "-" } else { "" };
if exponent <= 0 && adjusted >= -6 {
if exponent == 0 {
return format!("{}{}", sign, digits);
}
let point = ndig + exponent; if point > 0 {
let p = point as usize;
format!("{}{}.{}", sign, &digits[..p], &digits[p..])
} else {
format!("{}0.{}{}", sign, "0".repeat((-point) as usize), digits)
}
} else {
let mut m = digits[..1].to_string();
if ndig > 1 {
m.push('.');
m.push_str(&digits[1..]);
}
let e = if adjusted >= 0 {
format!("+{}", adjusted)
} else {
format!("-{}", -adjusted)
};
format!("{}{}E{}", sign, m, e)
}
}
fn py_round_half_even_dec(v: &BigDecimal) -> BigInt {
let (mant, exp) = v.as_bigint_and_exponent();
if exp <= 0 {
return mant * BigInt::from(10u8).pow((-exp) as u32);
}
let p = BigInt::from(10u8).pow(exp as u32);
let q = &mant / &p;
let r = &mant - &q * &p;
let half = &p / 2u8; let bump = r > half || (r == half && (&q % 2u8) != BigInt::zero());
if bump {
q + 1u8
} else {
q
}
}
fn dec_round_prec(x: &BigDecimal, prec: usize) -> BigDecimal {
let (mant, exp) = x.as_bigint_and_exponent();
let neg = mant.is_negative();
let m = if neg { -&mant } else { mant.clone() };
let d = ndigits(&m);
if d <= prec {
return x.clone();
}
let k = d - prec;
let p = BigInt::from(10u8).pow(k as u32);
let q = &m / &p;
let r = &m - &q * &p;
let half = &p / 2u8;
let bump = r > half || (r == half && (&q % 2u8) != BigInt::zero());
let q = if bump { q + 1u8 } else { q };
let q = if neg { -q } else { q };
BigDecimal::new(q, exp - k as i64)
}
fn to_str_decimal(x: &BigDecimal) -> String {
let integer = x.with_scale(0).as_bigint_and_exponent().0;
if BigDecimal::from(integer.clone()) == *x {
return integer.to_string();
}
let frac = dec_round_prec(&(x - BigDecimal::from(integer.clone())), DEFAULT_PREC);
let scaled = dec_round_prec(
&(frac * BigDecimal::from(BigInt::from(10u8).pow(9))),
DEFAULT_PREC,
);
let decimal = py_round_half_even_dec(&scaled);
let padded = format!("{:0>9}", decimal);
format!("{}.{}", integer, padded.trim_end_matches('0'))
}
fn to_str_float_checked(x: f64) -> Result<String> {
if x.is_nan() {
return Err(N2WError::Value(
"cannot convert float NaN to integer".into(),
));
}
if x.is_infinite() {
return Err(N2WError::Overflow(
"cannot convert float infinity to integer".into(),
));
}
Ok(to_str_float(x))
}
fn convert_groups_exact(integer_value: &BigInt, fem_name: bool) -> Result<String> {
let thousand = BigInt::from(1000u16);
let mut ret_val = String::new();
let mut group: i32 = 0;
let mut temp = integer_value.clone();
while temp > BigInt::zero() {
let number_to_process = (&temp % &thousand).to_u32().expect("0..=999");
temp /= &thousand;
let group_description =
process_arabic_group(number_to_process, group, integer_value, fem_name)?;
if !group_description.is_empty() {
if group > 0 {
if !ret_val.is_empty() {
ret_val = format!("و{}", ret_val);
}
if number_to_process != 2 && number_to_process != 1 {
if group >= ARABIC_GROUP.len() as i32 {
return Err(assertion_error("group < len(self.arabicGroup)"));
}
if number_to_process % 100 != 1 {
if (3..=10).contains(&number_to_process) {
ret_val =
format!("{} {}", ARABIC_PLURAL_GROUPS[group as usize], ret_val);
} else if !ret_val.is_empty() {
ret_val =
format!("{} {}", ARABIC_APPENDED_GROUP[group as usize], ret_val);
} else {
ret_val = format!("{} {}", ARABIC_GROUP[group as usize], ret_val);
}
} else {
ret_val = format!("{} {}", ARABIC_GROUP[group as usize], ret_val);
}
}
}
ret_val = format!("{} {}", group_description, ret_val);
}
group += 1;
}
Ok(ret_val)
}
fn convert_cardinal_float(number: &str) -> Result<String> {
let mut splits = number.splitn(2, '.');
let int_str = splits.next().unwrap_or("0");
let frac = splits.next();
let integer_value = BigInt::parse_bytes(int_str.as_bytes(), 10).ok_or_else(|| {
N2WError::Value(format!(
"invalid literal for int() with base 10: '{}'",
int_str
))
})?;
let decimal_value_num: u32 = match frac {
Some(f) => decimal_value(f, 2)
.parse()
.map_err(|_| N2WError::Value("invalid literal for int() with base 10".into()))?,
None => 0,
};
if number_str_is_zero(number) {
return Ok(ZERO_WORD.to_string());
}
let fem_name = false;
let decimal_string = process_arabic_group(decimal_value_num, -1, &integer_value, fem_name)?;
let ret_val = convert_groups_exact(&integer_value, fem_name)?;
let mut formatted = ret_val;
if decimal_value_num != 0 {
formatted = formatted.trim_end().to_string();
formatted.push_str(" ، ");
formatted.push_str(&decimal_string);
formatted.push(' ');
}
Ok(formatted)
}
fn cardinal_float_case(value: &FloatValue, is_oblique: bool) -> Result<String> {
let (number, minus) = match value {
FloatValue::Float { value: x, .. } => {
if validate_number_float(*x) {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
py_repr_f64_big(*x),
maxval()
)));
}
let minus = *x < 0.0;
let a = if *x >= 0.0 { *x } else { -*x };
(to_str_float_checked(a)?, minus)
}
FloatValue::Decimal { value: d, .. } => {
if *d >= BigDecimal::from(maxval()) {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
py_str_decimal(d),
maxval()
)));
}
let minus = d.is_negative();
let a = if d.is_negative() {
dec_round_prec(&(-d), DEFAULT_PREC)
} else {
d.clone()
};
(to_str_decimal(&a), minus)
}
};
let minus = if minus { MINUS } else { "" };
let out = format!("{}{}", minus, convert_cardinal_float(&number)?.trim());
Ok(finish_cardinal_case(out, minus, is_oblique))
}
pub struct LangAr;
impl LangAr {
pub fn new() -> Self {
LangAr
}
}
impl Default for LangAr {
fn default() -> Self {
Self::new()
}
}
impl Lang for LangAr {
fn python_maxval(&self) -> Option<num_bigint::BigInt> {
Some(num_bigint::BigInt::from(10u32).pow(51))
}
fn pointword(&self) -> &str {
"(.)"
}
fn default_currency(&self) -> &str {
"SR"
}
fn default_separator(&self) -> &str {
","
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
cardinal_int_case(value, false)
}
fn to_cardinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["case"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
cardinal_int_case(value, is_oblique_case(kw.get("case")))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
to_ordinal_impl(value, false, "")
}
fn to_ordinal_kw(&self, value: &BigInt, kw: &Kwargs) -> Result<String> {
if !kw.only(&["gender", "prefix"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
let feminine = match kw.get("gender") {
None => false,
Some(KwVal::Str(s)) => s != "m",
Some(_) => true,
};
let prefix = kw.get("prefix").map(kwval_display).unwrap_or_default();
to_ordinal_impl(value, feminine, &prefix)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
Ok(to_ordinal_impl(value, false, "")?.trim().to_string())
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
to_ordinal_impl(&float_int_trunc(value)?, false, "")
}
fn ordinal_num_float_entry(&self, value: &FloatValue, _repr_str: &str) -> Result<String> {
Ok(to_ordinal_impl(&float_int_trunc(value)?, false, "")?
.trim()
.to_string())
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::NaN => Err(N2WError::Custom {
module: "decimal",
class: "InvalidOperation",
msg: "[<class 'decimal.InvalidOperation'>]".into(),
}),
other => Ok(other),
}
}
fn to_year(&self, value: &BigInt) -> Result<String> {
validate_number(value)?;
self.to_cardinal(value)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
cardinal_float_case(value, false)
}
fn to_cardinal_float_kw(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
kw: &Kwargs,
) -> Result<String> {
if !kw.only(&["case"]) {
return Err(N2WError::Fallback("kwargs".into()));
}
cardinal_float_case(value, is_oblique_case(kw.get("case")))
}
fn lang_name(&self) -> &str {
"Num2Word_AR"
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
_cents: bool,
_separator: Option<&str>,
_adjective: bool,
) -> Result<String> {
let prefs = set_currency_prefer(currency);
let (number, minus) = match val {
CurrencyValue::Int(v) => {
validate_number(v)?;
let neg = v.is_negative();
let n = if neg { -v } else { v.clone() };
(to_str_int(&n), neg)
}
CurrencyValue::Decimal { value, .. } => {
let x: f64 = value.to_string().parse().map_err(|_| {
N2WError::Value(format!("could not convert string to float: '{}'", value))
})?;
if validate_number_float(x) {
return Err(N2WError::Overflow(format!(
"abs({}) must be less than {}.",
value,
maxval()
)));
}
let neg = x < 0.0;
let n = if neg { -x } else { x };
(to_str_float(n), neg)
}
};
let result = convert_currency(&number, &prefs)?;
if minus {
return Ok(format!("{}{}", MINUS, result));
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn f(x: f64) -> FloatValue {
FloatValue::Float {
value: x,
precision: 0,
}
}
fn d(s: &str) -> FloatValue {
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision: 0,
}
}
fn card(v: FloatValue) -> String {
LangAr::new().to_cardinal_float(&v, None).unwrap()
}
#[test]
fn corpus_float_rows() {
for (arg, out) in [
("0.0", "صفر"),
("0.5", "، خمسون"),
("1.0", "واحد"),
("1.5", "واحد ، خمسون"),
("2.25", "اثنان ، خمس وعشرون"),
("3.14", "ثلاثة ، أربع عشرة"),
("0.01", "، إحدى"),
("0.1", "، عشر"),
("0.99", "، تسع وتسعون"),
("1.01", "واحد ، إحدى"),
("12.34", "اثنا عشر ، أربع وثلاثون"),
("99.99", "تسعة وتسعون ، تسع وتسعون"),
("100.5", "مائة ، خمسون"),
("1234.56", "ألف ومئتان وأربعة وثلاثون ، ست وخمسون"),
("-0.5", "سالب ، خمسون"),
("-1.5", "سالب واحد ، خمسون"),
("-12.34", "سالب اثنا عشر ، أربع وثلاثون"),
("1.005", "واحد"),
("2.675", "اثنان ، سبع وستون"),
] {
assert_eq!(card(f(arg.parse().unwrap())), out, "float {}", arg);
}
}
#[test]
fn corpus_decimal_rows() {
for (arg, out) in [
("0.01", "، إحدى"),
("1.10", "واحد ، عشر"),
("12.345", "اثنا عشر ، أربع وثلاثون"),
(
"98746251323029.99",
"ثمانية وتسعون تريليوناً وسبعمائة وستة وأربعون ملياراً ومئتان وواحد وخمسون مليوناً وثلاثمائة وثلاثة وعشرون ألفاً وتسعة وعشرون ، تسع وتسعون",
),
("0.001", ""),
] {
assert_eq!(card(d(arg)), out, "Decimal {}", arg);
}
}
#[test]
fn fraction_is_truncated_to_two_digits_not_rounded() {
assert_eq!(card(f(12.345)), card(f(12.34)));
assert_eq!(card(d("12.999")), card(d("12.99")));
for p in [None, Some(1), Some(3), Some(5)] {
let got = LangAr::new().to_cardinal_float(&f(12.345), p).unwrap();
assert_eq!(
got, "اثنا عشر ، أربع وثلاثون",
"precision={:?} must be inert",
p
);
}
}
#[test]
fn float_artefacts_are_reproduced_not_repaired() {
assert_eq!(card(f(2.675)), "اثنان ، سبع وستون"); assert_eq!(card(f(1.005)), "واحد");
assert_eq!(card(f(2.9999999999)), "اثنان ، عشر");
assert_eq!(card(f(1.0000000001)), "واحد");
assert_eq!(card(f(1e-10)), "صفر");
assert_eq!(card(d("0.0000000005000000000000000000000000001")), "");
}
#[test]
fn zero_word_is_asked_of_the_whole_to_str_string() {
assert_eq!(card(f(0.0)), "صفر");
assert_eq!(card(f(-0.0)), "صفر"); assert_eq!(card(d("0")), "صفر");
assert_eq!(card(d("-0")), "صفر");
assert_eq!(card(d("0.00")), "صفر");
assert_eq!(card(f(0.001)), "");
assert_eq!(card(d("0.001")), "");
}
#[test]
fn standalone_dual_rewrite_reaches_negatives() {
assert_eq!(card(f(200.0)), "مئتان");
assert_eq!(card(f(-200.0)), "سالب مئتان");
assert_eq!(card(f(2000.0)), "ألفان");
assert_eq!(card(f(-2000.0)), "سالب ألفان");
assert_eq!(card(d("200.00")), "مئتان");
assert_eq!(card(d("-200.00")), "سالب مئتان");
assert_eq!(card(f(200.5)), "مئتا ، خمسون");
assert_eq!(card(f(2000.5)), "ألفا ، خمسون");
assert_eq!(card(d("2E+30")), "نونيليونا");
}
#[test]
fn abs_rounds_negative_decimals_to_28_significant_digits() {
let neg = card(d("-646888273752320466851667632519.93"));
assert!(
neg.ends_with("وخمسمائة"),
"tail should be 500, got {:?}",
neg
);
assert!(!neg.contains('،'), "fraction must vanish, got {:?}", neg);
let pos = card(d("646888273752320466851667632519.93"));
assert!(pos.ends_with("وتسعة عشر ، ثلاث وتسعون"), "got {:?}", pos);
assert_eq!(
card(d("-12345678901234567890123456.78"))
.trim_start_matches("سالب ")
.to_string(),
card(d("12345678901234567890123456.78")),
);
}
#[test]
fn group_loop_is_exact_across_the_old_danger_band() {
let via_float = card(f(1e30));
let via_decimal = card(d("1000000000000000019884624838656"));
assert_eq!(via_float, via_decimal);
assert!(
via_float.contains("وثمانمائة وثمانية وثلاثون ألفاً"),
"got {:?}",
via_float
);
assert!(card(f(2e30)).starts_with("نونيليونان و"));
}
#[test]
fn bounds_and_error_variants() {
let l = LangAr::new();
assert!(l.to_cardinal_float(&f(1e51), None).is_ok());
match l.to_cardinal_float(&f(1e52), None) {
Err(N2WError::Overflow(m)) => assert_eq!(
m,
"abs(1e+52) must be less than 1000000000000000000000000000000000000000000000000000."
),
o => panic!("expected Overflow, got {:?}", o),
}
match l.to_cardinal_float(&d("1E+51"), None) {
Err(N2WError::Overflow(m)) => assert_eq!(
m,
"abs(1E+51) must be less than 1000000000000000000000000000000000000000000000000000."
),
o => panic!("expected Overflow, got {:?}", o),
}
assert!(matches!(
l.to_cardinal_float(&f(-1e52), None),
Err(N2WError::Assertion(_))
));
assert!(matches!(
l.to_cardinal_float(
&d("-1000000000000000000000000000000000000000000000000000"),
None
),
Err(N2WError::Assertion(_))
));
match l.to_cardinal_float(&f(f64::INFINITY), None) {
Err(N2WError::Overflow(m)) => assert!(m.starts_with("abs(inf) must be less than")),
o => panic!("expected Overflow, got {:?}", o),
}
match l.to_cardinal_float(&f(f64::NEG_INFINITY), None) {
Err(N2WError::Overflow(m)) => assert_eq!(m, "cannot convert float infinity to integer"),
o => panic!("expected Overflow, got {:?}", o),
}
match l.to_cardinal_float(&f(f64::NAN), None) {
Err(N2WError::Value(m)) => assert_eq!(m, "cannot convert float NaN to integer"),
o => panic!("expected ValueError, got {:?}", o),
}
}
#[test]
fn py_str_decimal_matches_python() {
for (input, want) in [
("1E+51", "1E+51"),
("1.5E+51", "1.5E+51"),
("123E+51", "1.23E+53"),
("9.99E+51", "9.99E+51"),
("1.10", "1.10"),
("0.5", "0.5"),
("12.345", "12.345"),
] {
assert_eq!(
py_str_decimal(&BigDecimal::from_str(input).unwrap()),
want,
"input {}",
input
);
}
}
#[test]
fn fraction_is_feminine_integer_is_masculine() {
assert_eq!(card(f(12.34)), "اثنا عشر ، أربع وثلاثون");
assert_eq!(card(f(2.02)), "اثنان ، اثنتان");
}
fn ord_f(v: FloatValue) -> String {
LangAr::new().ordinal_float_entry(&v).unwrap()
}
fn ordnum_f(v: FloatValue, repr: &str) -> String {
LangAr::new().ordinal_num_float_entry(&v, repr).unwrap()
}
fn kws(pairs: &[(&str, KwVal)]) -> Kwargs {
Kwargs(
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect(),
)
}
#[test]
fn ordinal_float_entry_truncates_like_int() {
for (arg, out) in [
(-1000000.0, "مليون"),
(-1000.0, "ألف"),
(-21.0, "إحدى وعشرون"),
(-2.0, "اثنتان"),
(-1.0, "إحدى"),
(-0.0, "صفر"),
(0.0, "صفر"),
(0.5, "صفر"),
(1.0, "الأول"),
(2.5, "الثاني"),
(-1.5, "إحدى"),
(3.25, "الثالث"),
(12.0, "الثاني عشر"),
(20.0, "العشرون"),
(21.0, "الأول والعشرون"),
(42.0, "الثاني والأربعون"),
(100.0, "المائة"),
(101.0, "الأول بعد المائة"),
(1234.0, "ألف ومئتان وأربعة وثلاثون"),
(1e16, "عشرة كوادريليونات"),
(1e20, "مائة كوينتليون"),
] {
assert_eq!(ord_f(f(arg)), out, "ordinal {}", arg);
assert_eq!(ordnum_f(f(arg), "x"), out, "ordinal_num {}", arg);
}
for (arg, out) in [
("0", "صفر"),
("5", "الخامس"),
("5.00", "الخامس"),
("-3.0", "ثلاث"),
("1E+2", "المائة"),
("12345.000", "اثنا عشر ألفاً وثلاثمائة وخمسة وأربعون"),
("1E+20", "مائة كوينتليون"),
("-0.0", "صفر"),
("1.5", "الأول"),
] {
assert_eq!(ord_f(d(arg)), out, "ordinal Decimal {}", arg);
assert_eq!(ordnum_f(d(arg), "x"), out, "ordinal_num Decimal {}", arg);
}
assert!(matches!(
LangAr::new().ordinal_float_entry(&f(f64::INFINITY)),
Err(N2WError::Overflow(_))
));
assert!(matches!(
LangAr::new().ordinal_float_entry(&f(f64::NAN)),
Err(N2WError::Value(_))
));
}
#[test]
fn cardinal_case_kwarg() {
let l = LangAr::new();
let n = |v: i64| BigInt::from(v);
for case in [
"a",
"accusative",
"نصب",
"منصوب",
"g",
"genitive",
"جر",
"مجرور",
"o",
"oblique",
] {
let kw = kws(&[("case", KwVal::Str(case.into()))]);
assert_eq!(
l.to_cardinal_kw(&n(1234), &kw).unwrap(),
"ألف ومئتين وأربعة وثلاثون",
"case={}",
case
);
assert_eq!(l.to_cardinal_kw(&n(200), &kw).unwrap(), "مئتين");
assert_eq!(l.to_cardinal_kw(&n(-200), &kw).unwrap(), "سالب مئتين");
assert_eq!(l.to_cardinal_kw(&n(2), &kw).unwrap(), "اثنان");
}
let kw = kws(&[("case", KwVal::Str("ACCUSATIVE".into()))]);
assert_eq!(l.to_cardinal_kw(&n(200), &kw).unwrap(), "مئتين");
for v in [
KwVal::Str("nominative".into()),
KwVal::Str("bogus".into()),
KwVal::None,
KwVal::Int(3),
KwVal::Bool(true),
] {
let kw = kws(&[("case", v)]);
assert_eq!(
l.to_cardinal_kw(&n(1234), &kw).unwrap(),
"ألف ومئتان وأربعة وثلاثون"
);
assert_eq!(l.to_cardinal_kw(&n(200), &kw).unwrap(), "مئتان");
}
let kw = kws(&[("plural", KwVal::Bool(true))]);
assert!(matches!(
l.to_cardinal_kw(&n(5), &kw),
Err(N2WError::Fallback(_))
));
let kw = kws(&[("case", KwVal::Str("a".into()))]);
assert_eq!(
l.to_cardinal_float_kw(&f(200.0), None, &kw).unwrap(),
"مئتين"
);
assert_eq!(
l.to_cardinal_float_kw(&f(1234.5), None, &kw).unwrap(),
"ألف ومئتين وأربعة وثلاثون ، خمسون"
);
assert_eq!(
l.to_cardinal_float_kw(&f(200.5), None, &kw).unwrap(),
"مئتا ، خمسون"
);
}
#[test]
fn ordinal_gender_and_prefix_kwargs() {
let l = LangAr::new();
let n = |v: i64| BigInt::from(v);
let fem = kws(&[("gender", KwVal::Str("f".into()))]);
assert_eq!(l.to_ordinal_kw(&n(1), &fem).unwrap(), "الأولى");
assert_eq!(l.to_ordinal_kw(&n(11), &fem).unwrap(), "الحادية عشرة");
assert_eq!(l.to_ordinal_kw(&n(21), &fem).unwrap(), "الأولى والعشرون");
assert_eq!(l.to_ordinal_kw(&n(100), &fem).unwrap(), "المائة");
assert_eq!(l.to_ordinal_kw(&n(-5), &fem).unwrap(), "خمس");
assert_eq!(
l.to_ordinal_kw(&n(1234), &fem).unwrap(),
"ألف ومئتان وأربعة وثلاثون"
);
for v in [KwVal::Str("M".into()), KwVal::None, KwVal::Int(0)] {
let kw = kws(&[("gender", v)]);
assert_eq!(l.to_ordinal_kw(&n(1), &kw).unwrap(), "الأولى");
}
let px = kws(&[("prefix", KwVal::Str("xx".into()))]);
assert_eq!(l.to_ordinal_kw(&n(2000), &px).unwrap(), "xx ألفا");
assert_eq!(l.to_ordinal_kw(&n(0), &px).unwrap(), "صفر");
assert_eq!(l.to_ordinal_kw(&n(5), &px).unwrap(), "الخامس");
let kw = kws(&[("case", KwVal::Str("a".into()))]);
assert!(matches!(
l.to_ordinal_kw(&n(5), &kw),
Err(N2WError::Fallback(_))
));
}
#[test]
fn str_to_number_nan_raises_invalid_operation() {
let l = LangAr::new();
match l.str_to_number("NaN") {
Err(N2WError::Custom { module, class, msg }) => {
assert_eq!(module, "decimal");
assert_eq!(class, "InvalidOperation");
assert_eq!(msg, "[<class 'decimal.InvalidOperation'>]");
}
o => panic!("expected InvalidOperation, got {:?}", o),
}
assert!(matches!(
l.str_to_number("Infinity"),
Ok(ParsedNumber::Inf { negative: false })
));
assert!(matches!(l.str_to_number("1.5"), Ok(ParsedNumber::Dec(_))));
assert!(l.str_to_number("abc").is_err());
}
}