<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.5"
```
```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
// 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"),
);
assert_eq!(plus.to_string(), "red, green, plus blue");
```
---
## Locales
English is the default. Russian and Polish are available behind feature flags:
```toml
[dependencies]
humfmt = { version = "0.5", features = ["russian", "polish"] }
```
```rust
use humfmt::{number_with, ordinal_with, NumberOptions};
use humfmt::locale::{Russian, Polish};
// Russian: comma decimal separator, space group separator, inflected suffixes
println!("{}", number_with(15_320, NumberOptions::new().locale(Russian))); // 15,3 тыс.
println!("{}", number_with(1_000, NumberOptions::new().locale(Russian).long_units())); // 1 тысяча
println!("{}", number_with(5_000, NumberOptions::new().locale(Russian).long_units())); // 5 тысяч
// Polish
println!("{}", number_with(15_320, NumberOptions::new().locale(Polish))); // 15,3 tys.
println!("{}", ordinal_with(21, Polish)); // 21.
```
Custom locale for anything else:
```rust
use humfmt::{number_with, NumberOptions, locale::CustomLocale};
let locale = CustomLocale::english()
.short_suffix(1, "k")
.separators(',', '.');
println!("{}", number_with(15_320, NumberOptions::new().locale(locale))); // 15,3k
```
---
## 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`, `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 |
| `english` | ✓ | English locale |
| `russian` | | Russian locale pack |
| `polish` | | Polish locale pack |
| `chrono` | | `chrono::TimeDelta` / `DateTime` adapters |
| `time` | | `time::Duration` / `OffsetDateTime` adapters |
For `no_std` targets:
```toml
[dependencies]
humfmt = { version = "0.5", default-features = false }
```
With all locales:
```toml
[dependencies]
humfmt = { version = "0.5", features = ["russian", "polish"] }
```
---
## 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.