use crate::base::{Cards, Lang, N2WError, Result};
use crate::currency::{CurrencyForms, CurrencyValue};
use crate::floatpath::FloatValue;
use crate::lang_en::LangEn;
use crate::lang_en_aero::{
aero_special_of, aero_special_of_decimal, aero_str_to_number, python_float_repr,
};
use crate::strnum::ParsedNumber;
use bigdecimal::BigDecimal;
use num_bigint::BigInt;
use num_traits::Signed;
const ICAO_DIGITS: [&str; 10] = [
"zero", "wun", "too", "tree", "fower", "fife", "six", "seven", "ait", "niner", ];
const MINUS_WORD: &str = "minus";
const DECIMAL_WORD: &str = "decimal";
pub struct LangEnAeroNato {
english: LangEn,
}
impl Default for LangEnAeroNato {
fn default() -> Self {
Self::new()
}
}
impl LangEnAeroNato {
pub fn new() -> Self {
LangEnAeroNato {
english: LangEn::new(),
}
}
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 digits_of(v: &FloatValue) -> (bool, String, String) {
match v {
FloatValue::Float { value, precision } => {
let s = python_float_repr(*value, *precision);
let (is_negative, body) = match s.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, s.as_str()),
};
match body.split_once('.') {
Some((int_part, frac_part)) => {
(is_negative, int_part.to_string(), frac_part.to_string())
}
None => (is_negative, body.to_string(), String::new()),
}
}
FloatValue::Decimal { value, .. } => {
let is_negative = value.is_negative();
let (digits, scale) = value.abs().as_bigint_and_exponent();
if scale <= 0 {
let s = if num_traits::Zero::is_zero(&digits) {
"0".to_string()
} else {
format!("{}{}", digits, "0".repeat((-scale) as usize))
};
(is_negative, s, String::new())
} else {
let mut s = digits.to_string();
let p = scale as usize;
while s.len() <= p {
s.insert(0, '0');
}
let split = s.len() - p;
(is_negative, s[..split].to_string(), s[split..].to_string())
}
}
}
}
impl Lang for LangEnAeroNato {
fn cards(&self) -> &Cards {
self.english.cards()
}
fn maxval(&self) -> &BigInt {
self.english.maxval()
}
fn merge(&self, l: (&str, &BigInt), r: (&str, &BigInt)) -> (String, BigInt) {
self.english.merge(l, r)
}
fn negword(&self) -> &str {
"minus "
}
fn pointword(&self) -> &str {
"point"
}
fn exclude_title(&self) -> &[String] {
self.english.exclude_title()
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let s = value.to_string();
let mut words: Vec<&str> = Vec::new();
let digits = if let Some(stripped) = s.strip_prefix('-') {
words.push(MINUS_WORD);
stripped
} else {
&s[..]
};
for ch in digits.chars() {
if let Some(d) = ch.to_digit(10) {
words.push(ICAO_DIGITS[d as usize]);
}
}
Ok(words.join(" "))
}
fn to_ordinal(&self, value: &BigInt) -> Result<String> {
self.english.to_ordinal(value)
}
fn to_ordinal_num(&self, value: &BigInt) -> Result<String> {
self.verify_ordinal(value)?;
Ok(value.to_string())
}
fn to_year(&self, value: &BigInt) -> Result<String> {
self.to_cardinal(value)
}
fn cardinal_float_entry(
&self,
value: &FloatValue,
precision_override: Option<u32>,
) -> Result<String> {
if let Some(sp) = aero_special_of(value) {
return Ok(sp.cardinal_words().to_string());
}
self.to_cardinal_float(value, precision_override)
}
fn ordinal_float_entry(&self, value: &FloatValue) -> Result<String> {
if let Some(sp) = aero_special_of(value) {
return Err(sp.int_error());
}
self.english.ordinal_float_entry(value)
}
fn ordinal_num_float_entry(&self, value: &FloatValue, repr_str: &str) -> Result<String> {
if let Some(sp) = aero_special_of(value) {
return Err(sp.int_error());
}
match value.as_whole_int() {
None => Err(N2WError::Type(format!(
"Cannot treat float {} as ordinal.",
repr_str
))),
Some(i) => {
if i.is_negative() {
Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
repr_str
)))
} else {
Ok(i.to_string())
}
}
}
}
fn year_float_entry(&self, value: &FloatValue) -> Result<String> {
self.cardinal_float_entry(value, None)
}
fn to_fraction(&self, numerator: &BigInt, denominator: &BigInt) -> Result<String> {
self.english.to_fraction(numerator, denominator)
}
fn str_to_number(&self, s: &str) -> Result<ParsedNumber> {
aero_str_to_number(s)
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let (is_negative, int_part, frac_part) = digits_of(value);
let mut words: Vec<&str> = Vec::new();
if is_negative {
words.push(MINUS_WORD);
}
for ch in int_part.chars() {
if let Some(d) = ch.to_digit(10) {
words.push(ICAO_DIGITS[d as usize]);
}
}
if !frac_part.is_empty() {
words.push(DECIMAL_WORD);
for ch in frac_part.chars() {
if let Some(d) = ch.to_digit(10) {
words.push(ICAO_DIGITS[d as usize]);
}
}
}
Ok(words.join(" "))
}
fn lang_name(&self) -> &str {
"Num2Word_EN_AERO_NATO"
}
fn currency_forms(&self, code: &str) -> Option<&CurrencyForms> {
self.english.currency_forms(code)
}
fn currency_adjective(&self, code: &str) -> Option<&str> {
self.english.currency_adjective(code)
}
fn currency_precision(&self, code: &str) -> i64 {
self.english.currency_precision(code)
}
fn pluralize(&self, n: &BigInt, forms: &[String]) -> Result<String> {
self.english.pluralize(n, forms)
}
fn to_currency(
&self,
val: &CurrencyValue,
currency: &str,
cents: bool,
separator: Option<&str>,
adjective: bool,
) -> Result<String> {
if let CurrencyValue::Decimal { value: d, .. } = val {
if let Some(sp) = aero_special_of_decimal(d) {
return Err(sp.int_error());
}
}
self.english
.to_currency(val, currency, cents, separator, adjective)
}
fn to_cheque(&self, val: &BigDecimal, currency: &str) -> Result<String> {
self.english.to_cheque(val, currency)
}
}
#[cfg(test)]
mod float_tests {
use super::*;
use bigdecimal::BigDecimal;
use std::str::FromStr;
fn cf(v: &FloatValue) -> String {
LangEnAeroNato::new().to_cardinal_float(v, None).unwrap()
}
fn flt(value: f64, precision: u32) -> FloatValue {
FloatValue::Float { value, precision }
}
fn dec(s: &str, precision: u32) -> FloatValue {
FloatValue::Decimal {
value: BigDecimal::from_str(s).unwrap(),
precision,
}
}
#[test]
fn float_corpus_rows() {
let cases: &[(f64, u32, &str)] = &[
(0.0, 1, "zero decimal zero"),
(0.5, 1, "zero decimal fife"),
(1.0, 1, "wun decimal zero"),
(1.5, 1, "wun decimal fife"),
(2.25, 2, "too decimal too fife"),
(3.14, 2, "tree decimal wun fower"),
(0.01, 2, "zero decimal zero wun"),
(0.1, 1, "zero decimal wun"),
(0.99, 2, "zero decimal niner niner"),
(1.01, 2, "wun decimal zero wun"),
(12.34, 2, "wun too decimal tree fower"),
(99.99, 2, "niner niner decimal niner niner"),
(100.5, 1, "wun zero zero decimal fife"),
(1234.56, 2, "wun too tree fower decimal fife six"),
(-0.5, 1, "minus zero decimal fife"),
(-1.5, 1, "minus wun decimal fife"),
(-12.34, 2, "minus wun too decimal tree fower"),
(1.005, 3, "wun decimal zero zero fife"),
(2.675, 3, "too decimal six seven fife"),
];
for (v, p, expect) in cases {
assert_eq!(cf(&flt(*v, *p)), *expect, "float {v} (prec {p})");
}
}
#[test]
fn decimal_corpus_rows() {
let cases: &[(&str, u32, &str)] = &[
("0.01", 2, "zero decimal zero wun"),
("1.10", 2, "wun decimal wun zero"),
("12.345", 3, "wun too decimal tree fower fife"),
(
"98746251323029.99",
2,
"niner ait seven fower six too fife wun tree too tree zero too niner decimal niner niner",
),
("0.001", 3, "zero decimal zero zero wun"),
];
for (s, p, expect) in cases {
assert_eq!(cf(&dec(s, *p)), *expect, "decimal {s} (prec {p})");
}
}
#[test]
fn negative_zero_float() {
assert_eq!(cf(&flt(-0.0, 1)), "minus zero decimal zero");
}
}