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:
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 thecommafy
function and then compare the resulting string with the expected string on the right hand side. We use theassert_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, exceptu128
. - 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 and even later I wrote an example using the thousands crate.