HumanNumbers.AspNetCore
2.0.2
dotnet add package HumanNumbers.AspNetCore --version 2.0.2
NuGet\Install-Package HumanNumbers.AspNetCore -Version 2.0.2
<PackageReference Include="HumanNumbers.AspNetCore" Version="2.0.2" />
<PackageVersion Include="HumanNumbers.AspNetCore" Version="2.0.2" />
<PackageReference Include="HumanNumbers.AspNetCore" />
paket add HumanNumbers.AspNetCore --version 2.0.2
#r "nuget: HumanNumbers.AspNetCore, 2.0.2"
#:package HumanNumbers.AspNetCore@2.0.2
#addin nuget:?package=HumanNumbers.AspNetCore&version=2.0.2
#tool nuget:?package=HumanNumbers.AspNetCore&version=2.0.2
HumanNumbers
Human-readable numbers for .NET — a governed, high-performance, API-safe formatting engine.
⚡ Quick Start
using HumanNumbers;
1500.ToHuman(); // "1.50K"
1500000.ToHuman(); // "1.50M"
🎯 Why HumanNumbers?
Formatting numbers correctly at scale is deceptively complex. Naive implementations often:
- Break at rounding boundaries: (e.g.,
999,499rounding to1,000Kinstead of1.00M). - Allocate excessively: Creating intermediate strings in hot telemetry or logging paths.
- Lack consistency: Different services formatting the same data in conflicting ways.
HumanNumbers provides a governed, high-performance platform for number presentation that prioritizes:
- Correctness: Smart suffix promotion logic that handles rounding boundaries gracefully.
999499m.ToHuman(); // "1.00M" (Readable) 999499m.ToHuman(HumanNumberFormatOptions.StrictPreset); // "999.50K" (Accurate) - Performance: Optimized
Span<char>-based paths and zero-allocation parsing. - Safety: Non-intrusive ASP.NET Core integration that preserves API contracts by default.
- Governance: A central Policy system to ensure consistent formatting across distributed services.
🚀 Comparison: Three Ways to Format
| Approach | Code | Trade-off |
|---|---|---|
| Naive .NET | (val / 1000).ToString("F2") + "K" |
Error-prone at thresholds, high allocation, no culture support. |
| Fluent API | val.ToHuman(2) |
Balanced. Minimal allocation with full formatting support. |
| Span API | val.ToHuman(span, out _) |
High-Performance. Zero-allocation; ideal for telemetry and logging. |
The "Naive" example reflects common real-world implementations, not optimized hand-written formatters. Both APIs use the same high-performance core engine; the Span overload simply bypasses string materialization for critical paths.
🔢 Core Formatting Engine
Readable vs. Strict Promotion
By default, we prioritize readability. If a number rounds up to the next threshold (e.g., 999,499 to 1.00M), we promote the suffix. For audit-heavy scenarios, use Strict Mode:
999499m.ToHuman(); // "1.00M" (Readable Default)
999499m.ToHuman(HumanNumberFormatOptions.StrictPreset); // "999.50K" (Strict Accuracy)
Specialized Units
- Financial:
1234.56m.ToHumanWords(); // "One Thousand Two Hundred..." - Bytes:
1024L.ToHumanBytes(); // "1.00 KiB"(Supports Binary/Decimal) - Fractions:
1.5m.ToHumanFraction(32); // "1 16/32" - Roman:
2024.ToRoman(); // "MMXXIV"
🌍 Internationalization & Global Support
HumanNumbers is built on top of the native .NET CultureInfo system, meaning it automatically respects global numbering rules, separators, and currency symbols out of the box.
Verified Global Outputs (Examples)
| Culture | Scaled Human | Currency Format | Non-Scaled Grouping | Notes |
|---|---|---|---|---|
| ar-SA (Arabic) | 1٫25M |
ر.س. 1٫25M |
1٬000٬000 |
Uses unique Arabic separators. |
| hi-IN (Hindi) | 1.25M |
₹1.25M |
10,00,000 |
Lakh/Crore grouping (10,00,000). |
| fr-FR (French) | 1,25M |
1,25M € |
1 000 000 |
Space separator and trailing currency. |
| de-DE (German) | 1,25M |
1,25M € |
1.000.000 |
Comma decimal and dot grouping. |
| am-ET (Amharic) | 1.25M |
Br1.25M |
1,000,000 |
Custom Ethiopian currency symbol. |
Custom Words (Financial I18n)
For non-English support in financial word-formatting (Check Writing), implement the IWordsProvider interface:
public class SpanishWordsProvider : IWordsProvider
{
public string NegativeWord => "Negativo";
public string ConjunctionWord => "con";
public string ToWords(decimal value) => "Mil Doscientos";
}
// Usage
1200m.ToHumanWords(provider: new SpanishWordsProvider()); // "Mil Doscientos"
🚀 Modernization & New Features (v2.0.2)
We have significantly hardened and optimized the codebase with robust enterprise-grade features:
- Pluggable Currency Dependency Injection (
ICurrencyMappingProvider): Map custom regional or business keys (like"EastAfrica","AsiaPacific") to ISO currency codes by registering a custom provider to the DI container:builder.Services.AddSingleton<ICurrencyMappingProvider, MyCustomMappingProvider>(); - O(1) Pre-Cached Currency Lookups: High-latency culture scanning is gone! Currency symbols are fully pre-cached, turning regional scans into O(1) dictionary lookups.
- O(log N) Suffix Lookups: Support for large custom suffix arrays with O(log N) binary search threshold selection, retaining fast linear scanning for standard
<= 8suffix arrays. - Thread-Safe Option Mutations: Shared options registration utilizes record
with-expressions, preventing thread cross-contamination during concurrent executions. - Circular Reference Protection: Dynamic serialization filter protects APIs from recursion crashes by tracing visited nodes via identity-based reference sets.
🧩 Policy-Driven Formatting (Enterprise Ready)
Define formatting rules once and enforce them across your entire infrastructure. Ensure that APIs, dashboards, and reports share the same magnitude logic and rounding rules.
// Setup central governance
builder.Services.AddHumanNumbersDefaults(options =>
{
options.AddPolicy("Finance", new HumanNumberFormatOptions
{
DecimalPlaces = 2,
PromotionThreshold = 1.0m // strict mode: 999,499 -> "999.50K"
});
});
// Apply consistently across services
HumanNumber.Format(value).UsingPolicy("Finance").ToHuman();
🌍 Multi-Culture & Custom Magnitude Examples
The policy system handles unique numbering systems (like 10,000-based scaling) with ease:
builder.Services.AddHumanNumbersDefaults(options =>
{
// Indian System: Lakhs (10^5) and Crores (10^7)
options.AddPolicy("Indian", new HumanNumberFormatOptions
{
CachedCustomSuffixes = new[] {
new MagnitudeSuffix(1000m, "K"),
new MagnitudeSuffix(100_000m, "Lakh"),
new MagnitudeSuffix(10_000_000m, "Crore")
},
Threshold = 1000m,
DecimalPlaces = 2
});
// Chinese System: Wàn (10^4) and Yì (10^8)
options.AddPolicy("Chinese", new HumanNumberFormatOptions
{
CachedCustomSuffixes = new[] {
new MagnitudeSuffix(10_000m, "Wàn"),
new MagnitudeSuffix(100_000_000m, "Yì")
},
Threshold = 10_000m
});
});
// Usage
120000m.ToHuman(HumanNumbersConfig.Instance.GetPolicies()["Indian"]); // "1.20 Lakh"
120000m.ToHuman(HumanNumbersConfig.Instance.GetPolicies()["Chinese"]); // "12.00 Wàn"
Use CachedCustomSuffixes for non-standard scaling (like 10^4 or 10^5). For standard 10^3 scaling, simply use CustomSuffixes.
🚀 Real-World Scenario: Zero-Allocation Telemetry
In high-frequency logging or telemetry, even small string allocations can trigger GC pressure. HumanNumbers allows you to format directly into reusable buffers:
// Reusable buffer (stack or ArrayPool)
Span<char> buffer = stackalloc char[32];
if (largeValue.ToHuman(buffer, out var written))
{
// Log directly from the span without creating a string
logger.LogInformation("Metric: {Value}", buffer[..written]);
}
📊 Performance & Engineering Nuance
We benchmark against "Naive" implementations to provide an honest look at the costs of convenience.
| Operation | Latency | Allocated Memory | Engineering Note |
|---|---|---|---|
| Manual Concatenation | ~60-80 ns | 64-80 B | Standard ToString() + "K" approach. |
ToHuman() |
~160-180 ns | 56 B | Balanced. Uses less memory than naive concatenation. |
ToHuman (Span) |
~150-170 ns | 24 B | Optimized. Significant memory reduction via Span paths. |
TryParse |
~40-50 ns | 0 B | Zero-Alloc. Fully allocation-free parsing. |
Latency figures are environment-dependent. The "Naive" example reflects common real-world implementations, not optimized hand-written formatters.
🌐 ASP.NET Core: Safe by Default
Many libraries implicitly change JSON output globally, breaking API contracts. HumanNumbers is designed to be non-intrusive.
By default, HumanNumbers does not modify JSON output unless explicitly enabled via attributes or result extensions.
1. Register Policies
builder.Services.AddHumanNumbersDefaults(options => {
options.AddPolicy("Compact", new HumanNumberFormatOptions { DecimalPlaces = 1 });
});
2. Selective Serialization
Control exactly what the client sees without breaking your DTOs.
public class AnalyticsDto {
public decimal RawValue { get; set; } // JSON: 1500000
[HumanNumber(OutputMode = HumanNumberOutputMode.SerializeAsHuman)]
public decimal DisplayValue { get; set; } // JSON: "1.50M"
}
3. Explicit Transformations
In Minimal APIs, use HumanOk to trigger the transformation only when intended:
app.MapGet("/stats", () => Results.Extensions.HumanOk(new { Revenue = 1500000 }));
🧠 Design Philosophy
- Predictable Performance: Every feature has a known and stable allocation profile.
- Governance First: Use the
Policysystem to define "Brand Guidelines" for numbers once, then apply them everywhere. - Allocation Aware: We leverage
Span<char>andISpanFormattableto ensure that adding "humanity" to your data doesn't sink your GC performance. - Contract Safety: Your API types remain
decimal. We only change the representation during the final serialization step.
🔄 Migration from NumberFormatter
HumanNumbers is the official successor to the legacy NumberFormatter package. It features a modernized API, significantly improved performance (via Span<char>), and a unified policy system.
Key Changes
- Namespace:
NumberFormatter→HumanNumbers - Method Renaming:
ToShortString()→ToHuman()ToShortCurrencyString()→ToHumanCurrency()
- Binary Compatibility: A legacy shim is provided in the
NumberFormatternamespace (marked as[Obsolete]) to help with a zero-friction transition.
// Legacy (NumberFormatter)
using NumberFormatter;
1500.ToShortString();
// Modern (HumanNumbers)
using HumanNumbers;
1500.ToHuman();
📦 Installation
dotnet add package HumanNumbers
dotnet add package HumanNumbers.AspNetCore
🙌 Contributing & License
Licensed under MIT. Contributions are welcome via Issues and Pull Requests.
📈 Appendix: Detailed Benchmarks
Detailed benchmarks below reflect full production runs and may differ slightly from summarized figures above. All results were generated using BenchmarkDotNet v0.14.0 on .NET 10.0 (X64 RyuJIT).
| Method | Scenario / Input | Mean | Gen 0 | Allocated | Engineering Context |
|---|---|---|---|---|---|
StandardScaled |
Naive (999,499) | 78.33 ns | 0.0026 | 80 B | Naive ToString + Concat approach. |
ToHuman |
Governed (999,499) | 172.79 ns | 0.0024 | 56 B | 30% less memory than naive. |
ToHuman (Span) |
Governed (999,499) | 152.09 ns | 0.0007 | 24 B | Optimized Span path. |
TryParse |
$1.50M |
50.86 ns | - | 0 B | Zero-Alloc parsing. |
ToHumanBytes |
1024 Bytes | 56.62 ns | 0.0013 | 40 B | Single materialized string. |
ToHumanWords |
1234.56 | 322.15 ns | 0.0186 | 560 B | Non-recursive assembly. |
ToRoman |
2024 | 31.79 ns | 0.0013 | 40 B | Optimized buffer path. |
Key Takeaway: While HumanNumbers handles complex thresholds and rounding logic that manual code often misses, it does so with a smaller memory footprint than naive string concatenation approaches.
🔎 Keywords
human-readable numbers, number formatting, suffix formatting, K/M/B formatting, span formatting, zero allocation, .NET performance, telemetry formatting, financial formatting, magnitude formatting.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- HumanNumbers (>= 2.0.2)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.2 | 81 | 5/20/2026 |
| 2.0.1 | 104 | 5/11/2026 |
| 2.0.0 | 92 | 5/10/2026 |
| 2.0.0-preview.1 | 57 | 4/27/2026 |