number_to_words/
lib.rs

1//!
2//! A function to convert a number to a string of words.
3//! ====================================================
4//!
5//! **Copyright (c) NexPro 2022**<br><br>
6//!  *Based on C# version by Jonathan Wood<br>
7//!   Copyright (c) 2019-2020 Jonathan Wood (www.softcircuits.com)*
8//!
9//! Licensed under the MIT license. see: <https://mit-license.org/>
10//! <br>
11//! **Liabilities**<br>
12//! <br>
13//! THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESSED OR IMPLIED,<br>
14//! INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,<br>
15//! FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.<br>
16//! IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,<br>
17//! DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,<br>
18//! ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
19//!<br>
20//!
21//! **Purpose:**
22//!
23//! Converts a number to a rust **std::string String** representation of the number in words
24//! with the part after the decimal point represented as **xx/100**
25//!
26//! Typical uses would be for cheque printing or remittance notices.
27//!
28//!
29//! **Examples:**<br>
30//! <br>
31//! Calling **number_to_words(99988389.123, true)** will return the String:
32//!     <br>    **Ninety-nine million, nine hundred eighty-eight thousand, three hundred eighty-nine and 12/100**<br>
33//!
34//! Calling **number_to_words(99988389.123, false)** will return the String:
35//!     <br>    **ninety-nine million, nine hundred eighty-eight thousand, three hundred eighty-nine and 12/100**<br>
36//!
37//! Calling **number_to_words(10.0, true)** will return the String:
38//!     <br> **Ten** *
39//!<br>
40//! <br>
41//! **Errors:**<br>
42//! <br>
43//! Numbers greater than 9_999_999_999_999.99 will return the String: **Number too large**
44/*
45    TODO add tests for handle_tens()
46*/
47
48static ONES: [&str; 10] = [
49    "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine",
50];
51
52static TEENS: [&str; 10] = [
53    "ten",
54    "eleven",
55    "twelve",
56    "thirteen",
57    "fourteen",
58    "fifteen",
59    "sixteen",
60    "seventeen",
61    "eighteen",
62    "nineteen",
63];
64
65static TENS: [&str; 10] = [
66    "", "ten", "twenty", "thirty", "forty", "fifty", "sixty", "seventy", "eighty", "ninety",
67];
68// US
69static THOUSANDS: [&str; 5] = ["", "thousand", "million", "billion", "trillion"];
70
71const ASCII_ZERO_OFFSET: u8 = 48;
72const LARGEST_ALLOWABLE_INPUT_VALUE: f64 = 9_999_999_999_999.99;
73
74pub fn number_to_words<T: std::convert::Into<f64>>(
75    number: T,
76    should_capitalise_first_word: bool,
77) -> String {
78    // Convert to f64 and ensure number is positive value
79    let number = num::abs(number.into());
80    if number > LARGEST_ALLOWABLE_INPUT_VALUE {
81        return "number too large".to_owned();
82    }
83    let formatted_num: String = round_and_format_number(number);
84    let split_number = split_on_decimal_point(formatted_num);
85    let mantissa = split_number[0].clone();
86    let mut cents = split_number[1].clone();
87    let mut all_zeros = true;
88    let mut should_skip_next_iteration = false;
89    let mut result: String = String::new();
90    let mut temp: String;
91
92    // Convert integer portion of value to string
93    let mut mantissa = mantissa.into_bytes();
94
95    // Convert digits to bytes so we can simply compare ints
96    for _digit in mantissa.iter_mut() {
97        *_digit -= ASCII_ZERO_OFFSET;
98    }
99    // Reverse iterate over digits in order to build our output string
100    for i in (0..mantissa.len()).rev() {
101        if should_skip_next_iteration {
102            should_skip_next_iteration = false;
103            continue;
104        }
105        let next_digit = mantissa[i];
106        let column = mantissa.len() - (i + 1);
107
108        // Determine if digit is in the ones, tens or hundreds column
109        match column % 3 {
110            0 => {
111                // Ones
112                let mut show_thousands = true;
113                if i == 0 {
114                    temp = ONES[next_digit as usize].to_string() + " ";
115                } else if mantissa[i - 1] == 1 {
116                    // This digit is part of "teen" value
117                    temp = TEENS[next_digit as usize].to_owned() + " ";
118                    // Skip tens position
119                    should_skip_next_iteration = true;
120                } else if next_digit != 0 {
121                    // Any non-zero digit
122                    temp = ONES[next_digit as usize].to_owned() + " ";
123                } else {
124                    // This digit is zero. If digits in tens and hundreds
125                    // column are also zero, don't show "thousands"
126                    temp = String::new();
127                    show_thousands = mantissa[i - 1] != 0 || (i > 1 && mantissa[i - 2] != 0);
128                }
129                // Show "thousands" if non-zero in grouping
130                if show_thousands {
131                    if column > 0 {
132                        temp = temp
133                            + &(THOUSANDS[column / 3].to_owned()
134                                + if all_zeros { " " } else { ", " });
135                    }
136                    // Non-zero digit found
137                    all_zeros = false;
138                }
139                result = (temp.clone() + &result).to_owned();
140            }
141            1 => {
142                // Tens
143                result = handle_tens(next_digit.into(), i, mantissa.clone()) + &result;
144            }
145            2 => {
146                // Hundreds
147                if next_digit > 0 {
148                    temp = ONES[next_digit as usize].to_owned() + " hundred ";
149                    result = temp + &result;
150                }
151            }
152            _ => {
153                // Default case. Do nothing?
154            }
155        }
156    }
157
158    if should_capitalise_first_word {
159        result = capitalise_first_letter(result);
160    }
161    // Remove leading zero from cents if present
162    if cents.starts_with('0') {
163        cents.remove(0);
164    }
165    // Append cents
166    if cents == "0" {
167        // Remove trailing space
168        result.pop();
169        result
170    } else {
171        result + "and " + &cents + "/100"
172    }
173}
174
175fn round_and_format_number(num: f64) -> String {
176    format!("{:.2}", f64::round(num * 100.0) / 100.0)
177}
178
179fn handle_tens(next: usize, idx: usize, mantissa: Vec<u8>) -> String {
180    if next > 0 {
181        return TENS[next].to_owned() + (if mantissa[idx + 1] != 0 { "-" } else { " " });
182    }
183    String::new()
184}
185
186fn split_on_decimal_point(number: String) -> [String; 2] {
187    let mut v: [String; 2] = [String::new(), String::new()];
188    number
189        .split('.')
190        .into_iter()
191        .enumerate()
192        .for_each(|(idx, n)| v[idx] = n.to_owned());
193    v
194}
195
196fn capitalise_first_letter(mut word: String) -> String {
197    if word.is_empty() {
198        return "".to_owned();
199    }
200    word.remove(0).to_uppercase().to_string() + &word
201}
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use rstest::*;
206
207    // Tests for round_and_format_number()
208    #[rstest]
209    #[case(123.456, "123.46")] // Case1
210    #[case(123.4567, "123.46")]
211    #[case(123.4, "123.40")] // Case 3
212    #[case(123.056, "123.06")]
213    #[case(123.006, "123.01")] // Case 5
214    #[case(123.005, "123.01")]
215    #[case(123.004, "123.00")] // Case 7
216    #[case(123.9999, "124.00")]
217    #[case(9_999_999_999_999.99999, "10000000000000.00")]
218    fn test_round_and_format_number(#[case] input: f64, #[case] expected: &str) {
219        assert_eq!(round_and_format_number(input), expected);
220    }
221
222    #[rstest]
223    #[case(0.099, true, "Zero and 10/100")] // Case1
224    #[case(1.0, true, "One")] // Case 2
225    #[case(15.04, true, "Fifteen and 4/100")] // Case 3
226    #[case(99988389.123, true, // Case 4
227        "Ninety-nine million, \
228         nine hundred eighty-eight thousand, \
229         three hundred eighty-nine and 12/100"
230        )]
231    #[case(9308120381241.876, true,  // Case 5
232        "Nine trillion, \
233        three hundred eight billion, \
234        one hundred twenty million, \
235        three hundred eighty-one thousand, \
236        two hundred forty-one and 88/100"
237    )]
238    #[case(9890984381241.55, true, // Case 6
239        "Nine trillion, \
240        eight hundred ninety billion, \
241        nine hundred eighty-four million, \
242        three hundred eighty-one thousand, \
243        two hundred forty-one and 55/100"
244    )]
245    #[case(9_999_999_999_999.0100, // Case 7
246        true,
247        "Nine trillion, \
248        nine hundred ninety-nine billion, \
249        nine hundred ninety-nine million, \
250        nine hundred ninety-nine thousand, \
251        nine hundred ninety-nine and 1/100"
252    )]
253    #[case(999_999_999_999.9999, true, "One trillion")] // Case 8
254    #[case(9_999_999_999_999.09999, true, // Case 9
255        "Nine trillion, nine hundred ninety-nine billion, \
256        nine hundred ninety-nine million, \
257        nine hundred ninety-nine thousand, \
258        nine hundred ninety-nine and 10/100"
259    )]
260    #[case(9_999_999_999_999.989, true, // Case 10
261        "Nine trillion, nine hundred ninety-nine billion, \
262        nine hundred ninety-nine million, \
263        nine hundred ninety-nine thousand, \
264        nine hundred ninety-nine and 99/100"
265    )]
266    #[case(9_999_999_999_999.99, true, // Case 11
267        "Nine trillion, \
268        nine hundred ninety-nine billion, \
269        nine hundred ninety-nine million, \
270        nine hundred ninety-nine thousand, \
271        nine hundred ninety-nine and 99/100"
272    )]
273    #[case(9_999_999_999_999.9999, true, // Case 12
274        "number too large"
275    )]
276    #[case(0.999_999_999_999_999_999_999_999_999_999_999_999_999_999_999_999_999, true, // Case 13
277        "One"
278    )]
279    #[case("222.22", true, "Two hundred twenty-two and 22/100")]
280    #[case("1.1e+6", true, "One million, one hundred thousand")]
281    #[case("-1.1e+6", true, "One million, one hundred thousand")]
282    #[case("10.0e+6", true, "Ten million")]
283
284    fn test_float_inputs(#[case] input: f64, #[case] capitalise: bool, #[case] expected: &str) {
285        assert_eq!(number_to_words(input, capitalise), expected);
286    }
287
288    #[rstest]
289    #[case(1, false, "one")]
290    #[case(15, false, "fifteen")]
291    #[case(1266, false, "one thousand, two hundred sixty-six")]
292    #[case(
293        1230812,
294        false,
295        "one million, \
296        two hundred thirty thousand, \
297        eight hundred twelve"
298    )]
299    #[case(
300        99988389,
301        false,
302        "ninety-nine million, \
303        nine hundred eighty-eight thousand, \
304        three hundred eighty-nine"
305    )]
306    fn test_signed_integer_inputs(
307        #[case] input: i32,
308        #[case] capitalise: bool,
309        #[case] expected: &str,
310    ) {
311        assert_eq!(number_to_words(input, capitalise), expected);
312    }
313
314    #[rstest]
315    #[case(1, true, "One")]
316    #[case(15, true, "Fifteen")]
317    #[case(
318        1266,
319        true,
320        "One thousand, \
321        two hundred sixty-six"
322    )]
323    #[case(
324        1230812,
325        true,
326        "One million, \
327        two hundred thirty thousand, \
328        eight hundred twelve"
329    )]
330    #[case(
331        99988389,
332        true,
333        "Ninety-nine million, \
334        nine hundred eighty-eight thousand, \
335        three hundred eighty-nine"
336    )]
337    
338    fn test_unsigned_integer_inputs(
339        #[case] input: u32,
340        #[case] capitalise: bool,
341        #[case] expected: &str,
342    ) {
343        assert_eq!(number_to_words(input, capitalise), expected);
344    }
345
346    // Tests for split_on_decimal_point()
347    #[rstest]
348    #[case("0.0", ["0", "0"])]
349    #[case("0.00", ["0", "00"])]
350    #[case("1.0", ["1", "0"])]
351    #[case("1.1", ["1", "1"])]
352    #[case("99.999", ["99", "999"])] // Case 5
353    #[case("000.0", ["000", "0"])]
354    #[case("9999999999.99", ["9999999999", "99"])]
355    #[case("1.", ["1", ""])]
356    #[case(".", ["", ""])]
357    #[case("", ["", ""])] // Case 10
358    fn splitting_test(#[case] input: String, #[case] expected: [&str; 2]) {
359        assert_eq!(split_on_decimal_point(input), expected);
360    }
361
362    // Tests for capitalise_first_word()
363    #[rstest]
364    #[case("one and", "One and")]
365    #[case("fifteen and 4/100", "Fifteen and 4/100")]
366    #[case("ninety", "Ninety")]
367    #[case("12345", "12345")]
368    #[case("tWELVE", "TWELVE")]
369    #[case("$banana", "$banana")]
370    #[case("", "")]
371
372    fn test_capitalisation(#[case] input: String, #[case] expected: String) {
373        assert_eq!(capitalise_first_letter(input), expected);
374    }
375}