diff --git a/core/src/ast.rs b/core/src/ast.rs index 7619e938..f229874b 100644 --- a/core/src/ast.rs +++ b/core/src/ast.rs @@ -561,6 +561,7 @@ fn evaluate_add( }) } +#[allow(clippy::too_many_lines)] fn evaluate_as( a: Expr, b: Expr, @@ -641,6 +642,14 @@ fn evaluate_as( } return Ok(Value::String(borrow::Cow::Owned(to_roman(a)))); } + "words" => { + let uint = evaluate(a, scope, attrs, context, int)? + .expect_num()? + .into_unitless_complex(int)? + .try_as_real()? + .try_as_biguint(int)?; + return Ok(Value::String(borrow::Cow::Owned(uint.to_words(int)?))); + } _ => (), } } diff --git a/core/src/num/bigrat.rs b/core/src/num/bigrat.rs index d1257c6a..507c1670 100644 --- a/core/src/num/bigrat.rs +++ b/core/src/num/bigrat.rs @@ -136,6 +136,17 @@ impl BigRat { self.den == 1.into() } + pub(crate) fn try_as_biguint(mut self, int: &I) -> FResult { + if self.sign == Sign::Negative && self.num != 0.into() { + return Err(FendError::NegativeNumbersNotAllowed); + } + self = self.simplify(int)?; + if self.den != 1.into() { + return Err(FendError::FractionToInteger); + } + Ok(self.num) + } + pub(crate) fn try_as_usize(mut self, int: &I) -> FResult { if self.sign == Sign::Negative && self.num != 0.into() { return Err(FendError::NegativeNumbersNotAllowed); diff --git a/core/src/num/biguint.rs b/core/src/num/biguint.rs index 7d53c248..244fa7f0 100644 --- a/core/src/num/biguint.rs +++ b/core/src/num/biguint.rs @@ -611,6 +611,128 @@ impl BigUint { } Ok(self) } + + pub(crate) fn to_words(&self, int: &I) -> FResult { + let num = self + .format( + &FormatOptions { + base: Base::from_plain_base(10)?, + sf_limit: None, + write_base_prefix: false, + }, + int, + )? + .value + .to_string(); + + if num == "0" { + return Ok("zero".to_string()); + } + + let mut result = String::new(); + let mut chunks = Vec::new(); + + let mut i = num.len(); + while i > 0 { + let start = if i >= 3 { i - 3 } else { 0 }; + chunks.push(&num[start..i]); + i = start; + } + + for (i, chunk) in chunks.iter().enumerate().rev() { + let part = chunk.parse::().unwrap_or(0); + if part != 0 { + if !result.is_empty() { + result.push(' '); + } + convert_below_1000(part, &mut result); + if i > 0 { + result.push(' '); + result.push_str(SCALE_NUMBERS.get(i).ok_or_else(|| FendError::OutOfRange { + value: Box::new(num.clone()), + range: Range { + start: RangeBound::Closed(Box::new("0")), + end: RangeBound::Closed(Box::new("10^66 - 1")), + }, + })?); + } + } + } + + Ok(result.trim().to_string()) + } +} + +const SMALL_NUMBERS: &[&str] = &[ + "zero", + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "ten", + "eleven", + "twelve", + "thirteen", + "fourteen", + "fifteen", + "sixteen", + "seventeen", + "eighteen", + "nineteen", +]; +const TENS: &[&str] = &[ + "", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety", +]; +const SCALE_NUMBERS: &[&str] = &[ + "", + "thousand", + "million", + "billion", + "trillion", + "quadrillion", + "quintillion", + "sextillion", + "septillion", + "octillion", + "nonillion", + "decillion", + "undecillion", + "duodecillion", + "tredecillion", + "quattuordecillion", + "quindecillion", + "sexdecillion", + "septendecillion", + "octodecillion", + "novemdecillion", + "vigintillion", +]; + +fn convert_below_1000(num: usize, result: &mut String) { + if num >= 100 { + result.push_str(SMALL_NUMBERS[num / 100]); + result.push_str(" hundred"); + if num % 100 != 0 { + result.push_str(" and "); + } + } + + let remainder = num % 100; + + if remainder < 20 && remainder > 0 { + result.push_str(SMALL_NUMBERS[remainder]); + } else if remainder >= 20 { + result.push_str(TENS[remainder / 10]); + if remainder % 10 != 0 { + result.push('-'); + result.push_str(SMALL_NUMBERS[remainder % 10]); + } + } } impl Ord for BigUint { @@ -982,4 +1104,22 @@ mod tests { ); Ok(()) } + + #[test] + fn words() -> Res { + let int = &crate::interrupt::Never; + assert_eq!( + BigUint::from(123).to_words(int)?, + "one hundred and twenty-three" + ); + assert_eq!( + BigUint::from(10_000_347_001_023_000_002).to_words(int)?, + "ten quintillion three hundred and forty-seven trillion one billion twenty-three million two" + ); + assert_eq!( + BigUint::Large(vec![0, 0x0e3c_bb5a_c574_1c64, 0x1cfd_a3a5_6977_58bf, 0x097e_dd87]).to_words(int).unwrap_err().to_string(), + "1000000000000000000000000000000000000000000000000000000000000000000 must lie in the interval [0, 10^66 - 1]" + ); + Ok(()) + } } diff --git a/core/src/num/complex.rs b/core/src/num/complex.rs index 68462596..26e3329c 100644 --- a/core/src/num/complex.rs +++ b/core/src/num/complex.rs @@ -52,6 +52,13 @@ impl Complex { }) } + pub(crate) fn try_as_real(self) -> FResult { + if !self.imag.is_zero() { + return Err(FendError::ComplexToInteger); + } + Ok(self.real) + } + pub(crate) fn try_as_usize(self, int: &I) -> FResult { if !self.imag.is_zero() { return Err(FendError::ComplexToInteger); diff --git a/core/src/num/real.rs b/core/src/num/real.rs index a71be60f..292e5647 100644 --- a/core/src/num/real.rs +++ b/core/src/num/real.rs @@ -10,6 +10,7 @@ use std::ops::Neg; use std::{fmt, hash, io}; use super::bigrat; +use super::biguint::BigUint; #[derive(Clone)] pub(crate) struct Real { @@ -102,6 +103,19 @@ impl Real { } } + pub(crate) fn try_as_biguint(self, int: &I) -> FResult { + match self.pattern { + Pattern::Simple(s) => s.try_as_biguint(int), + Pattern::Pi(n) => { + if n == 0.into() { + Ok(BigUint::Small(0)) + } else { + Err(FendError::CannotConvertToInteger) + } + } + } + } + pub(crate) fn try_as_i64(self, int: &I) -> FResult { match self.pattern { Pattern::Simple(s) => s.try_as_i64(int), diff --git a/core/src/num/unit.rs b/core/src/num/unit.rs index 5d661607..f066e988 100644 --- a/core/src/num/unit.rs +++ b/core/src/num/unit.rs @@ -470,7 +470,7 @@ impl Value { self.convert_to(Self::unitless(), int) } - fn into_unitless_complex(mut self, int: &I) -> FResult { + pub(crate) fn into_unitless_complex(mut self, int: &I) -> FResult { self = self.remove_unit_scaling(int)?; if !self.is_unitless(int)? { return Err(FendError::ExpectedAUnitlessNumber); diff --git a/core/tests/integration_tests.rs b/core/tests/integration_tests.rs index 9a405591..52206c10 100644 --- a/core/tests/integration_tests.rs +++ b/core/tests/integration_tests.rs @@ -6005,3 +6005,31 @@ fn uppercase_identifiers() { expect_error("foo = 1; FOO", Some("unknown identifier 'FOO'")); } + +#[test] +fn test_words() { + test_eval_simple("1 to words", "one"); + test_eval_simple("9 to words", "nine"); + test_eval_simple("15 to words", "fifteen"); + test_eval_simple("20 to words", "twenty"); + test_eval_simple("99 to words", "ninety-nine"); + test_eval_simple("154 to words", "one hundred and fifty-four"); + test_eval_simple("500 to words", "five hundred"); + test_eval_simple("999 to words", "nine hundred and ninety-nine"); + test_eval_simple("1000 to words", "one thousand"); + test_eval_simple( + "4321 to words", + "four thousand three hundred and twenty-one", + ); + test_eval_simple("1000000 to words", "one million"); + test_eval_simple( + "1234567 to words", + "one million two hundred and thirty-four thousand five hundred and sixty-seven", + ); + test_eval_simple("1000000000 to words", "one billion"); + test_eval_simple("9876543210 to words", "nine billion eight hundred and seventy-six million five hundred and forty-three thousand two hundred and ten"); + test_eval_simple("1000000000000 to words", "one trillion"); + test_eval_simple("1234567890123456 to words", "one quadrillion two hundred and thirty-four trillion five hundred and sixty-seven billion eight hundred and ninety million one hundred and twenty-three thousand four hundred and fifty-six"); + test_eval_simple("1000000000000000000000 to words", "one sextillion"); + test_eval_simple("1000000000000000000000000 to words", "one septillion"); +}