50

For instance

println!("{}", 10_000_000);

results in

10000000

whereas I'd like to format it to look something like

10,000,000

I went through the fmt module documentation, but there's nothing to cover this particular situation. I thought something like this would work

println!("{:,i}", 10_000_000);

but it throws an error

invalid format string: expected `}`, found `,`
Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Caballero
  • 11,546
  • 22
  • 103
  • 163
  • 3
    I don't think that's possible without writing custom code right now. – Dogbert Nov 18 '14 at 16:05
  • 4
    Why `10,000,000` and not `1,00,00,000`? How would you indicate how to group digits? – Matthieu M. Nov 18 '14 at 16:40
  • 2
    @MatthieuM. Thousand separator for readability. Most if not all the languages have this functionality, so I was wondering if Rust does too. – Caballero Nov 18 '14 at 16:48
  • 3
    @Caballero: the thing is, different cultures separate differently; the second format is how Indians do it. – Matthieu M. Nov 18 '14 at 16:55
  • 2
    @MatthieuM. I didn't realize that there are different notations. I was referring to more or less SI standard - http://en.wikipedia.org/wiki/Decimal_mark#Examples_of_use – Caballero Nov 18 '14 at 17:01

8 Answers8

19

The num_format crate will solve this issue for you. Add your locale and it will do the magic.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
Tom
  • 2,688
  • 3
  • 29
  • 53
12

the simplest way to format a number with thousands separator – but w/o Locale
use the thousands crate

use thousands::Separable;

println!("{}", 10_000_000.separate_with_commas());

a one-liner
using only the Rust std library

let num = 10_000_000.to_string()
    .as_bytes()
    .rchunks(3)
    .rev()
    .map(std::str::from_utf8)
    .collect::<Result<Vec<&str>, _>>()
    .unwrap()
    .join(",");  // separator

Playground

stites
  • 4,903
  • 5
  • 32
  • 43
Kaplan
  • 2,572
  • 13
  • 14
5

There isn't, and there probably won't be.

Depending on where you are, the thousands separator may also work like 1,00,00,000, or 1.000.000,000 or some other variant.

Localization isn't the job of the stdlib, plus format! is mostly handled at compile time (though to be fair this could be placed in its runtime portion easily), and you don't want to hard-bake a locale into the program.

Manishearth
  • 14,882
  • 8
  • 59
  • 76
  • 25
    “Localization isn’t the job of the stdlib” is severely debatable. I certainly disagree with it, though I will say that it isn’t *yet* suitable to have it as part of the standard libraries. – Chris Morgan Nov 19 '14 at 07:36
  • That is assuming that your culture group digits by 3. – Aron Jan 11 '16 at 04:41
  • 4
    The number of digits per group and the separator could be specified. E.g. groups of 4 separated by _ would make a nice output format for binary or hexadecimal digits. (As far as I could find out Italian also uses groups of three digits, not the irregular format mentioned above.) – starblue Jan 11 '16 at 12:37
  • 5
    @starblue The "irregular format" is used in India and in other places in South Asia. It's not a niche thing. – Manishearth Jan 11 '16 at 16:26
  • 16
    Nonsense. There should be a format!() string which says "add this seperator every x digits" the same way we can tell format!() to add a decimal point and how many decimal places to show and how many zeroes to pad. – Tom Apr 03 '19 at 11:37
3

Here is a naive implementation for integers

fn pretty_print_int(i: isize) {
    let mut s = String::new();
    let i_str = i.to_string();
    let a = i_str.chars().rev().enumerate();
    for (idx, val) in a {
        if idx != 0 && idx % 3 == 0 {
            s.insert(0, ',');
        }
        s.insert(0, val);
    }
    println!("{}", s);
}

pretty_print_int(10_000_000);
// 10,000,000

If you want to make this a little more generic for integers you could use the num::Integer trait

extern crate num;

use num::Integer;

fn pretty_print_int<T: Integer>(i: T) {
    ...
}
Michael Hall
  • 2,834
  • 1
  • 22
  • 40
1

Regarding a custom function, I played around with this and here are some ideas:

use std::str;

fn main() {
    let i = 10_000_000i;
    println!("{}", decimal_mark1(i.to_string()));
    println!("{}", decimal_mark2(i.to_string()));
    println!("{}", decimal_mark3(i.to_string()));
}

