#humanize #numeric #display

no-std humfmt

Ergonomic human-readable formatting toolkit for Rust

9 releases (5 breaking)

new 0.6.0 May 15, 2026
0.5.2 May 3, 2026
0.4.0 May 1, 2026
0.3.0 Apr 28, 2026
0.1.1 Apr 25, 2026

#96 in Value formatting

MIT license

175KB
2K SLoC

humfmt

Ergonomic human-readable formatting toolkit for Rust

CI crates.io docs.rs License


humfmt turns raw machine values into readable text without turning formatting into a side quest.

use humfmt::Humanize;

println!("{}", 1_500_000.human_number());   // 1.5M
println!("{}", 1536_u64.human_bytes());     // 1.5KB
println!("{}", 0.423_f64.human_percent());  // 42.3%
println!("{}", humfmt::ordinal(21));        // 21st

That's it. Import the trait, call a method, done.


What it does

Input Formatter Output
15320 number 15.3K
1536 bytes 1.5KB
0.423 percent 42.3%
21 ordinal 21st
3661s duration 1h 1m
90s ago 1m 30s ago
["red", "green", "blue"] list red, green, and blue

All formatters implement Display — no intermediate heap strings. Write directly into any buffer.


Quick start

[dependencies]
humfmt = "0.6"
use humfmt::Humanize;
use core::time::Duration;

// Extension trait — shortest path
println!("{}", 1_500_000.human_number());                     // 1.5M
println!("{}", 1536_u64.human_bytes());                       // 1.5KB
println!("{}", 0.423_f64.human_percent());                    // 42.3%
println!("{}", 42_u32.human_ordinal());                       // 42nd
println!("{}", Duration::from_secs(90).human_ago());          // 1m 30s ago

// Free functions — same result, no trait import needed
println!("{}", humfmt::number(15320));                        // 15.3K
println!("{}", humfmt::bytes(1536));                          // 1.5KB
println!("{}", humfmt::percent(0.423));                       // 42.3%
println!("{}", humfmt::ordinal(21));                          // 21st
println!("{}", humfmt::duration(Duration::from_secs(3661)));  // 1h 1m
println!("{}", humfmt::list(&["red", "green", "blue"]));      // red, green, and blue

Customization

Every formatter has a *_with variant that takes an options builder:

use core::time::Duration;
use humfmt::{BytesOptions, DurationOptions, Humanize, NumberOptions, PercentOptions};

// Bytes: binary units, space before suffix
let disk = 1536_u64.human_bytes_with(
    BytesOptions::new().binary().precision(2).space(true)
);
println!("{disk}"); // 1.5 KiB

// Bytes: bits mode for network speeds
let speed = 1_500_000_u64.human_bytes_with(
    BytesOptions::new().bits(true)
);
println!("{speed}"); // 12Mb

// Bytes: always show in megabytes
let fixed = 1_500_000_u64.human_bytes_with(
    BytesOptions::new().unit(humfmt::ByteUnit::MB).precision(3)
);
println!("{fixed}"); // 1.5MB

// Bytes: clamp minimum to KB (500 bytes -> 0.5 KB)
let clamped = 500_u64.human_bytes_with(
    BytesOptions::new().min_unit(humfmt::ByteUnit::KB).precision(2)
);
println!("{clamped}"); // 0.5KB

// Numbers: long-form
let n = 15_320.human_number_with(
    NumberOptions::new().precision(2).long_units()
);
println!("{n}"); // 15.32 thousand

// Numbers: full number with digit grouping
let full = 1_234_567.human_number_with(
    NumberOptions::new().compact(false).separators(true)
);
println!("{full}"); // 1,234,567

// Numbers: significant digits
let sig = 12345.human_number_with(
    NumberOptions::new().significant_digits(3)
);
println!("{sig}"); // 12.3K

// Numbers: forced sign for deltas
let delta = 1500.human_number_with(
    NumberOptions::new().force_sign(true)
);
println!("{delta}"); // +1.5K

// Numbers: rounding modes
use humfmt::RoundingMode;
let floor = 1_900.human_number_with(
    NumberOptions::new().precision(0).rounding(RoundingMode::Floor)
);
println!("{floor}"); // 1K

// Numbers: custom separators
let european = 1_234_567.human_number_with(
    NumberOptions::new()
        .compact(false)
        .separators(true)
        .decimal_separator(',')
        .group_separator(' ')
);
println!("{european}"); // 1 234 567

// Percentages: 2 decimal places, fixed precision
let ratio = 0.425_f64.human_percent_with(
    PercentOptions::new().precision(2).fixed_precision(true)
);
println!("{ratio}"); // 42.50%

// Percentages: forced sign
let change = 0.15_f64.human_percent_with(
    PercentOptions::new().force_sign(true)
);
println!("{change}"); // +15%

// Duration: long-form, 3 units
let elapsed = Duration::from_secs(3665).human_duration_with(
    DurationOptions::new().long_units().max_units(3)
);
println!("{elapsed}"); // 1 hour 1 minute 5 seconds

// Relative time: 3 units
let ago = Duration::from_secs(3665).human_ago_with(
    DurationOptions::new().max_units(3)
);
println!("{ago}"); // 1h 1m 5s ago

Lists

use humfmt::{list, list_with, ListOptions};

// Default: Oxford comma
assert_eq!(
    list(&["red", "green", "blue"]).to_string(),
    "red, green, and blue"
);

// No serial comma
let no_oxford = list_with(
    &["red", "green", "blue"],
    ListOptions::new().no_serial_comma(),
);
assert_eq!(no_oxford.to_string(), "red, green and blue");

// Custom conjunction
let plus = list_with(
    &["red", "green", "blue"],
    ListOptions::new().conjunction("plus").no_serial_comma(),
);
assert_eq!(plus.to_string(), "red, green plus blue");

// Custom separator
let piped = list_with(
    &["red", "green", "blue"],
    ListOptions::new().separator(" | ").conjunction("&"),
);
assert_eq!(piped.to_string(), "red | green & blue");

Performance

humfmt is designed to be cheap in hot paths:

  • Zero-alloc Display — formatters write directly into the output, no intermediate String
  • O(1) scaling — integer path uses ilog10, float path uses IEEE 754 exponent
  • no_std — works without the standard library
  • No dependencies — core crate has zero required dependencies

See BENCHMARKS.md for comparisons against humansize, bytesize, byte-unit, prettier-bytes, human_format, numfmt, humantime, timeago, and others.

Charts

Bytes comparison benchmarks

Numbers comparison benchmarks

Duration and relative-time benchmarks


Feature flags

Feature Default Description
std Standard library build
chrono chrono::TimeDelta / DateTime adapters
time time::Duration / OffsetDateTime adapters

For no_std targets:

[dependencies]
humfmt = { version = "0.6", default-features = false }

With ecosystem integrations:

[dependencies]
humfmt = { version = "0.6", features = ["chrono", "time"] }

Documentation


Philosophy

This crate follows one simple rule:

Human formatting should feel stupidly easy.

No giant config ceremony. No formatting gymnastics. No "why is this so annoying?" moments.

Just:

println!("{}", 1_500_000.human_number());

and move on with your life.


License

MIT.

Dependencies

~0–540KB