Commafy - add a comma after every 3 digits

commafy assert_eq! into abs thousands

A number like this: 1234567890 is quite unreadable 1,234,567,890 is much better.

Converting a number to this more readable form is sometimes called commafy. Here is an implementation:

examples/commafy/src/main.rs

fn main() {
    let number = get_command_line_number();
    println!("number: {}", number);
    println!("commafied: {}", commafy(number));
}

// fn commafy<Integer: Into<i128> + Copy + std::fmt::Debug + std::fmt::Display>(
//     number: Integer,
// ) -> String {
//     let num = number.into().abs();

//     let num = num.to_string();

//     let mut ix = 0;

//     let num = num
//         .chars()
//         .rev()
//         .map(|chr| {
//             ix += 1;
//             if ix % 3 == 1 && ix > 1 {
//                 format!(",{chr}")
//             } else {
//                 format!("{chr}")
//             }
//         })
//         .collect::<String>();

//     let prefix = if number.into() < 0 { "-" } else { "" };

//     format!("{}{}", prefix, num.chars().rev().collect::<String>())
// }

fn commafy<Integer: Into<i128> + Copy + std::fmt::Debug + std::fmt::Display>(
    number: Integer,
) -> String {
    let prefix = if number.into() < 0 { "-" } else { "" };
    format!(
        "{}{}",
        prefix,
        number
            .into()
            .abs()
            .to_string()
            .as_bytes()
            .rchunks(3)
            .rev()
            .map(std::str::from_utf8)
            .collect::<Result<Vec<&str>, _>>()
            .unwrap()
            .join(",")
    )
}

fn get_command_line_number() -> i32 {
    let argv: Vec<String> = std::env::args().collect();

    if argv.len() != 2 {
        eprintln!("Usage: {} value", argv[0]);
        std::process::exit(1);
    }

    let value: i32 = match argv[1].parse() {
        Ok(val) => val,
        Err(err) => {
            eprintln!("Could not convert '{}' to i32. {}", argv[1], err);
            std::process::exit(1);
        }
    };

    value
}

#[test]
fn test_commafy() {
    assert_eq!(commafy(0), "0");
    assert_eq!(commafy(23), "23");
    assert_eq!(commafy(123), "123");
    assert_eq!(commafy(-123), "-123");
    assert_eq!(commafy(1234), "1,234");
    assert_eq!(commafy(-23), "-23");
    assert_eq!(commafy(-1234), "-1,234");

    assert_eq!(commafy(23i128), "23");
    assert_eq!(commafy(23u64), "23");
    //assert_eq!(commafy(23u128), "23");
}

  • We have function called get_command_line_number taken from the example expecting one command line parameter.

  • We also have a main function to make it easier to try the code on the command line.

  • We also have a bunch of tests at the bottom of the file. We can run the tests by typing in cargo test. In each test-case we call the commafy function and then compare the resulting string with the expected string on the right hand side. We use the assert_eq! functions for this.

  • The real code is in the commafy function.

Explanation

number: Integer

means the function is expecting a single parameter of type Integer. This name refers to any type that fulfills the requirements in the declaration of the type right after the name of the function:

Integer: Into<i128> + Copy + std::fmt::Debug + std::fmt::Display

This means that the newly created Integer type:

  • Must be convertible to i128 without loss. Basically every integer type and unsigned-integer type fulfills this, except u128.
  • Has to have the Copy trait.
  • Has the Debug trait.
  • And the Display trait.

The function will return a String.

We only want to work with non-negative numbers, so we work with the absolute value, but the Integer type we created does not have the abs methods so first we need to call the into method to convert the value to i128.

let num = number.into().abs();

We actually want to work with characters, disregarding the numerical values so we convert the number into a string.

let num = format!("{num}");

It would be hard to add a comma after every 3 character from the right side (the back) of the string, so we split it up into individual characters (using the chars method, then reverse the characters using the rev method.

Then using map we go over each character and add a comma (,) after each 3rd character. Actually, we add a comma in-front the character whose index is 1 modulo 3 (ix % 3 == 1). This way we can make sure that we convert 123456 to 123,456 and not to ,123,456.

The we call collect and provide the type using the turbofish syntax: ```::`.

The result is a string with all the commas.

The prefix is the - for the negative numbers. It is really nice of Rust that if returns the expression in it.

let prefix = if number.into() < 0 { "-" } else { "" };

Finally we need to reverse the string again to get in the correct direction.

format!("{}{}", prefix, num.chars().rev().collect::<String>())

The thousands crate

After implementing this someone pointed me to the thousands crate that does this already.

Author

Gabor Szabo (szabgab)

Gabor Szabo, the author of the Rust Maven web site maintains several Open source projects in Rust and while he still feels he has tons of new things to learn about Rust he already offers training courses in Rust and still teaches Python, Perl, git, GitHub, GitLab, CI, and testing.

Gabor Szabo