简体   繁体   中英

Is it possible to print a number formatted with thousand separator in Rust?

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 `,`

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

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.

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.connect(" ").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: http://is.gd/UigzCf

Comments welcome, none of them feels really nice.

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.

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) {
    ...
}

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());

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);
    }
}

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
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM