Crate numfmt

source ·
Expand description

Fast and friendly number formatting.

Provides a Formatter to format decimal numbers with various methods. Formatting is performance focused, it is generally faster than std with more features. There is also a string parser which can use a string to define a Formatter following a specific grammar.

Procedure

Formatting is done through the Formatter::fmt which follows the procedure:

  1. Scale the number with the defined Scales,
  2. Check if scaled number is above or below the scientific notation cutoffs,
  3. Add defined thousands separator,
  4. Stop at defined Precision,
  5. Applies valid prefix, suffix, and unit decorations.

Usage

Default use

Default::default provides a general use default formatter with the following properties:

let mut f = Formatter::default();
assert_eq!(f.fmt(0.0), "0");
assert_eq!(f.fmt(12345.6789), "12.345 K");
assert_eq!(f.fmt(0.00012345), "1.234e-4");
assert_eq!(f.fmt(123456e22), "1,234.559 Y");

Custom use

The Formatter has many different options to customise how the number should be formatted. The example below shows how a currency format would be developed:

let mut f = Formatter::new() // start with blank representation
    .separator(',').unwrap()
    .prefix("AU$").unwrap()
    .precision(Precision::Decimals(2));

assert_eq!(f.fmt(0.52), "AU$0.52");
assert_eq!(f.fmt(1234.567), "AU$1,234.56");
assert_eq!(f.fmt(12345678900.0), "AU$12,345,678,900.0");

Scientific Notation

Scientific notation kicks in when the scaled number is greater than 12 integer digits (123,456,789,000) or less than 3 leading zeros (0.0001234). The number always has a leading integer digit and has a default of 7 significant figures.

Precision

Precision, either with number of decimals or significant figures can be specified with Precision.

let mut f = Formatter::new();
assert_eq!(f.fmt(1234.56789), "1234.56789");

f = f.precision(Precision::Decimals(2));
assert_eq!(f.fmt(1234.56789), "1234.56");

f = f.precision(Precision::Significance(5));
assert_eq!(f.fmt(1234.56789), "1234.5");

Performance

Formatting is generally faster than std’s f64::to_string implementation. When constructing a Formatter there is an allocation for the buffer, and an allocation for any scales. Reusing a Formatter is recommended to avoid unnecessary allocations. The cached row shows the better performance reusing a formatter.

Time (ns)0.00.12342.7182818284590451.797693148623157e307
numfmt - default35115153195
numfmt - cached27589126
std3596105214

Example - File size formatter

Using a combination of a scale, suffix, and precision, a file size printer can be constructed:

let mut f = Formatter::new()
                .scales(Scales::binary())
                .precision(Precision::Significance(3))
                .suffix("B").unwrap();

assert_eq!(f.fmt(123_f64), "123 B");
assert_eq!(f.fmt(1234_f64), "1.20 kiB");
assert_eq!(f.fmt(1_048_576_f64), "1.0 MiB");
assert_eq!(f.fmt(123456789876543_f64), "112 TiB");

Parsing

A grammar is defined that can parse into a Formatter. This string representation can be used as a user input for formatting numbers. The grammar is defined by a prefix, the number format enclosed in brackets, and then the suffix.

