use crate::base::{Lang, N2WError, Result};
use crate::currency::CurrencyValue;
use crate::floatpath::FloatValue;
use crate::lang_en::LangEn;
use crate::strnum::{python_decimal_parse, ParsedNumber};
use bigdecimal::BigDecimal;
use num_bigint::{BigInt, Sign};
use num_traits::Signed;
const ICAO_DIGITS: [&str; 10] = [
"zero", "wun", "too", "tree", "fower", "fife", "six", "seven", "ait", "niner",
];
const ICAO_DECIMAL: &str = "decimal";
const ICAO_MINUS: &str = "minus";
pub struct LangEnAero {
english: LangEn,
digit_table: [&'static str; 10],
decimal_word: &'static str,
minus_word: &'static str,
}
impl Default for LangEnAero {
fn default() -> Self {
Self::new()
}
}
impl LangEnAero {
pub fn new() -> Self {
LangEnAero {
english: LangEn::new(),
digit_table: ICAO_DIGITS,
decimal_word: ICAO_DECIMAL,
minus_word: ICAO_MINUS,
}
}
fn digits_of(&self, value: &BigInt) -> (bool, String) {
let s = value.to_string();
let is_negative = s.starts_with('-');
let int_part = if is_negative { s[1..].to_string() } else { s };
(is_negative, int_part)
}
fn digits_of_float(&self, v: &FloatValue) -> (bool, String, String) {
match v {
FloatValue::Float { value, precision } => {
let is_negative = value.is_sign_negative();
let signed = python_float_repr(*value, *precision);
let s = signed.strip_prefix('-').unwrap_or(&signed).to_string();
let (int_part, frac_part) = match s.split_once('.') {
Some((i, f)) => (i.to_string(), f.to_string()),
None => (s, String::new()),
};
let int_part = if int_part.is_empty() {
"0".to_string()
} else {
int_part
};
(is_negative, int_part, frac_part)
}
FloatValue::Decimal { value, .. } => {
let is_negative = value.is_negative();
let (unscaled, scale) = value.abs().as_bigint_and_exponent();
if scale <= 0 {
let digits =
(unscaled * BigInt::from(10).pow((-scale) as u32)).to_string();
return (is_negative, digits, String::new());
}
let scale = scale as usize;
let digits = unscaled.to_string();
let (int_part, frac_part) = {
let padded = if digits.len() < scale + 1 {
format!("{}{}", "0".repeat(scale + 1 - digits.len()), digits)
} else {
digits
};
let split = padded.len() - scale;
(padded[..split].to_string(), padded[split..].to_string())
};
let int_part = if int_part.is_empty() {
"0".to_string()
} else {
int_part
};
(is_negative, int_part, frac_part)
}
}
}
fn verify_ordinal(&self, value: &BigInt) -> Result<()> {
if value.sign() == Sign::Minus {
return Err(N2WError::Type(format!(
"Cannot treat negative num {} as ordinal.",
value
)));
}
Ok(())
}
}
impl Lang for LangEnAero {
fn python_maxval(&self) -> Option<num_bigint::BigInt> {
Some(num_bigint::BigInt::from(10u32).pow(306))
}
fn negword(&self) -> &str {
"minus "
}
fn pointword(&self) -> &str {
"point"
}
fn to_cardinal(&self, value: &BigInt) -> Result<String> {
let (is_negative, int_part) = self.digits_of(value);
let mut words: Vec<&str> = Vec::new();
if is_negative {
words.push(self.minus_word);
}
for ch in int_part.chars() {
if let Some(d) = ch.to_digit(10) {
words.push(self.digit_table[d as usize]);
}
}
let _ = self.decimal_word;
Ok(words.join(" "))
}
fn to_cardinal_float(
&self,
value: &FloatValue,
_precision_override: Option<u32>,
) -> Result<String> {
let (is_negative, int_part, frac_part) = self.digits_of_float(value);
let mut words: Vec<&str> = Vec::new();
if is_negative {
words.push(self.minus_word);
}
for ch in int_part.chars() {
if let Some(d) = ch.to_digit(10) {
words.push(self.digit_table[d as usize]);
}
}
if !frac_part.is_empty() {
words.push(self.decimal_word);
for ch in frac_part.chars() {
if let Some(d) = ch.to_digit(10) {
words.push(self.digit_table[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.sign() == Sign::Minus {
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_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)
}
}
pub(crate) fn python_float_repr(v: f64, precision: u32) -> String {
if v.is_nan() {
return "nan".to_string();
}
if v.is_infinite() {
return if v < 0.0 { "-inf" } else { "inf" }.to_string();
}
let a = v.abs();
if a != 0.0 && (a >= 1e16 || a < 1e-4) {
let sci = format!("{:e}", v);
let (mantissa, exp) = sci
.split_once('e')
.expect("Rust's {:e} always emits an 'e' separator");
let exp: i32 = exp
.parse()
.expect("Rust's {:e} always emits a decimal exponent");
return format!(
"{}e{}{:02}",
mantissa,
if exp < 0 { '-' } else { '+' },
exp.abs()
);
}
format!("{:.*}", precision as usize, v)
}
pub(crate) const AERO_INF_EXPONENT: i64 = 1_000_000_001;
pub(crate) const AERO_NAN_EXPONENT: i64 = 1_000_000_003;
pub(crate) enum AeroSpecial {
Inf { negative: bool },
NaN,
}
impl AeroSpecial {
pub(crate) fn cardinal_words(&self) -> &'static str {
match self {
AeroSpecial::Inf { negative: true } => "minus",
_ => "",
}
}
pub(crate) fn int_error(&self) -> N2WError {
match self {
AeroSpecial::Inf { .. } => {
N2WError::Overflow("cannot convert Infinity to integer".into())
}
AeroSpecial::NaN => N2WError::Value("cannot convert NaN to integer".into()),
}
}
}
pub(crate) fn aero_str_to_number(s: &str) -> Result<ParsedNumber> {
match python_decimal_parse(s)? {
ParsedNumber::Inf { negative } => Ok(ParsedNumber::Dec(BigDecimal::new(
BigInt::from(if negative { -1 } else { 1 }),
-AERO_INF_EXPONENT,
))),
ParsedNumber::NaN => Ok(ParsedNumber::Dec(BigDecimal::new(
BigInt::from(1),
-AERO_NAN_EXPONENT,
))),
other => Ok(other),
}
}
pub(crate) fn aero_special_of_decimal(d: &BigDecimal) -> Option<AeroSpecial> {
let (m, scale) = d.as_bigint_and_exponent();
if scale == -AERO_INF_EXPONENT {
return Some(AeroSpecial::Inf {
negative: m.sign() == Sign::Minus,
});
}
if scale == -AERO_NAN_EXPONENT {
return Some(AeroSpecial::NaN);
}
None
}
pub(crate) fn aero_special_of(v: &FloatValue) -> Option<AeroSpecial> {
match v {
FloatValue::Decimal { value, .. } => aero_special_of_decimal(value),
FloatValue::Float { .. } => None,
}
}