Skip to content

Commit

Permalink
Add to words conversion
Browse files Browse the repository at this point in the history
  • Loading branch information
printfn committed Sep 15, 2024
1 parent 30333d7 commit a413580
Show file tree
Hide file tree
Showing 7 changed files with 210 additions and 1 deletion.
9 changes: 9 additions & 0 deletions core/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,7 @@ fn evaluate_add<I: Interrupt>(
})
}

#[allow(clippy::too_many_lines)]
fn evaluate_as<I: Interrupt>(
a: Expr,
b: Expr,
Expand Down Expand Up @@ -641,6 +642,14 @@ fn evaluate_as<I: Interrupt>(
}
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)?)));
}
_ => (),
}
}
Expand Down
11 changes: 11 additions & 0 deletions core/src/num/bigrat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,17 @@ impl BigRat {
self.den == 1.into()
}

pub(crate) fn try_as_biguint<I: Interrupt>(mut self, int: &I) -> FResult<BigUint> {
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<I: Interrupt>(mut self, int: &I) -> FResult<usize> {
if self.sign == Sign::Negative && self.num != 0.into() {
return Err(FendError::NegativeNumbersNotAllowed);
Expand Down
140 changes: 140 additions & 0 deletions core/src/num/biguint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,128 @@ impl BigUint {
}
Ok(self)
}

pub(crate) fn to_words<I: Interrupt>(&self, int: &I) -> FResult<String> {
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::<usize>().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 {
Expand Down Expand Up @@ -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(())
}
}
7 changes: 7 additions & 0 deletions core/src/num/complex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ impl Complex {
})
}

pub(crate) fn try_as_real(self) -> FResult<Real> {
if !self.imag.is_zero() {
return Err(FendError::ComplexToInteger);
}
Ok(self.real)
}

pub(crate) fn try_as_usize<I: Interrupt>(self, int: &I) -> FResult<usize> {
if !self.imag.is_zero() {
return Err(FendError::ComplexToInteger);
Expand Down
14 changes: 14 additions & 0 deletions core/src/num/real.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -102,6 +103,19 @@ impl Real {
}
}

pub(crate) fn try_as_biguint<I: Interrupt>(self, int: &I) -> FResult<BigUint> {
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<I: Interrupt>(self, int: &I) -> FResult<i64> {
match self.pattern {
Pattern::Simple(s) => s.try_as_i64(int),
Expand Down
2 changes: 1 addition & 1 deletion core/src/num/unit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ impl Value {
self.convert_to(Self::unitless(), int)
}

fn into_unitless_complex<I: Interrupt>(mut self, int: &I) -> FResult<Complex> {
pub(crate) fn into_unitless_complex<I: Interrupt>(mut self, int: &I) -> FResult<Complex> {
self = self.remove_unit_scaling(int)?;
if !self.is_unitless(int)? {
return Err(FendError::ExpectedAUnitlessNumber);
Expand Down
28 changes: 28 additions & 0 deletions core/tests/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}

0 comments on commit a413580

Please sign in to comment.