prefix[[.#|,#|~#|.*|,*][%|s|b|n][/<char>]]suffix
^----^ ^--------------^^-------^^-------^ ^----^
prefix precision scale    separator suffix

Each component is optional, including the number format. All formats are applied to the default Formatter so an empty format results in the default formatter.

Prefix and Suffix

The prefix and suffix are bound to the supported lengths, and can have any character in them. To use [] characters, a double bracket must be used.

Example

let mut f: Formatter;
f = "".parse().unwrap();
assert_eq!(f.fmt(1.234), "1.234");

f = "prefix ".parse().unwrap();
assert_eq!(f.fmt(1.234), "prefix 1.234");

f = "[] suffix".parse().unwrap();
assert_eq!(f.fmt(1.234), "1.234 suffix");

f = "[[prefix [] suffix]]".parse().unwrap();
assert_eq!(f.fmt(1.234), "[prefix 1.234 suffix]");

Precision

Precision is defined using a ./, for decimals, or a ~ for significant figures, followed by a number. A maximum of 255 is supported. There is a special case: .*/,* which removes any default precision and uses Precision::Unspecified. Note that usage of , signals to use periods as the separator and comma as the decimal marker. To use a comma with signficant figures, use a period separator.

Example

let mut f: Formatter;
f = "[.2]".parse().unwrap(); // use two decimal places
assert_eq!(f.fmt(1.2345), "1.23");

f = "[,2]".parse().unwrap(); // use two decimal places with comma
assert_eq!(f.fmt(1.2345), "1,23");

f = "[.0]".parse().unwrap(); // use zero decimal places
assert_eq!(f.fmt(10.234), "10");

f = "[.*]".parse().unwrap(); // arbitary precision
assert_eq!(f.fmt(1.234), "1.234");
assert_eq!(f.fmt(12.2), "12.2");

f = "[,*]".parse().unwrap(); // arbitary precision with comma
assert_eq!(f.fmt(1.234), "1,234");

f = "[~3]".parse().unwrap(); // 3 significant figures
assert_eq!(f.fmt(1.234), "1.23");
assert_eq!(f.fmt(10.234), "10.2");
f = "[~3/.]".parse().unwrap(); // 3 significant figures with comma
assert_eq!(f.fmt(1.234), "1,23");

Scale

Scale uses a character to denote what scaling should be used. By default the SI scaling is used. The following characters are supported:

Example

let mut f: Formatter;
f = "".parse().unwrap(); // default si scaling used
assert_eq!(f.fmt(12345.0), "12.345 K");

f = "[n]".parse().unwrap(); // turn off scaling
assert_eq!(f.fmt(12345.0), "12,345.0");

f = "[%.2]".parse().unwrap(); // format as percentages with 2 decimal places
assert_eq!(f.fmt(0.234), "23.40%");

f = "[b]".parse().unwrap(); // use a binary scaler
assert_eq!(f.fmt(3.14 * 1024.0 * 1024.0), "3.14 Mi");

Separator

A separator character can be specified by using a forward slash / followed by a character. The parser uses the next character, unless that character is ] in which case the separator is set to None. The default separator is a comma. If a period separator . is specified, we take this as a signal to use a comma , as the decimal signifier.

Example

let mut f: Formatter;
f = "[n]".parse().unwrap(); // turn off scaling to see separator
assert_eq!(f.fmt(12345.0), "12,345.0");

f = "[n/]".parse().unwrap(); // use no separator
assert_eq!(f.fmt(12345.0), "12345.0");

f = "[n/_]".parse().unwrap(); // use a underscroll
assert_eq!(f.fmt(12345.0), "12_345.0");

f = "[n/ ]".parse().unwrap(); // use a space
assert_eq!(f.fmt(12345.0), "12 345.0");

f = "[n/.]".parse().unwrap(); // use period and commas
assert_eq!(f.fmt(12345.0), "12.345,0");

Composing formats

There have been examples of composing formats already. The prefix[num]suffix order must be adhered to, but the ordering within the number format is arbitary. It is recommended to keep it consistent with precison, scaling, separator as this assists with readability and lowers the risk of malformed formats (which will error on the parsing phase).

Various composed examples

let mut f: Formatter;

// Percentages to two decimal places
f = "[.2%]".parse().unwrap();
assert_eq!(f.fmt(0.012345), "1.23%");

// Currency to zero decimal places
// notice the `n` for no scaling
f = "$[.0n] USD".parse().unwrap();
assert_eq!(f.fmt(123_456_789.12345), "$123,456,789 USD");

// Formatting file sizes
f = "[~3b]B".parse().unwrap();
assert_eq!(f.fmt(123_456_789.0), "117 MiB");

// Units to 1 decimal place
f = "[.1n] m/s".parse().unwrap();
assert_eq!(f.fmt(12345.68), "12,345.6 m/s");

// Using custom separator and period for decimals
f = "[,1n/_]".parse().unwrap();
assert_eq!(f.fmt(12345.68), "12_345,6");

Structs

Enums

Traits

  • An object is representable as a 64-bit floating point number.

Type Definitions