Choose thousands and decimal separators for f64 in Rust
Asked Answered
L

0

2

In the case of Brazil, the thousands separator is '.' and the decimal separator is ','.

Is there any more efficient way using just the standard Rust library?

I'm currently using the following functions:

thousands_separator with decimal >= 1:

pub fn thousands_separator(value: f64, decimal: usize) -> String {

    let abs_value: f64 = value.abs(); // absolute value
    let round:  String = format!("{abs_value:0.decimal$}");

    // integer and fractional part of f64 numbers,
    let integer:  &str = &round[..(round.len() - decimal - 1)];
    let fraction: &str = &round[(round.len() - decimal)..];

    let decimal_sep: &str = "," ;
    let thousands_sep: char = '.';

    let integer_splitted: String = split_and_insert(integer, thousands_sep);

    if value.is_sign_negative() {
        "-".to_string() + &integer_splitted + decimal_sep + fraction
    } else {
        integer_splitted + decimal_sep + fraction
    }
}

Such that fn split_and_insert():

fn split_and_insert(integer: &str, insert: char) -> String {

    let group_size = 3;

    let string_splitted: String = integer
        .chars()
        .enumerate()
        .flat_map(|(i, c)| {
            if (integer.len() - i) % group_size == 0 && i > 0 {
                Some(insert)
            } else {
                None
            }
            .into_iter()
            .chain(std::iter::once(c))
        })
        .collect::<String>();

    string_splitted
}

For this main() function:

fn main() {

    let tuples: Vec<(f64, usize)> = vec![
        (-2987954368.369177, 2),
        (123.4, 3),
        (1234.5, 3),
        (1234.5, 1),
        (12345.54321, 8),
        (-0.15, 4),
        (1234566.996, 2),
    ];

    let result: Vec<String> = tuples
        .iter()
        .map(|(value, decimal)| thousands_separator(*value, *decimal))
        .collect();

    let valid = vec![
        "-2.987.954.368,37",
        "123,400",
        "1.234,500",
        "1.234,5",
        "12.345,54321000",
        "-0,1500",
        "1.234.567,00",
    ];

    for ((n, d), r) in tuples.iter().zip(&result) {
        println!("value: {n:20}, decimal: {d} => {r:#?}");
    }

    assert_eq!(valid, result);
}

We get the result:

value:   -2987954368.369177, decimal: 2 => "-2.987.954.368,37"
value:                123.4, decimal: 3 => "123,400"
value:               1234.5, decimal: 3 => "1.234,500"
value:               1234.5, decimal: 1 => "1.234,5"
value:          12345.54321, decimal: 8 => "12.345,54321000"
value:                -0.15, decimal: 4 => "-0,1500"
value:          1234566.996, decimal: 2 => "1.234.567,00"

The Rust Playground

Another version with same results:

/// <https://mcmap.net/q/244517/-is-it-possible-to-print-a-number-formatted-with-thousand-separator-in-rust>
pub fn thousands_separator_v2(value: f64, decimal: usize) -> String {

    let round: String = format!("{value:0.decimal$}");

    let decimal_sep: u8 = b',' ;
    let thousands_sep: char = '.';
    let group_size: usize = 3;

    // Position of the `.`
    let dot_position: usize = round.bytes().position(|c| c == b'.').unwrap_or(round.len());
    // Is the number negative (starts with `-`)?
    let negative: bool = value.is_sign_negative();
    // Number of integer digits remaning (between the `-` or start and the `.`).
    let mut integer_digits_remaining = dot_position - usize::from(negative);
    // Output. Add capacity for commas. It's a slight over-estimate but that's fine.
    let mut formatted = String::with_capacity(round.len() + integer_digits_remaining / group_size);

    // We can iterate on bytes because everything must be ASCII. Slightly faster.
    for (index, mut byte) in round.bytes().enumerate() {
        match byte {
            b'.' => {
                // Change the decimal separator from '.' for decimal_sep
                byte = decimal_sep;
            },
            b'0' ..= b'9' => {
                // Possibly add a thousands_sep.
                if integer_digits_remaining > 0 {
                    // Don't add a thousands_sep at the start of the string.
                    // usize::from(negative); // if negative { 1 } else { 0 }
                    if index != usize::from(negative) && integer_digits_remaining % group_size == 0 {
                        formatted.push(thousands_sep);
                    }
                    integer_digits_remaining -= 1;
                }
            },
            _ => (),
        }
        formatted.push(byte as char);
    }

    formatted
}
Leapfrog answered 6/7, 2022 at 18:52 Comment(3)
As far as I know, there is no standard library function for this. I'd suggest using docs.rs/num-formatRent
If you're looking for ways to improve your existing code, I'd suggest submitting to Rust code review insteadRent
@Rent your suggestion does not answer this question. OP seems want to do implement it without depending any crate.Upgrade

© 2022 - 2025 — McMap. All rights reserved.