Functional Weave
Code in TypeScript

text.integer-to-words@1.0.0

impl/rust.rs

3,192 bytes · the Rust implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one

/// Largest integer a JavaScript number holds exactly; beyond it the three
/// languages disagree about what number was passed.
const MAX_MAGNITUDE: i64 = 9007199254740991;

const ONES: [&str; 20] = [
    "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
    "ten", "eleven", "twelve", "thirteen", "fourteen", "fifteen", "sixteen",
    "seventeen", "eighteen", "nineteen",
];
const TENS: [&str; 10] = ["", "", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety"];

/// Short scale, as used in the UK since 1974: a billion is 10^9.
const SCALES: [&str; 6] = ["", "thousand", "million", "billion", "trillion", "quadrillion"];

fn below_hundred(n: i64) -> String {
    if n < 20 {
        return ONES[n as usize].to_string();
    }
    let unit = n % 10;
    if unit == 0 {
        TENS[(n / 10) as usize].to_string()
    } else {
        format!("{}-{}", TENS[(n / 10) as usize], ONES[unit as usize])
    }
}

/// One group of three digits, 1 to 999.
fn group(n: i64) -> String {
    let hundreds = n / 100;
    let rest = n % 100;
    if hundreds == 0 {
        return below_hundred(rest);
    }
    let head = format!("{} hundred", ONES[hundreds as usize]);
    if rest == 0 {
        head
    } else {
        format!("{} and {}", head, below_hundred(rest))
    }
}

/// Spell out a whole number in British English words, cheque style:
/// 1234 is "one thousand two hundred and thirty-four".
///
/// # Panics
/// Panics if the magnitude is above 9007199254740991.
pub fn integer_to_words(value: i64) -> String {
    if value > MAX_MAGNITUDE || value < -MAX_MAGNITUDE {
        panic!(
            "{} is out of range: integerToWords handles magnitudes up to {}",
            value, MAX_MAGNITUDE
        );
    }
    if value == 0 {
        return "zero".to_string();
    }

    // Groups of three, least significant first.
    let mut groups: Vec<i64> = Vec::new();
    let mut remaining = value.abs();
    while remaining > 0 {
        groups.push(remaining % 1000);
        remaining /= 1000;
    }

    let mut words: Vec<String> = Vec::new();
    for i in (0..groups.len()).rev() {
        let g = groups[i];
        if g == 0 {
            continue;
        }
        let mut text = group(g);
        if i > 0 {
            text = format!("{} {}", text, SCALES[i]);
        } else if groups.len() > 1 && g < 100 {
            // British usage: "one thousand and one", "one million and fifty".
            // Only the final group gets it, and only without hundreds of its own.
            text = format!("and {}", text);
        }
        words.push(text);
    }

    let body = words.join(" ");
    if value < 0 {
        format!("minus {}", body)
    } else {
        body
    }
}

pub fn fune_vector(args: &[Value]) -> Value {
    // Refuse what the typed signature cannot hold, with the wording TypeScript
    // and Python use, rather than let the conversion below quietly change it.
    if let Value::Float(f) = args[0] {
        if f.fract() != 0.0 {
            panic!("integerToWords needs a whole number, received {}", f);
        }
    }
    Value::Str(integer_to_words(args[0].as_i64()))
}