Skip to main content

Crate humfmt

Crate humfmt 

Source
Expand description

§humfmt

Ergonomic human-readable formatting toolkit for Rust.

humfmt turns raw machine values into readable text. All formatters implement Display and write directly into the output — no intermediate heap strings, no hidden allocations.

§Quick start

The crate exposes two usage styles for every formatter:

  1. Free functionshumfmt::number(15320) -> "15.3K"
  2. Extension trait15320.human_number() -> "15.3K"
use humfmt::Humanize;
use core::time::Duration;

// Numbers
assert_eq!(humfmt::number(15320).to_string(), "15.3K");
assert_eq!(1_500_000.human_number().to_string(), "1.5M");
assert_eq!(humfmt::number(-12_500).to_string(), "-12.5K");

// Bytes
assert_eq!(humfmt::bytes(1536).to_string(), "1.5KB");
assert_eq!(1024_u64.human_bytes_with(
    humfmt::BytesOptions::new().binary()
).to_string(), "1KiB");

// Percentages
assert_eq!(humfmt::percent(0.423).to_string(), "42.3%");
assert_eq!(0.15_f64.human_percent().to_string(), "15%");

// Ordinals
assert_eq!(humfmt::ordinal(21).to_string(), "21st");
assert_eq!(42_u32.human_ordinal().to_string(), "42nd");

// Durations
assert_eq!(humfmt::duration(Duration::from_secs(3661)).to_string(), "1h 1m");

// Relative time
assert_eq!(humfmt::ago(Duration::from_secs(90)).to_string(), "1m 30s ago");

// Lists
assert_eq!(humfmt::list(&["red", "green", "blue"]).to_string(), "red, green, and blue");

§Allocation model

All formatters implement Display and write directly into the provided formatter. This means:

  • format!("{}", humfmt::number(x)) allocates — because format! must produce a String
  • write!(&mut buf, "{}", humfmt::number(x)) reuses the buffer — no new allocation from the formatter

Example:

use core::fmt::Write as _;

let mut out = String::with_capacity(32);
out.clear();
write!(&mut out, "{}", humfmt::bytes(9_876_543_210_u64)).unwrap();
// `out` was reused, no heap allocation from the formatter

§Compact numbers

Turns large values into short forms: 15320 -> "15.3K", 1_500_000 -> "1.5M".

use humfmt::{number, number_with, NumberOptions};

assert_eq!(number(15320).to_string(), "15.3K");
assert_eq!(number(1_500_000).to_string(), "1.5M");
assert_eq!(number(-12_500).to_string(), "-12.5K");

§Defaults

OptionDefaultMeaning
precision1fractional digits for compact values
significant_digitsnoneround to N total significant digits
compacttrueenable magnitude scaling (1.5K vs 1500)
force_signfalseoutput + for positive numbers
roundingHalfUpHalfUp, Floor, Ceil behaviour
long_unitsfalseK vs thousand
separatorsfalsegroup separator for unscaled output
fixed_precisionfalsekeep trailing zeros (1.50K)
decimal_separator'.'decimal separator character
group_separator','digit grouping separator character

§Suffix table

