<div align="center">
# humfmt
**Ergonomic human-readable formatting toolkit for Rust**
[](https://github.com/MuXolotl/humfmt/actions/workflows/ci.yml)
[](https://crates.io/crates/humfmt)
[](https://docs.rs/humfmt)

</div>
---
`humfmt` turns raw machine values into readable text without turning formatting
into a side quest.
```rust
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
| `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
```toml
[dependencies]
humfmt = "0.6"
```
```rust
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:
```rust
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
```rust
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("&"),
);
---
## 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`](./BENCHMARKS.md) for comparisons against `humansize`,
`bytesize`, `byte-unit`, `prettier-bytes`, `human_format`, `numfmt`,
`humantime`, `timeago`, and others.
<details>
<summary>Charts</summary>
<p align="center">
<img alt="Bytes comparison benchmarks" src="assets/benchmarks/bytes_comparison_dark.svg">
</p>
<p align="center">
<img alt="Numbers comparison benchmarks" src="assets/benchmarks/numbers_dark.svg">
</p>
<p align="center">
<img alt="Duration and relative-time benchmarks" src="assets/benchmarks/time_comparison_dark.svg">
</p>
</details>
---
## Feature flags
| `std` | ✓ | Standard library build |
| `chrono` | | `chrono::TimeDelta` / `DateTime` adapters |
| `time` | | `time::Duration` / `OffsetDateTime` adapters |
For `no_std` targets:
```toml
[dependencies]
humfmt = { version = "0.6", default-features = false }
```
With ecosystem integrations:
```toml
[dependencies]
humfmt = { version = "0.6", features = ["chrono", "time"] }
```
---
## Documentation
- **[docs.rs](https://docs.rs/humfmt)** — full API reference
- **[BENCHMARKS.md](./BENCHMARKS.md)** — performance comparisons and methodology
- **[CHANGELOG.md](./CHANGELOG.md)** — what changed and when
- **[TODO.md](./TODO.md)** — planned features and known issues
- **[examples/](./examples)** — runnable examples
- **[tests/](./tests)** — integration and property tests
---
## 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:
```rust
println!("{}", 1_500_000.human_number());
```
and move on with your life.
---
## License
MIT.