fn decimal_mark1(s: String) -> String {
    let bytes: Vec<_> = s.bytes().rev().collect();
    let chunks: Vec<_> = bytes.chunks(3).map(|chunk| str::from_utf8(chunk).unwrap()).collect();
    let result: Vec<_> = chunks.join(" ").bytes().rev().collect();
    String::from_utf8(result).unwrap()
}

fn decimal_mark2(s: String) -> String {
    let mut result = String::with_capacity(s.len() + ((s.len() - 1) / 3));
    let mut i = s.len();
    for c in s.chars() {
        result.push(c);
        i -= 1;
        if i > 0 && i % 3 == 0 {
            result.push(' ');
        }
    }
    result
}

fn decimal_mark3(s: String) -> String {
    let mut result = String::with_capacity(s.len() + ((s.len() - 1) / 3));
    let first = s.len() % 3;
    result.push_str(s.slice_to(first));
    for chunk in s.slice_from(first).as_bytes().chunks(3) {
        if !result.is_empty() {
            result.push(' ');
        }
        result.push_str(str::from_utf8(chunk).unwrap());
    }
    result
}

Playpen

Comments welcome, none of them feels really nice.

simanacci
  • 2,197
  • 3
  • 26
  • 35
robinst
  • 30,027
  • 10
  • 102
  • 108
1

Another workaround for this is to use the separator crate which implements a .separated_string() method on float, integer and size types. Here is an example:

extern crate separator;
use separator::Separatable;

fn main() {
    let x1: u16 = 12345;
    let x2: u64 = 4242424242;
    let x3: u64 = 232323232323;
    println!("Unsigned ints:\n{:>20}\n{:>20}\n{:>20}\n", x1.separated_string(), x2.separated_string(), x3.separated_string());

    let x1: i16 = -12345;
    let x2: i64 = -4242424242;
    let x3: i64 = -232323232323;
    println!("Signed ints:\n{:>20}\n{:>20}\n{:>20}\n", x1.separated_string(), x2.separated_string(), x3.separated_string());


    let x1: f32 = -424242.4242;
    let x2: f64 = 23232323.2323;
    println!("Floats:\n{:>20}\n{:>20}\n", x1.separated_string(), x2.separated_string());


    let x1: usize = 424242;
    // let x2: isize = -2323232323;  // Even though the docs say so, the traits seem not to be implemented for isize
    println!("Size types:\n{:>20}\n", x1.separated_string());        
}

Which gives you the following output:

Unsigned ints:
              12,345
       4,242,424,242
     232,323,232,323

Signed ints:
             -12,345
      -4,242,424,242
    -232,323,232,323

Floats:
         -424,242.44
     23,232,323.2323

Size types:
             424,242

Note that aligning floats like this is not easy since separated_string() returns a string. However, this is a relatively quick way to get separated numbers.

squiguy
  • 32,370
  • 6
  • 56
  • 63
m00am
  • 5,910
  • 11
  • 53
  • 69
1

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);
    }
}
Timmmm
  • 88,195
  • 71
  • 364
  • 509
  • I tried using this function with modifications. See the [link](https://stackoverflow.com/questions/72888582/choose-thousands-and-decimal-separators-for-f64-in-rust). Good results. – Claudio Fsr Jul 21 '23 at 17:31
0

If you don't immediately need Strings, and perhaps need variable grouping, you might want to consider an iterator-based approach.

fn thsep(digits: &str, n: usize) -> impl Iterator<Item = &str> {
    let (chars, tip) = (digits.as_bytes(), digits.len() % n);
    if tip != 0 { Some(&chars[..tip]) } else { None }
        .into_iter()
        .chain(chars[tip..].chunks(n))
        .map(|digits| {
            std::str::from_utf8(digits).expect("unexpected non-utf8 char encountered")
        })
}

fn join(i: impl Iterator<Item = &'static str>) -> String {
    i.collect::<Vec<_>>().join(",")
}

fn main() {
    let val = "1234567890";
    println!("{}", join(thsep(val, 1))); // 1,2,3,4,5,6,7,8,9,0
    println!("{}", join(thsep(val, 2))); // 12,34,56,78,90
    println!("{}", join(thsep(val, 3))); // 1,234,567,890 • 3
    println!("{}", join(thsep(val, 4))); // 12,3456,7890
}
Miraclx
  • 371
  • 3
  • 8