IndexMagnitudeShortLong
01""""
110^3K thousand
210^6M million
310^9B billion
410^12T trillion
510^15Qa quadrillion
610^18Qi quintillion
710^21Sx sextillion
810^24Sp septillion
910^27Oc octillion
1010^30No nonillion
1110^33Dc decillion

§Precision

use humfmt::{number_with, NumberOptions};

assert_eq!(number_with(15_320, NumberOptions::new().precision(0)).to_string(), "15K");
assert_eq!(number_with(15_320, NumberOptions::new().precision(1)).to_string(), "15.3K");
assert_eq!(number_with(15_320, NumberOptions::new().precision(2)).to_string(), "15.32K");

Precision is clamped to 0..=6.

§Significant digits

Instead of fixed decimal places, round to N total significant digits:

use humfmt::{number_with, NumberOptions};

let opts = NumberOptions::new().significant_digits(3);
assert_eq!(number_with(1234, opts).to_string(), "1.23K");
assert_eq!(number_with(12345, opts).to_string(), "12.3K");
assert_eq!(number_with(123456, opts).to_string(), "123K");
assert_eq!(number_with(1234567, opts).to_string(), "1.23M");

With fixed precision, zeros are padded intelligently:

use humfmt::{number_with, NumberOptions};

let opts = NumberOptions::new().significant_digits(3).fixed_precision(true);
assert_eq!(number_with(1, opts).to_string(), "1.00");
assert_eq!(number_with(10, opts).to_string(), "10.0");
assert_eq!(number_with(100, opts).to_string(), "100");

§Disabling compact scaling

Combine compact(false) with separators(true) for fully formatted large numbers:

use humfmt::{number_with, NumberOptions};

let opts = NumberOptions::new().compact(false).separators(true);
assert_eq!(number_with(1_234_567, opts).to_string(), "1,234,567");
assert_eq!(number_with(12_345, opts).to_string(), "12,345");

Without separators:

use humfmt::{number_with, NumberOptions};

let opts = NumberOptions::new().compact(false);
assert_eq!(number_with(1_500_000, opts).to_string(), "1500000");
assert_eq!(number_with(1_500_000.5_f64, opts).to_string(), "1500000.5");

§Custom separators

use humfmt::{number_with, NumberOptions};

// European-style: comma decimal, space group
let opts = NumberOptions::new()
    .compact(false)
    .separators(true)
    .decimal_separator(',')
    .group_separator(' ');
assert_eq!(number_with(1_234_567, opts).to_string(), "1 234 567");
assert_eq!(number_with(1_234_567.5_f64, opts).to_string(), "1 234 567,5");

§Forced sign

Useful for delta/change displays:

use humfmt::{number_with, NumberOptions};

let opts = NumberOptions::new().force_sign(true);
assert_eq!(number_with(1500, opts).to_string(), "+1.5K");
assert_eq!(number_with(42, opts).to_string(), "+42");
assert_eq!(number_with(0, opts).to_string(), "0");
assert_eq!(number_with(-1500, opts).to_string(), "-1.5K");

§Rounding modes

use humfmt::{number_with, NumberOptions, RoundingMode};

let base = NumberOptions::new().precision(0);

assert_eq!(number_with(1_900, base.rounding(RoundingMode::HalfUp)).to_string(), "2K");
assert_eq!(number_with(1_900, base.rounding(RoundingMode::Floor)).to_string(), "1K");
assert_eq!(number_with(1_900, base.rounding(RoundingMode::Ceil)).to_string(), "2K");

let base2 = NumberOptions::new().precision(2);
assert_eq!(number_with(-1234, base2.rounding(RoundingMode::HalfUp)).to_string(), "-1.23K");
assert_eq!(number_with(-1234, base2.rounding(RoundingMode::Floor)).to_string(), "-1.24K");
assert_eq!(number_with(-1234, base2.rounding(RoundingMode::Ceil)).to_string(), "-1.23K");

Rounding may rescale across a suffix boundary:

use humfmt::{number_with, NumberOptions, RoundingMode};

let base = NumberOptions::new().precision(0);
assert_eq!(number_with(999_500, base.rounding(RoundingMode::HalfUp)).to_string(), "1M");
assert_eq!(number_with(999_500, base.rounding(RoundingMode::Floor)).to_string(), "999K");
assert_eq!(number_with(999_500, base.rounding(RoundingMode::Ceil)).to_string(), "1M");

§Long-form labels

use humfmt::{number_with, NumberOptions};

let opts = NumberOptions::new().long_units();
assert_eq!(number_with(15_320, opts).to_string(), "15.3 thousand");
assert_eq!(number_with(1_000_000, opts).to_string(), "1 million");

§Edge cases

InputOutputNotes
0"0"No suffix, no sign
1"1"Below threshold
999"999"Below threshold
1_000"1K"First compact threshold
999_950"1M"Rounds up across boundary
-1"-1"Sign preserved
-12_500"-12.5K"Sign + compact
i128::MIN"-170.1Dc"No panic, no overflow
u128::MAX"340.3Dc"No panic, no overflow
0.0"0"
-0.0"0"Negative zero suppressed
-0.004"0"Rounds to zero, sign suppressed
f64::INFINITY"inf"
f64::NEG_INFINITY"-inf"
f64::NAN"NaN"

§Byte sizes

Formats byte counts using decimal (SI, 1000-based) or binary (IEC, 1024-based) units.

use humfmt::{bytes, bytes_with, BytesOptions};

assert_eq!(bytes(1536_u64).to_string(), "1.5KB");
assert_eq!(bytes_with(1536_u64, BytesOptions::new().binary()).to_string(), "1.5KiB");

§Defaults

OptionDefaultMeaning
precision1fractional digits for scaled values
significant_digitsnoneround to N total significant digits
binaryfalseSI (1000) vs IEC (1024)
bitsfalsemultiply by 8 and use bit units (Mb)
roundingHalfUpHalfUp, Floor, Ceil behaviour
long_unitsfalseKB vs kilobytes
decimal_separator.decimal separator for scaled output
spacefalseadd a space before short unit labels
fixed_precisionfalsekeep trailing zeros (1.50KB)
min_unitBclamp minimum unit
max_unitEBclamp maximum unit

§SI vs IEC

use humfmt::{bytes, bytes_with, BytesOptions};

assert_eq!(bytes(1000_u64).to_string(), "1KB");
assert_eq!(bytes(1_500_000_u64).to_string(), "1.5MB");

let bin = BytesOptions::new().binary();
assert_eq!(bytes_with(1024_u64, bin).to_string(), "1KiB");
assert_eq!(bytes_with(1_536_u64, bin).to_string(), "1.5KiB");

§Bits mode

use humfmt::{bytes_with, BytesOptions};

let opts = BytesOptions::new().bits(true);
assert_eq!(bytes_with(1000_u64, opts).to_string(), "8Kb");
assert_eq!(bytes_with(1_500_000_u64, opts).to_string(), "12Mb");

let opts_bin = BytesOptions::new().bits(true).binary();
assert_eq!(bytes_with(1024_u64, opts_bin).to_string(), "8Kib");

let opts_long = BytesOptions::new().bits(true).long_units();
assert_eq!(bytes_with(1_u64, opts_long).to_string(), "8 bits");
assert_eq!(bytes_with(125_u64, opts_long).to_string(), "1 kilobit");

§Unit forcing and clamping

use humfmt::{bytes_with, BytesOptions, ByteUnit};

let opts = BytesOptions::new().unit(ByteUnit::MB).precision(3);
assert_eq!(bytes_with(150_000_u64, opts).to_string(), "0.15MB");
assert_eq!(bytes_with(1_500_000_u64, opts).to_string(), "1.5MB");
assert_eq!(bytes_with(1_500_000_000_u64, opts).to_string(), "1500MB");

let opts = BytesOptions::new().min_unit(ByteUnit::KB).precision(2);
assert_eq!(bytes_with(500_u64, opts).to_string(), "0.5KB");

let opts = BytesOptions::new().max_unit(ByteUnit::GB);
assert_eq!(bytes_with(2_000_000_000_000_u64, opts).to_string(), "2000GB");

§Long-form labels

use humfmt::{bytes_with, BytesOptions};

let opts = BytesOptions::new().long_units();
assert_eq!(bytes_with(1_u64, opts).to_string(), "1 byte");
assert_eq!(bytes_with(1536_u64, opts).to_string(), "1.5 kilobytes");

let bin = BytesOptions::new().binary().long_units();
assert_eq!(bytes_with(1024_u64, bin).to_string(), "1 kibibyte");

§Custom decimal separator

use humfmt::BytesOptions;

let opts = BytesOptions::new().decimal_separator(',');
assert_eq!(humfmt::bytes_with(1536_u64, opts).to_string(), "1,5KB");

§Edge cases

InputOutputNotes
0"0B"Zero bytes
999"999B"Below threshold
1000"1KB"SI threshold
1024"1KB" (SI) / "1KiB" (IEC)Threshold difference
-1536"-1.5KB"Negative supported
u128::MAX"...EB"Largest unit, no overflow
999_950"1MB"Rounds up across boundary

§Percentages

Converts a ratio to a percentage: 0.423 -> "42.3%".

The input is a ratio where 1.0 = 100%. Values outside 0.0..=1.0 are accepted and rendered as-is.

use humfmt::{percent, percent_with, PercentOptions};

assert_eq!(percent(0.0_f64).to_string(), "0%");
assert_eq!(percent(0.5_f64).to_string(), "50%");
assert_eq!(percent(1.0_f64).to_string(), "100%");
assert_eq!(percent(0.423_f64).to_string(), "42.3%");

§Defaults

OptionDefaultMeaning
precision1fractional digits
force_signfalseoutput + for positive percentages
fixed_precisionfalsekeep trailing zeros (42.50%)
decimal_separator'.'decimal separator character

§Examples

use humfmt::{percent_with, PercentOptions};

let two = PercentOptions::new().precision(2);
assert_eq!(percent_with(0.4236_f64, two).to_string(), "42.36%");

let fixed = PercentOptions::new().precision(2).fixed_precision(true);
assert_eq!(percent_with(0.5_f64, fixed).to_string(), "50.00%");

let signed = PercentOptions::new().force_sign(true);
assert_eq!(percent_with(0.42_f64, signed).to_string(), "+42%");

let comma = PercentOptions::new().precision(1).decimal_separator(',');
assert_eq!(percent_with(0.423_f64, comma).to_string(), "42,3%");

§Edge cases

InputOutputNotes
0.0"0%"
-0.0"0%"Negative zero suppressed
0.5"50%"
1.0"100%"
1.5"150%"Above 100% accepted
-0.423"-42.3%"Negative accepted
-0.0004"0%"Rounds to zero, sign suppressed
f64::NAN"NaN%"Non-finite preserved
f64::INFINITY"inf%"Non-finite preserved
f64::NEG_INFINITY"-inf%"Non-finite preserved

§Ordinals

English ordinal markers (with the standard teen exceptions).

use humfmt::ordinal;

assert_eq!(ordinal(1).to_string(), "1st");
assert_eq!(ordinal(2).to_string(), "2nd");
assert_eq!(ordinal(11).to_string(), "11th");
assert_eq!(ordinal(21).to_string(), "21st");
assert_eq!(ordinal(42).to_string(), "42nd");
assert_eq!(ordinal(103).to_string(), "103rd");
assert_eq!(ordinal(111).to_string(), "111th");
assert_eq!(ordinal(-1).to_string(), "-1st");

If you only need the suffix (e.g. for custom rendering pipelines):

assert_eq!(humfmt::ordinal::ordinal_suffix(21), "st");
assert_eq!(humfmt::ordinal::ordinal_suffix(11), "th");

§Durations

Compact or long-form duration formatting for core::time::Duration.

use core::time::Duration;
use humfmt::{duration, duration_with, DurationOptions};

assert_eq!(duration(Duration::from_secs(3661)).to_string(), "1h 1m");
assert_eq!(duration(Duration::from_secs(90061)).to_string(), "1d 1h");

let opts = DurationOptions::new().long_units().max_units(3);
assert_eq!(
    duration_with(Duration::from_secs(3665), opts).to_string(),
    "1 hour 1 minute 5 seconds"
);

§Defaults

OptionDefaultMeaning
max_units2maximum number of non-zero units to render
long_unitsfalseh vs hour

§Edge cases

InputOutputNotes
0s"0s"Zero duration
900ms"900ms"Below 1s
1500ms"1s 500ms"Compound
90s"1m 30s"Two units (default)
3661s"1h 1m"Seconds truncated
90061s"1d 1h"Days included

§Relative time

Builds on the duration formatter and appends " ago".

use core::time::Duration;
use humfmt::{ago, ago_with, DurationOptions};

assert_eq!(ago(Duration::from_secs(90)).to_string(), "1m 30s ago");
assert_eq!(ago(Duration::from_secs(3661)).to_string(), "1h 1m ago");
assert_eq!(ago(Duration::ZERO).to_string(), "0s ago");

let opts = DurationOptions::new().long_units();
assert_eq!(
    ago_with(Duration::from_millis(1500), opts).to_string(),
    "1 second 500 milliseconds ago"
);

§Lists

Natural-language list formatting.

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

assert_eq!(list(&["red", "green", "blue"]).to_string(), "red, green, and blue");
assert_eq!(list(&["red"]).to_string(), "red");
assert_eq!(list::<&str>(&[]).to_string(), "");

assert_eq!(list(&["red", "green"]).to_string(), "red and green");

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

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

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

// Any Display type works
assert_eq!(list(&[1, 2, 3]).to_string(), "1, 2, and 3");

§Edge cases

InputOutputNotes
[]""Empty list
["red"]"red"Single item
["red", "green"]"red and green"Two items, no comma
["red", "green", "blue"]"red, green, and blue"Three items, serial comma

The serial comma is only injected when the separator is comma-style. Custom separators like " | " will not get a serial comma even if enabled.


§Optional integrations

Enable via feature flags:

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

§chrono

Adapts chrono::TimeDelta and chrono::DateTime:

use humfmt::chrono as humchrono;

let delta = chrono::TimeDelta::try_seconds(90).unwrap();
assert_eq!(humchrono::duration(delta).unwrap().to_string(), "1m 30s");
assert_eq!(humchrono::ago(delta).unwrap().to_string(), "1m 30s ago");

let then = chrono::DateTime::from_timestamp(0, 0).unwrap();
let now = chrono::DateTime::from_timestamp(3665, 0).unwrap();
assert_eq!(
    humchrono::ago_since(then, now).unwrap().to_string(),
    "1h 1m ago"
);

§time

Adapts time::Duration and time::OffsetDateTime:

use humfmt::time as humtime;

let delta = time::Duration::seconds(90);
assert_eq!(humtime::duration(delta).unwrap().to_string(), "1m 30s");

let then = time::OffsetDateTime::from_unix_timestamp(0).unwrap();
let now = time::OffsetDateTime::from_unix_timestamp(3665).unwrap();
assert_eq!(
    humtime::ago_since(then, now).unwrap().to_string(),
    "1h 1m ago"
);

§Checked variants

Both integrations provide *_checked functions that return DurationConversionError:

use humfmt::{chrono as humchrono, DurationConversionError};

let delta = chrono::TimeDelta::try_seconds(-5).unwrap();
assert!(matches!(
    humchrono::duration_checked(delta),
    Err(DurationConversionError::NegativeDuration)
));

§no_std

humfmt supports no_std:

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

This disables the std feature. Optional integrations (chrono, time) can still be enabled individually.


§Benchmarks

See BENCHMARKS.md for a reproducible comparison report and methodology.

The repository includes a standalone benchmark harness under tools/benchmarks/ that compares humfmt against humansize, bytesize, byte-unit, prettier-bytes, human_format, numfmt, humantime, timeago, and others.

Re-exports§

pub use ago::ago;
pub use ago::ago_with;
pub use ago::AgoDisplay;
pub use bytes::bytes;
pub use bytes::bytes_with;
pub use bytes::ByteUnit;
pub use bytes::BytesDisplay;
pub use bytes::BytesLike;
pub use bytes::BytesOptions;
pub use duration::duration;
pub use duration::duration_with;
pub use duration::DurationDisplay;
pub use duration::DurationLike;
pub use duration::DurationOptions;
pub use list::list;
pub use list::list_with;
pub use list::ListDisplay;
pub use list::ListOptions;
pub use number::number;
pub use number::number_with;
pub use number::NumberDisplay;
pub use number::NumberOptions;
pub use ordinal::ordinal;
pub use ordinal::OrdinalDisplay;
pub use ordinal::OrdinalLike;
pub use percent::percent;
pub use percent::percent_with;
pub use percent::PercentDisplay;
pub use percent::PercentLike;
pub use percent::PercentOptions;

Modules§

ago
Relative-time formatting.
bytes
Human-readable byte-size formatting.
chronochrono
Optional integration with chrono.
duration
Human-readable duration formatting.
list
Natural-language list formatting.
number
Compact number formatting.
ordinal
Ordinal formatting (English).
percent
Percentage formatting.
prelude
Common imports for ergonomic humfmt usage.
timetime
Optional integration with time.

Structs§

NegativeDurationError
Error returned when a duration-like value is negative.

Enums§

DurationConversionError
Conversion error for duration adapters.
RoundingMode
Specifies how numerical values should be rounded.

Traits§

Humanize
Extension trait providing ergonomic .human_*() methods.