It's pretty simple to write code to do this manually. Here's some code which operates directly on strings to do it. This has the advantage of allowing you to use decimal place formatting using the normal methods (I don't think any of the existing options allow that). Also it avoids pulling in over-complicated dependencies for something so simple. Feel free to copy/paste (public domain):
/// Add thousands comma separators to a number. The number must match the following
/// regex: `^-?\d*(\.\d*)?$`. Returns None if it does not match that format.
/// Note that empty strings and just `-` are allowed.
pub fn with_comma_separators(s: &str) -> Option<String> {
// Position of the `.`
let dot = s.bytes().position(|c| c == b'.').unwrap_or(s.len());
// Is the number negative (starts with `-`)?
let negative = s.bytes().next() == Some(b'-');
// The dot cannot be at the front if it is negative.
assert!(!(negative && dot == 0));
// Number of integer digits remaning (between the `-` or start and the `.`).
let mut integer_digits_remaining = dot - negative as usize;
// Output. Add capacity for commas. It's a slight over-estimate but that's fine.
let mut out = String::with_capacity(s.len() + integer_digits_remaining / 3);
// We can iterate on bytes because everything must be ASCII. Slightly faster.
for (i, c) in s.bytes().enumerate() {
match c {
b'-' => {
// `-` can only occur at the start of the string.
if i != 0 {
return None;
}
}
b'.' => {
// Check we only have a dot at the expected position.
// This return may happen if there are multiple dots.
if i != dot {
return None;
}
}
b'0'..=b'9' => {
// Possibly add a comma.
if integer_digits_remaining > 0 {
// Don't add a comma at the start of the string.
if i != negative as usize && integer_digits_remaining % 3 == 0 {
out.push(',');
}
integer_digits_remaining -= 1;
}
}
_ => {
// No other characters allowed.
return None;
}
}
out.push(c as char);
}
Some(out)
}
#[cfg(test)]
mod test {
use super::with_comma_separators;
#[test]
fn basic() {
assert_eq!(with_comma_separators("123.45").as_deref(), Some("123.45"));
assert_eq!(
with_comma_separators("1234.56").as_deref(),
Some("1,234.56")
);
assert_eq!(with_comma_separators(".56").as_deref(), Some(".56"));
assert_eq!(with_comma_separators("56").as_deref(), Some("56"));
assert_eq!(with_comma_separators("567").as_deref(), Some("567"));
assert_eq!(with_comma_separators("5678").as_deref(), Some("5,678"));
assert_eq!(
with_comma_separators("12345678").as_deref(),
Some("12,345,678")
);
assert_eq!(with_comma_separators("5678.").as_deref(), Some("5,678."));
assert_eq!(with_comma_separators(".0123").as_deref(), Some(".0123"));
assert_eq!(with_comma_separators("-123.45").as_deref(), Some("-123.45"));
assert_eq!(
with_comma_separators("-1234.56").as_deref(),
Some("-1,234.56")
);
assert_eq!(with_comma_separators("-.56").as_deref(), Some("-.56"));
assert_eq!(with_comma_separators("-56").as_deref(), Some("-56"));
assert_eq!(with_comma_separators("-567").as_deref(), Some("-567"));
assert_eq!(with_comma_separators("-5678").as_deref(), Some("-5,678"));
assert_eq!(
with_comma_separators("-12345678").as_deref(),
Some("-12,345,678")
);
assert_eq!(with_comma_separators("-5678.").as_deref(), Some("-5,678."));
assert_eq!(with_comma_separators("-.0123").as_deref(), Some("-.0123"));
assert_eq!(with_comma_separators("").as_deref(), Some(""));
assert_eq!(with_comma_separators("-").as_deref(), Some("-"));
assert_eq!(with_comma_separators("a").as_deref(), None);
assert_eq!(with_comma_separators("0-").as_deref(), None);
assert_eq!(with_comma_separators("0..1").as_deref(), None);
assert_eq!(with_comma_separators("0..1").as_deref(), None);
assert_eq!(with_comma_separators("01a").as_deref(), None);
assert_eq!(with_comma_separators("01.a").as_deref(), None);
assert_eq!(with_comma_separators(".0.").as_deref(), None);
}
}