Getting Started

HumanNumbers is a formatting pipeline that ensures every number your application emits is immediately understandable to humans. It centralizes numeric presentation into a single, culture-aware system integrated throughout the ASP.NET Core stack.

Installation

# Core Library
dotnet add package HumanNumbers

# ASP.NET Core Integration
dotnet add package HumanNumbers.AspNetCore

Quick Start

Start formatting numbers with just a few lines of code:

using HumanNumbers;

// Basic number formatting
1500000m.ToHuman();        // "1.5M"
1500000m.ToHumanCurrency(); // "$1.5M"

// Extension methods for all numeric types
(48000).ToHuman();         // "48K"

Request Culture Resolution

HumanNumbers integrates directly with ASP.NET Core localization features to format numbers dynamically according to the requester's culture:

Resolution Precedence Order

  1. Explicit Context Override: Looks for a culture set inside HttpContext Items using the CultureResolver helper.
  2. Request Localization Middleware: Resolves using standard ASP.NET Core IRequestCultureFeature (based on cookies, query strings, or Accept-Language headers).
  3. Thread Default: Falls back to CultureInfo.CurrentCulture.

Setting Explicit Culture Manually

To manually enforce a specific culture on a per-request basis (e.g., depending on a customer's database setting or custom token), use the CultureResolver.SetCulture method in your controllers or middleware:

// Enforce French localization on the active HttpContext for the current request
CultureResolver.SetCulture(HttpContext, new CultureInfo("fr-FR"));

Automatic API Formatting

HumanNumbers provides high-performance filters and serialization middleware to automatically scale and format response payloads.

Response Transformation & Global Modes

When automatic formatting is enabled via AddHumanNumbersMvc(), the library registers a global action filter:

// Program.cs
builder.Services.AddHumanNumbersMvc(options => {
    options.EnableAutoFormatting = true;
    options.AutoFormatMode = AutoFormatMode.OptOutAttribute; // Configure the auto-format mode
});

The filter intercepts successful OkObjectResult or raw objects returned from your controllers and dynamically formats numeric properties into human-readable strings using optimized ExpandoObject recursion.

⚠️ Swagger / OpenAPI Schema Warning

Enabling global auto-formatting filters (via EnableAutoFormatting = true) transforms response DTO payloads into dynamic ExpandoObject representations during MVC action execution. This causes Swagger/OpenAPI to lose strongly-typed schema metadata, rendering them as generic JSON objects in your Swagger UI page.

💡 Best Practice: If preserving exact Swagger schema models is critical for your client generation pipeline or API compliance, configure **Opt-in JSON Serialization** (via builder.Services.AddHumanNumbersJson() and explicit property attributes) rather than dynamic global filters.

AutoFormatMode Configurations

You can customize how aggressively numbers are automatically formatted by setting the AutoFormatMode enum:

Mode Name Description
AutoFormatMode.Off Auto-formatting is disabled entirely.
AutoFormatMode.OptInAttributeOnly (Default) Formats only properties decorated with the [HumanNumber] attribute.
AutoFormatMode.OptOutAttribute Formats all numeric properties automatically, *except* those explicitly marked with [NoHumanFormat].
AutoFormatMode.Global Automatically scales and formats every single numeric response property returned by the API.

Opt-Out with [NoHumanFormat]

When running in AutoFormatMode.OptOutAttribute mode, you can decorate sensitive DTO properties or database primary keys with [NoHumanFormat] to preserve their raw numeric representations:

public class AccountDetailsDto
{
    public string Owner { get; set; } = string.Empty;

    public decimal Balance { get; set; } // Formatted to "4.2M" in Opt-Out mode

    [NoHumanFormat]
    public decimal InternalAccountNumber { get; set; } // Kept as raw numeric: 9812401
}

Circular Reference & Schema Note

Safety Built-in: The response transformation filter utilizes a local backtracking reference tracker to prevent circular reference stack overflows. It dynamically yields the raw unformatted object reference if a loop is detected.

Built-in JSON Converters

Alternatively, enable the high-performance global JSON serialization converters:

// Program.cs
builder.Services.AddHumanNumbersJsonGlobal(); // Formats all decimals, floats, and doubles in JSON

This registers the HumanNumberJsonConverterFactory, intercepting serialization directly at the System.Text.Json layer without mutating objects or schemas on the heap.

Controller Example

A complete Web API controller showcasing auto-formatted results:

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("api/dashboard")]
public class DashboardController : ControllerBase
{
    [HttpGet("metrics")]
    public IActionResult GetMetrics()
    {
        return Ok(new DashboardMetrics
        {
            GrossRevenue = 12500450m,    // Formats to "12.50M"
            TransactionCount = 845000,   // Formats to "845.00K"
            NetProfitMargin = 0.155m     // Preserves default percentage precision
        });
    }
}

Attribute-Based Formatting

Decorate model properties with specific layout attributes to gain fine-grained control over JSON outputs while keeping DB types numeric.

Available Attributes

Decorating model or DTO properties with [HumanNumber] lets you control property-level rendering explicitly:

public class SalesRecordDto
{
    public string Product { get; set; } = string.Empty;

    // Forces formatting during JSON serialization
    [HumanNumber(OutputMode = HumanNumberOutputMode.SerializeAsHuman, DecimalPlaces = 1)]
    public decimal MonthlySales { get; set; } // JSON output: "45.8K"

    // Formats directly into currency based on context
    [HumanNumber(OutputMode = HumanNumberOutputMode.SerializeAsHuman, IsCurrency = true)]
    public decimal RetailPrice { get; set; } // JSON output: "$1,250.00"
}

Real-World Scenarios

You can combine property attributes with named formatting policies defined in your Startup:

// Decorate using named policies
public class AnalyticsReport
{
    [HumanNumber(PolicyName = "Financial")]
    public decimal DailyLiquidity { get; set; } // Applies the "Financial" policy configurations

    [HumanNumber(PolicyName = "Chinese")]
    public decimal RegionalExposure { get; set; } // Serializes using Chinese Wan/Yi magnitudes
}

Configuration Options

The [HumanNumber] attribute supports the following parameters:

Parameter Type Description
OutputMode HumanNumberOutputMode Determines if the property is serialized as a human-readable string (SerializeAsHuman) or formatted only as metadata.
DecimalPlaces int Overrides global decimal precision on a per-property basis.
IsCurrency bool Forces currency symbol and alignment logic on the property value.
PolicyName string Binds formatting to a named policy configured globally in DI.

⚠️ Upgrading from v1.x: Deprecated Aliases

The old [ShortNumberFormat] attribute is now marked as obsolete. It acts as a direct, backward-compatible alias for [HumanNumber(OutputMode = HumanNumberOutputMode.SerializeAsHuman)]. This alias will be completely removed in version 3.0.0. We recommend developers transition to the standardized [HumanNumber] attribute configuration:

// Legacy usage:
[ShortNumberFormat(2)]
public decimal Sales { get; set; }

// Modern usage:
[HumanNumber(OutputMode = HumanNumberOutputMode.SerializeAsHuman, DecimalPlaces = 2)]
public decimal Sales { get; set; }

ASP.NET Core Integration

HumanNumbers integrates deeply with the ASP.NET Core pipeline, from JSON serialization to Razor TagHelpers.

Integration Setup

Register HumanNumbers in your Program.cs:

builder.Services.AddHumanNumbersCore(options => {
    options.DefaultDecimalPlaces = 2;
    options.EnableLogging = true;
});

// Enable automatic JSON formatting (Attribute-driven by default)
builder.Services.AddHumanNumbersJson();

// Or enable GLOBAL JSON formatting for all numeric types
// builder.Services.AddHumanNumbersJsonGlobal();

// Enable MVC Filter for auto-formatting controller results
builder.Services.AddHumanNumbersMvc();

JSON Serialization

Control how numeric properties are serialized using the [HumanNumber] attribute:

public class DashboardDto
{
    // Mode: FormatOnly (Default)
    // The value remains numeric in JSON, but metadata is available for UI filters
    [HumanNumber]
    public decimal Revenue { get; set; }

    // Mode: SerializeAsHuman
    // The value is transformed into a string ("1.25M") during serialization
    [HumanNumber(OutputMode = HumanNumberOutputMode.SerializeAsHuman)]
    public decimal TotalSales { get; set; }
}

TagHelpers

Use declarative TagHelpers in your Razor views for clean, consistent formatting:

<!-- Basic Number -->
<hn-number value="@Model.Users" decimal-places="1" /> <!-- 48.4K -->

<!-- Currency -->
<hn-currency value="@Model.Price" currency-code="EUR" /> <!-- 1.5M € -->

<!-- Financial Words (for checks/contracts) -->
<hn-check value="@Model.Amount" /> <!-- One Thousand Five Hundred and 00/100 -->

<!-- Technical Metrics -->
<hn-bytes value="@Model.FileSize" /> <!-- 1.2 MiB -->

<!-- Financial Spreads -->
<hn-basis-points value="@Model.Yield" /> <!-- 25 bps -->

Middleware & Results

HumanNumbers provides specialized result types for Minimal APIs and auto-formatting filters:

Minimal API Results

app.MapGet("/api/stats", () => {
    var stats = new { Revenue = 1500000m, Users = 45000 };
    
    // Returns JSON with formatted strings
    return Results.Extensions.Human(stats, options => options.Strict());
});

Request Culture Resolution

HumanNumbers automatically integrates with RequestLocalizationOptions. It resolves culture from the Accept-Language header, query strings, or cookies via the standard ASP.NET Core IRequestCultureFeature.

Advanced Configuration

Fine-tune the formatting engine for specialized business domains and international markets.

Named Policies

Define reusable formatting policies to maintain consistency across your application:

// Registration in Program.cs
builder.Services.AddHumanNumbersCore(options => {
    options.AddPolicy("StrictFinancial", p => p.Strict().WithCurrency("USD"));
    options.AddPolicy("Technical", p => p.WithDecimalPlaces(3).AlwaysShowSuffix());
});

// Usage via Fluent API
val.Format().UsingPolicy("StrictFinancial").ToHuman();

// Usage via Attribute
[HumanNumber(PolicyName = "StrictFinancial")]
public decimal Balance { get; set; }

Built-in Policies Reference

HumanNumbers ships with several predefined named policies for common use cases:

Policy Name Key Configuration Best Use Case
"Default" DecimalPlaces = 2 Standard numeric UI fields and general displays.
"Dashboard" DecimalPlaces = 0, PromotionThreshold = 0.9 Compact high-level metrics, overview panels, and charts.
"Financial" DecimalPlaces = 2, Threshold = 1000 Standard portfolios, accounting tables, and transactional DTOs.
"StrictFinancial" DecimalPlaces = 2, ErrorMode = Strict SaaS checkout APIs and financial models where failure must throw.
"PublicApi" DecimalPlaces = 1, Threshold = 1000 High-throughput JSON APIs feeding mobile or third-party clients.

Custom Suffixes & Magnitudes

Override standard 3-digit scaling for unique numbering systems:

// Custom string suffixes (auto-mapped to 10^3, 10^6, etc.)
options.CustomSuffixes = new[] { "thou", "lac", "cr" }; // Indian numbering

// Manual Magnitude Thresholds (Full Control)
options.CachedCustomSuffixes = new[] {
    new MagnitudeSuffix(1_000_000m, "M"),
    new MagnitudeSuffix(500_000m, "Half-M"), // Custom mid-tier magnitude
    new MagnitudeSuffix(1_000m, "K"),
    new MagnitudeSuffix(1m, "")
};

Culture & Localization

Specific configuration for international markets like China:

Case Study: Chinese 10,000-based Scaling

In Chinese (zh-CN), numbers are scaled by 10,000 (万, Wan) and 100,000,000 (亿, Yi).

var zhOptions = new HumanNumberFormatOptions
{
    Culture = new CultureInfo("zh-CN"),
    CachedCustomSuffixes = new[]
    {
        new MagnitudeSuffix(100_000_000m, "亿"), // 100 Million
        new MagnitudeSuffix(10_000m, "万"),     // 10 Thousand
        new MagnitudeSuffix(1m, "")
    }
};

1250000m.ToHuman(zhOptions); // "125.00万"

Architecture Overview

HumanNumbers follows a layered architecture designed for performance, extensibility, and seamless integration with modern ASP.NET Core applications. The architecture separates concerns while maintaining a clean, intuitive API surface.

Layered Architecture Model

The system is organized into three distinct layers, each with specific responsibilities:

1. Formatting Engine (Core)

The foundation layer containing all number formatting logic and performance optimizations.

  • Zero-allocation Span<char> operations
  • Stack-based formatting with fallback allocation
  • Culture-aware number formatting
  • Currency, basis points, and specialized formatters
  • Performance-critical JIT optimizations

2. ASP.NET Integration Layer

Middleware and integration components for seamless ASP.NET Core adoption.

  • JSON serialization converters and factories
  • MVC model binding and validation integration
  • Dependency injection service registration
  • TagHelper implementations for Razor views
  • Culture resolution and configuration management

3. Presentation Layer (UI / JSON / Razor)

User-facing components and output formatting for different presentation contexts.

  • Razor TagHelpers for declarative formatting
  • JSON API response formatting
  • Error handling and validation feedback
  • Localization and internationalization support
  • Real-time dashboard and analytics integration

Design Principles

The architecture is guided by several core principles that ensure reliability and performance:

Zero Allocation by Default

All formatting operations use stack-based allocation unless the result exceeds buffer size.

Never-Throw Guarantee

UI formatting never throws exceptions in production paths, ensuring application stability.

Configuration Over Convention

Explicit configuration options with sensible defaults for different scenarios.

Integration First

Designed for seamless integration with existing ASP.NET Core applications.

Data Flow

Understanding how data flows through the architecture helps with debugging and optimization:

1. Input Data Source decimal, int, double, long, etc. 2. Formatting Engine (Core) Zero-allocation Span<char> ops Culture-aware formatting logic 3. Integration Layer JSON Serialization Pipeline TagHelpers & DI Services 4. Presentation Layer HTTP Response & Razor Views Human-readable UI output

Why This Architecture?

This layered approach provides clear separation of concerns, making the system easier to test, maintain, and extend. Each layer can be optimized independently while maintaining consistent behavior across all integration points.

Core Library

The HumanNumbers core library provides comprehensive number formatting capabilities with zero-allocation performance and extensive customization options.

Never-Throw Philosophy

HumanNumbers is designed for UI safety. Formatting never throws exceptions in production paths. This is a fundamental guarantee - invalid input results in safe fallback values, never crashes. This ensures your application remains stable even with malformed user input or unexpected data.

Overview

The core library is built for maximum performance and flexibility, supporting:

Number Formatting

  • Compact notation (K, M, B, T)
  • Custom decimal precision
  • Culture-aware formatting
  • Zero-allocation operations

Specialized Formatters

  • Roman Numerals for classic notation
  • Byte Scaling (Binary/Decimal)
  • Bidirectional Parsing
  • Financial-grade rounding

Number Formatting

Basic number formatting with compact notation across all numeric types:

// Extension methods for all numeric types
1500000m.ToHuman();        // "1.5M"
(48000).ToHuman();         // "48K"
(1.2345e9).ToHuman(1);     // "1.2B"

// Strict vs Default Promotion
// Default (0.95) promotes slightly early for "approaching" UI
950000.ToHuman();          // "0.95M"

// Strict (1.0) waits for the exact magnitude
950000.ToHuman(new HumanNumberFormatOptions { PromotionThreshold = 1.0m }); // "950K"

// Inline fluent override
1500000.Format().Strict().ToHuman(); // "1.5M" (same, but waits for 1M before M)

Why Early Promotion?

By default, HumanNumbers promotes suffixes at 95% of the next magnitude (e.g., 950K -> 0.95M). This is standard in many financial dashboards to provide a "leading indicator" of progress. Use Strict() mode when absolute mathematical correctness is required for accounting.

Currency Formatting

Currency-aware formatting with automatic symbol detection:

// Uses CurrentCulture default currency symbol
1500000.ToHumanCurrency();                    // "$1.5M" (en-US)

// Explicit ISO codes
1500000.ToHumanCurrency("EUR");               // "1.5M EUR"

// Force specific culture
1500000.ToHumanCurrency(culture: new CultureInfo("fr-FR")); // "1,5M €"

Roman Numerals

Classic Roman numeral notation for dates, chapters, and stylistic formatting:

using HumanNumbers.Roman;

2024.ToRoman();    // "MMXXIV"
14.ToRoman();      // "XIV"
9.ToRoman();       // "IX"

Why Roman Numerals?

Essential for document management systems, archival software, or creative applications where traditional numbering adds authority or aesthetic value.

Byte Scaling

Technical application scaling with support for both Binary (IEC) and Decimal (SI) prefixes:

using HumanNumbers.Bytes;

// Binary Prefixes (Base 1024) - Library Default
(1024L).ToHumanBytes();            // "1 KiB"
(1048576L).ToHumanBytes();         // "1 MiB"

// Decimal Prefixes (Base 1000)
(1000L).ToHumanBytes(useBinaryPrefixes: false); // "1 KB"

// Mixed types
double size = 1572864;
size.ToHumanBytes(1);             // "1.5 MiB"

Binary vs. Decimal Scaling

Software developers typically expect binary scaling (1024), where 1 MiB is 1,048,576 bytes. Storage manufacturers and network providers often use decimal scaling (1000). HumanNumbers defaults to binary prefixes for technical accuracy in software contexts.

Parsing & Conversion

Bidirectional formatting allows you to convert human-readable strings back into numeric values:

// Parse compact strings
decimal val1 = HumanNumber.Parse("1.5M");    // 1500000
decimal val2 = HumanNumber.Parse("$48K");    // 48000

// Culture-aware parsing
decimal val3 = HumanNumber.Parse("1,5M", new CultureInfo("fr-FR")); // 1500000

// Safe parsing
if (HumanNumber.TryParse("invalid", out decimal result)) {
    // Handle success
}

Why Bidirectional?

This is critical for data round-tripping. If a user sees "1.5M" in a dashboard and enters "1.2M" into a filter or input field, your backend must be able to resolve that back to a raw decimal for database queries or calculations.

Supported Types

HumanNumbers provides generic extensions for all .NET numeric types, including:

decimal
double
float
int
long
short
byte
uint/ulong

Financial Formatting

Specialized formatters for high-precision financial instruments, market metrics, and legal documents.

Overview

Standard number formatting is often insufficient for financial domains. HumanNumbers provides three key specialized formats:

Basis Points (BPS)

Formatting for interest rates, bond yields, and spreads where 1 bps = 0.01%:

using HumanNumbers.Financial;

0.0125m.ToHumanBps();      // "125 bps"
0.0025m.ToHumanBps();      // "25 bps"
0.01257m.ToHumanBps(1);    // "125.7 bps"

// Rounding control
0.01257m.ToHumanBps(0, MidpointRounding.ToEven); // "126 bps"

Why Basis Points?

Basis points eliminate ambiguity in percentage discussions. A "1% increase" on a 5% rate could mean 5.05% or 6%. A "100 bps increase" always means moving from 5% to 6%.

Fractional Pricing

Formatting for instruments traded in fractions (e.g., US Treasuries in 32nds):

using HumanNumbers.Financial;

// Default 32nds (Treasury Style)
99.5m.ToFractionalPrice();    // "99 16/32"

// Eighths (Stock Style)
101.125m.ToFractionalPrice(8); // "101 1/8"

// Mixed precision
98.03125m.ToFractionalPrice(); // "98 1/32"

Why Fractional Pricing?

US government bonds and certain commodity markets still trade and report prices in fractions rather than decimals. HumanNumbers ensures your UI matches industry standards.

Financial Words

Spelling out numbers for check-writing, invoices, and legal contracts:

using HumanNumbers.Financial;

1500.50m.ToHumanWords(); 
// "One Thousand Five Hundred and 50/100"

1234.00m.ToHumanWords("Dollars", "Dollar");
// "One Thousand Two Hundred Thirty Four Dollars and 00/100"

-45.20m.ToHumanWords("Euros");
// "Minus Forty Five Euros and 20/100"

Why Words?

Spelled-out amounts are legally required on physical checks and many formal contracts to prevent fraud (as words are harder to alter than digits).

Extensible Words Engine (IWordsProvider)

Need to spell out checks or invoices in Spanish, French, or another language? HumanNumbers' spelling engine is completely extensible. Simply implement the IWordsProvider interface:

public class FrenchWordsProvider : IWordsProvider
{
    public string ToWords(decimal value) => /* custom translation logic */;
    public string NegativeWord => "Moins";
    public string ConjunctionWord => "et";
}

// Pass your custom provider directly to the formatter
var checkAmount = 1500.50m;
var frenchCheck = checkAmount.ToHumanWords("Euros", "Euro", new FrenchWordsProvider());
// "Mille Cinq Cents Euros et 50/100"

Financial JSON Serialization

For capital markets APIs, register the specialized financial JSON serialization pipeline during application startup:

// Program.cs - Register specialized financial JSON converters
builder.Services.AddHumanFinancialFormatters(options => {
    options.BasisPoints.Decimals = 1;
    options.Fractions.MinFraction = 32;
});

Once registered, decorate DTO or model properties with specialized attributes to serialize values directly into basis points or fractional price notation natively, without any runtime overhead:

public class TreasuryBondDto
{
    public string CUSIP { get; set; } = string.Empty;

    // Formats directly to standard bps string in JSON: "125.7 bps"
    [BasisPoints(Decimals = 1, WriteAsString = true)]
    public decimal Spread { get; set; } = 0.01257m;

    // Formats US Treasury price to fractional notation in JSON: "99 16/32"
    [FractionPrice(WriteAsString = true)]
    public decimal BidPrice { get; set; } = 99.50m;
}

ASP.NET Core Integration

Seamless ASP.NET Core integration with dependency injection, JSON serialization, and TagHelpers for web applications.

Integration Overview

HumanNumbers provides comprehensive ASP.NET Core integration through dependency injection, JSON serialization, MVC enhancements, and TagHelpers. The integration is designed to be both powerful and developer-friendly while maintaining high performance characteristics.

Dependency Injection

Comprehensive DI services for formatting, validation, and culture resolution.

JSON Serialization

Attribute-driven and global JSON formatting with custom converter support.

MVC Integration

Enhanced model binding, validation, and action result formatting.

TagHelpers

Razor TagHelpers for declarative number formatting in views.

Choosing a Configuration Approach

HumanNumbers provides three layers of configuration to meet different needs. Understanding when to use each approach ensures clean, maintainable code:

1. Global Defaults

Set application-wide behavior during startup. Use for consistent formatting across your entire application.

// Program.cs - Global configuration
builder.Services.AddHumanNumbersDefaults();

// Or with custom defaults
builder.Services.AddHumanNumbersCore(options => 
{
    options.DefaultCulture = new CultureInfo("en-US"),
    options.ErrorMode = HumanNumbersErrorMode.Safe,
    options.EnableCaching = true
});

When to use: Most applications need consistent formatting rules across all endpoints and views.

2. Policies & Attributes

Define scenario-specific behavior through policies and model attributes. Use for different formatting needs in different contexts.

// Model attributes for JSON serialization
public class FinancialReport
{
    [HumanNumber(OutputMode = HumanNumberOutputMode.SerializeAsHuman)]
    public decimal Revenue { get; set; }
    
    [HumanNumber(IsCurrency = true, DecimalPlaces = 2)]
    public decimal Expenses { get; set; }
}

// Policy-based configuration
builder.Services.AddHumanNumbersCore(options => 
{
    options.Policies["Financial"] = new HumanNumberFormatOptions 
    { 
        DecimalPlaces = 2, 
        UseMinPrecision = false 
    };
    options.Policies["Technical"] = new HumanNumberFormatOptions 
    { 
        DecimalPlaces = 1, 
        UseMinPrecision = true 
    };
});

When to use: Different APIs or views need different formatting rules (financial vs technical, public vs internal).

3. Per-Call Options

Override behavior for specific formatting operations. Use for one-off formatting needs or dynamic requirements.

// Manual formatting with custom options
var result = value.ToHuman(new HumanNumberFormatOptions 
{ 
    DecimalPlaces = 3, 
    Culture = new CultureInfo("fr-FR") 
});

// Service-based formatting
var formatter = serviceProvider.GetRequiredService();
var formatted = formatter.Format(value, "FinancialPolicy");

// Span-based formatting for maximum performance
Span buffer = stackalloc char[32];
var success = value.TryFormatHuman(buffer, out var written, out var result);

When to use: Specific scenarios need different formatting than global or policy-based settings.

Configuration Hierarchy

The three layers follow a clear hierarchy: Per-call options override policies, which override global defaults. This allows you to start with sensible global behavior and selectively override where needed, keeping your code DRY while maintaining flexibility.

Integration Setup

Get HumanNumbers running in your ASP.NET Core application with a single line of code.

// Program.cs - Single line setup
builder.Services.AddHumanNumbersDefaults();

// This enables:
// - JSON serialization with [HumanNumber] attributes
// - MVC model binding and validation
// - TagHelpers for Razor views
// - Dependency injection services
// - Culture-aware formatting

// Manual configuration
builder.Services.AddHumanNumbersCore(options => 
{
    options.DefaultCulture = new CultureInfo("en-US"),
    options.ErrorMode = HumanNumbersErrorMode.Safe,
    options.EnableCaching = true
});

Why Defaults?

AddHumanNumbersDefaults() provides sensible defaults for most applications. It enables safe-by-default JSON formatting, comprehensive DI services, and automatic culture resolution without requiring extensive configuration.

JSON Serialization

Attribute-driven JSON formatting with compile-time safety:

public class FinancialReport
{
    // Metadata only - no JSON transformation
    [HumanNumber]
    public decimal Revenue { get; set; }
    
    // Explicit opt-in for JSON formatting
    [HumanNumber(OutputMode = HumanNumberOutputMode.SerializeAsHuman)]
    public decimal DisplayRevenue { get; set; }
    
    // Currency formatting with opt-in
    [HumanNumber(
        OutputMode = HumanNumberOutputMode.SerializeAsHuman,
        IsCurrency = true,
        CurrencyCode = "USD"
    )]
    public decimal UsdRevenue { get; set; }
}

Corresponding JSON output:

{
  "revenue": 1500000.00,
  "displayRevenue": "1.50M",
  "usdRevenue": "$1.50M"
}

Zero-Overhead Global Mode

Global mode uses a single JsonConverterFactory for all numeric types, eliminating per-property overhead. This provides maximum performance for high-volume APIs but reduces fine-grained control.

TagHelpers

Declarative formatting in Razor views:

@addTagHelper *, HumanNumbers.AspNetCore

<!-- Basic number formatting -->
<hn-number value="1500000" />                    <!-- 1.5M -->
<hn-number value="1500000" decimal-places="2" /> <!-- 1.50M -->

<!-- Currency formatting -->
<hn-currency value="1500000" />                  <!-- $1.5M -->
<hn-currency value="1500000" currency="EUR" />   <!-- 1.5M EUR -->

<!-- Financial words -->
<hn-check value="1500" />                        <!-- One Thousand Five Hundred -->
<hn-check value="1500" currency="USD" />         <!-- One Thousand Five Hundred Dollars -->

<!-- Byte scaling -->
<hn-bytes value="1048576" />                      <!-- 1.0 MB -->
<hn-bytes value="1000000" binary="false" />       <!-- 1.0 MB -->

<!-- Basis points -->
<hn-basis-points value="0.015" />                <!-- 150 bps -->

Razor View Components

In addition to TagHelpers, HumanNumbers ships with a built-in HumanNumberViewComponent for scenarios where TagHelpers cannot be cleanly resolved or when you prefer a programmatic MVC syntax:

@await Component.InvokeAsync("HumanNumber", new { 
    value = Model.TotalRevenue, 
    decimalPlaces = 2, 
    isCurrency = true, 
    currencyCode = "USD" 
})

The View Component resolves the global HumanNumbersOptions automatically from the DI container to respect your custom defaults, while allowing per-call parameter overrides.

Dependency Injection

Advanced DI patterns and custom services:

// Inject services in controllers
public class FinancialController : ControllerBase
{
    private readonly IHumanNumberService _formatter;
    private readonly IHumanNumberValidator _validator;
    
    public FinancialController(
        IHumanNumberService formatter,
        IHumanNumberValidator validator)
    {
        _formatter = formatter;
        _validator = validator;
    }
    
    [HttpGet("format/{value}")]
    public IActionResult FormatNumber(decimal value)
    {
        var result = _formatter.FormatHuman(value);
        return Ok(new { value, formatted = result });
    }
}

// Custom formatter registration
builder.Services.AddSingleton(sp => 
    new CustomFinancialFormatter());

// Custom validator
public class CustomValidator : IHumanNumberValidator
{
    public ValidationResult Validate(decimal value, HumanNumberFormatOptions options)
    {
        if (value < 0)
            return ValidationResult.Error("Negative values not allowed");
        return ValidationResult.Success();
    }
}

⚡ Pluggable Currency Mapping DI (v2.0.2)

You can register a custom ICurrencyMappingProvider implementation in the DI container. This allows you to dynamically resolve and map arbitrary, context-specific regional keys (such as "EastAfrica" or "AsiaPacific") to standard ISO currency codes during JSON serialization or database formatting:

// 1. Implement the provider
public class RegionalCurrencyMappingProvider : ICurrencyMappingProvider
{
    public string MapKeyToCurrencyCode(string key)
    {
        return key switch
        {
            "EastAfrica" => "ETB",
            "AsiaPacific" => "CNY",
            "Europe" => "EUR",
            _ => "USD"
        };
    }
}

// 2. Register it as a singleton in Program.cs
builder.Services.AddSingleton<ICurrencyMappingProvider, RegionalCurrencyMappingProvider>();

Currency Dictionary Converter

Under the hood, HumanNumbers provides a specialized CurrencyDictionaryConverter for serializing Dictionary<string, decimal> into a dynamic set of formatted regional currency strings. It resolves the keys of your dictionaries via the registered ICurrencyMappingProvider automatically.

Simply decorate dictionary DTO properties to serialize them instantly into regional currencies:

public class RegionalReportDto
{
    // The dictionary keys match the currency provider regions
    [JsonConverter(typeof(CurrencyDictionaryConverter))]
    public Dictionary<string, decimal> RegionalBalances { get; set; } = new()
    {
        { "EastAfrica", 450000m },   // Serializes to formatted: "Br450.00K"
        { "Europe", 120000m }        // Serializes to formatted: "€120.00K"
    };
}

Error Handling

Comprehensive error handling with safe and strict modes:

// Safe mode (default) - returns empty strings
var safe = "invalid".ToHuman(HumanNumbersErrorMode.Safe);
// Result: ""

// Strict mode - throws exceptions
try
{
    var strict = "invalid".ToHuman(HumanNumbersErrorMode.Strict);
}
catch (HumanNumbersException ex)
{
    // Handle validation error
    _logger.LogError(ex, "Formatting failed");
}

// Global error handling
builder.Services.AddHumanNumbersCore(options => 
{
    options.CoreOptions.ErrorMode = HumanNumbersErrorMode.Safe
});

// Custom error middleware
public class HumanNumberErrorHandlingMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (HumanNumbersException ex)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new 
            { 
                error = "Invalid number format",
                details = ex.Message 
            });
        }
    }
}

Performance

Deep dive into HumanNumbers performance characteristics, memory allocation patterns, and optimization techniques for high-throughput applications.

Performance Overview

HumanNumbers is designed from the ground up for maximum performance with zero-allocation operations, Span<char> usage, and JIT optimizations. The library maintains consistent sub-microsecond formatting times even under high load.

Zero Allocation

Stack-based formatting for most operations

0 B

per operation (typical)

Formatting Speed

Average time per formatting operation

< 1µs

microseconds

Throughput

Operations per second on modern hardware

10M+

ops/sec

Memory & Allocation

Zero-allocation design with stack-based operations:

// Zero allocation for most operations
Span buffer = stackalloc char[32];
var success = 1500000.TryFormatHuman(buffer, out var written, out var result);
// No heap allocation - result points to stack buffer

// Single allocation for large results
var large = 1500000000000000.ToHuman(); // "1.5P"
// Only one string allocation for the result

// Span-based API for maximum performance
public static bool TryFormatHuman(
    this decimal value,
    Span destination,
    out int charsWritten,
    out string result)
{
    // Implementation uses stackalloc and Span
    // Zero allocation when result fits in buffer
}

Memory Profile

Typical operations use 0-32 bytes of stack space. Only when the formatted result exceeds the buffer size does a single string allocation occur. This design enables consistent performance in GC-sensitive applications.

Span Operations

High-performance Span<char> operations:

// Direct Span operations
public static void FormatToSpan(
    this decimal value,
    Span destination,
    out int charsWritten)
{
    // Fast path for common cases
    if (value >= 1000000 && value < 1000000000)
    {
        var millions = value / 1000000;
        var formatted = millions.ToString("G", CultureInfo.InvariantCulture);
        formatted.AsSpan().CopyTo(destination);
        destination[formatted.Length] = 'M';
        charsWritten = formatted.Length + 1;
        return;
    }
    
    // Fallback to full formatting
    // ... implementation
}

// Usage in high-throughput scenarios
Span buffer = stackalloc char[32];
foreach (var value in largeDataSet)
{
    value.FormatToSpan(buffer, out var written);
    // Process buffer without allocation
    ProcessFormatted(buffer.Slice(0, written));
}

// Direct zero-allocation data size formatting into a Span
Span byteBuffer = stackalloc char[64];
long fileSizeBytes = 1048576 * 50; // 50 MiB
if (fileSizeBytes.ToHumanBytes(byteBuffer, out var charsWritten, decimalPlaces: 2))
{
    var formattedBytes = byteBuffer.Slice(0, charsWritten);
    // formattedBytes is "50.00 MiB" without any heap allocation
}

Caching Strategies

Intelligent caching for repeated operations:

// Culture-specific caching
private static readonly ConcurrentDictionary<string, string> _formatCache = new();

public static string FormatCached(this decimal value, CultureInfo culture)
{
    var cacheKey = $"{culture.Name}:{value}";
    return _formatCache.GetOrAdd(cacheKey, _ => value.ToHuman(culture));
}

// Pattern-based caching
private static readonly Dictionary<string, Func<decimal, string>> _formatters = new()
{
    ["K"] = v => (v / 1000).ToString("F1") + "K",
    ["M"] = v => (v / 1000000).ToString("F1") + "M",
    ["B"] = v => (v / 1000000000).ToString("F1") + "B"
};

Caching Benefits

Caching provides 10-100x performance improvements for repeated formatting operations with the same values and cultures. Memory overhead is minimal due to string interning and LRU eviction policies.

JIT Optimizations

Runtime optimizations and aggressive inlining:

// Aggressive inlining for hot paths
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static string ToHuman(this decimal value)
{
    // Fast path for common cases
    if (value >= 1000 && value < 1000000)
        return FormatThousands(value);
    if (value >= 1000000 && value < 1000000000)
        return FormatMillions(value);
    
    // Fallback for edge cases
    return FormatGeneric(value);
}

// Branchless operations
public static int GetScale(decimal value)
{
    // Branchless scale calculation
    var abs = Math.Abs(value);
    var scale = (int)Math.Log10(abs) / 3;
    return Math.Min(scale, 4); // Max P for quadrillion
}

// SIMD-optimized operations (where applicable)
public static void FormatBatch(
    ReadOnlySpan values,
    Span results)
{
    // Vectorized formatting for large batches
    // Takes advantage of CPU vector instructions
}

Benchmarks

Performance comparisons and metrics:

// BenchmarkDotNet results
| Method                    | Mean      | Allocated |
|--------------------------|----------|----------|
| HumanNumbers.ToHuman      | 85.21 ns | 0 B      |
| String.Format("N0")       | 342.15 ns| 96 B     |
| ToString("N0")            | 287.43 ns| 96 B     |
| CustomFormatter          | 156.78 ns| 32 B     |

// Large dataset performance
| Dataset Size | HumanNumbers | String.Format | Improvement |
|--------------|---------------|--------------|-------------|
| 1K items     | 0.085 ms      | 0.342 ms     | 4.0x        |
| 10K items    | 0.852 ms      | 3.421 ms     | 4.0x        |
| 100K items   | 8.521 ms      | 34.215 ms    | 4.0x        |
| 1M items     | 85.21 ms      | 342.15 ms    | 4.0x        |

// Memory allocation comparison
| Operation          | HumanNumbers | Standard Format |
|--------------------|---------------|-----------------|
| Single format      | 0 B           | 96 B           |
| 1K formats         | 0 B           | 93.75 KB       |
| 1M formats         | 0 B           | 91.55 MB       |

Benchmark Insights

HumanNumbers consistently outperforms standard formatting by 4x while using zero memory. The performance advantage increases with dataset size due to reduced GC pressure.

Real-World Scenarios

Performance in production environments:

// High-frequency trading dashboard
public class TradingDashboard
{
    private readonly Span _buffer = stackalloc char[32];
    
    public void UpdatePrices(ReadOnlySpan updates)
    {
        foreach (var update in updates)
        {
            // Zero allocation formatting for real-time display
            update.Price.TryFormatHuman(_buffer, out var written, out var formatted);
            DisplayPrice(update.Symbol, formatted);
        }
    }
}

// Financial report generation
public class ReportGenerator
{
    public void GenerateReport(IEnumerable data)
    {
        var report = new StringBuilder();
        foreach (var item in data)
        {
            // Batch formatting with minimal allocation
            report.AppendLine($"{item.Name}: {item.Value.ToHuman()}");
        }
        // Single allocation for final report
        return report.ToString();
    }
}

// API response formatting
public class FinancialController : ControllerBase
{
    [HttpGet("portfolio/{id}")]
    public IActionResult GetPortfolio(int id)
    {
        var portfolio = _service.GetPortfolio(id);
        
        // Zero allocation JSON formatting
        return Ok(new
        {
            portfolio.Name,
            TotalValue = portfolio.TotalValue.ToHuman(),
            Positions = portfolio.Positions.Select(p => new
            {
                p.Symbol,
                Value = p.Value.ToHuman(),
                Change = p.Change.ToBasisPoints()
            })
        });
    }
}

Next Steps

You're now equipped with comprehensive knowledge of HumanNumbers. Here's how to continue your journey and make the most of the library:

Explore Samples

Dive into real-world examples and implementation patterns:

Financial Dashboard

Complete ASP.NET Core dashboard with real-time metrics and charts.

  • Real-time portfolio tracking
  • Currency conversion displays
  • Performance metrics visualization

API Integration

RESTful API with JSON serialization and model validation.

  • Public API responses
  • Mobile app endpoints
  • Error handling patterns

High-Performance Systems

Zero-allocation patterns for high-throughput scenarios.

  • Trading platforms
  • Telemetry systems
  • Real-time analytics

Enterprise Applications

Large-scale enterprise integration patterns.

  • Multi-tenant systems
  • Internationalization
  • Compliance reporting

Releases & Support

Stay up to date with the latest features and improvements:

Semantic Versioning

HumanNumbers follows SemVer 2.0 for predictable versioning:

  • Major (X.0.0): Breaking changes - requires code updates
  • Minor (X.Y.0): New features - backward compatible
  • Patch (X.Y.Z): Bug fixes - drop-in replacement

Always review release notes before upgrading major versions.

Release Channels

Choose the right release channel for your needs:

  • Stable: Production-ready releases with full support
  • Preview: Feature-complete releases testing stability
  • Nightly: Latest features for early adopters

Support & Community

Get help and contribute to the project:

  • GitHub Discussions for questions and ideas
  • GitHub Issues for bug reports and feature requests
  • Stack Overflow with humannumbers tag
  • Discord community for real-time discussions

Contributions Welcome

HumanNumbers thrives on community contributions. Here's how you can help:

Code Contributions

  • Bug fixes and performance improvements
  • New formatting features
  • Integration with other frameworks
  • Documentation enhancements

Non-Code Contributions

  • Reporting bugs and usability issues
  • Improving documentation
  • Sharing examples and use cases
  • Community support and mentoring

Development Guidelines

All contributions should maintain the library's core principles: zero-allocation performance, never-throw safety, and clean API design. Check the contributing guidelines for detailed requirements and the development setup process.

Thank You

Thank you for choosing HumanNumbers for your number formatting needs. We're committed to providing the best possible experience for developers building high-performance, user-friendly applications.

Happy Formatting!

Join thousands of developers using HumanNumbers in production applications worldwide.

Core Library Reference

Deep dive into the types and members of the HumanNumbers core engine.

HumanNumber Extensions

The primary entry point for the library, providing static methods and extension methods for all numeric types.

Member Description Returns
ToHuman() Formats any numeric type into a human-readable string (e.g., 1.5M). string
TryToHuman() Resilient no-throw formatting. Outputs the formatted string in out string result; returns false on failure. bool
ToHumanCurrency() Formats a number as a human-readable currency (e.g., $1.5M). string
TryToHumanCurrency() Resilient no-throw currency formatting. Outputs currency in out string result; returns false on failure. bool
TryParse() Attempts to parse a human-readable string (e.g., "1.5M") back to a decimal. bool
Parse() Parses a human-readable string, throwing a FormatException on failure. decimal
Format(decimal) Begins a fluent formatting pipeline starting with the specified value. FormattingContext
Configure(Action) Global entry point for configuring library-wide defaults and policies. void

FormattingContext (Fluent Pipeline)

Returned by Format(), these methods allow fluent chaining of formatting rules and targets.

Method Description Returns
UsingPolicy(string name) Applies a registered named formatting policy to the context. FormattingContext
UsingOptions(HumanNumberFormatOptions options) Applies custom inline formatting options to the context. FormattingContext
UsingCulture(CultureInfo culture) Sets the formatting culture context (e.g., en-US, fr-FR). FormattingContext
ToHuman() Terminal operation. Evaluates the fluent pipeline and returns the human-readable string. string

Extension Targets

Primitive Types

decimal, double, float, int, long, short, byte, uint, ulong, ushort, sbyte

Modern Types

BigInteger, INumber<T> (.NET 7+), and all Nullable<T> variants.

HumanNumberFormatOptions

The core configuration record that controls all formatting logic.

Property Type Default Description
DecimalPlaces int 2 Number of digits after the decimal separator.
Threshold decimal 1,000 Minimum absolute value before suffixing begins.
PromotionThreshold decimal 0.95 Factor (0.0 to 1.0) for early promotion to next magnitude.
AlwaysShowSuffix bool false If true, suffix is shown even for small numbers (e.g., 500 becomes 0.50K).
SuppressDefaultDecimals bool true Removes .00 from numbers that are not scaled and have no fractional part.
CurrencySymbol string? null Manual override for the currency symbol. If null, culture is used.
CurrencyPosition Enum Before Placement of the symbol (Before/After).
ErrorMode Enum SafeFallback Behavior on error: returns unformatted string or throws.

Global Runtime Configuration (HumanNumbersConfig)

The singleton configuration instance available via HumanNumbersConfig.Instance allows runtime management and inspection of formatting policies.

Member Type Description
Instance Static Property Gets the singleton configuration instance.
GlobalOptions Property Gets or sets the global default formatting options.
AddPolicy(string, HumanNumberFormatOptions) Method Registers a named formatting policy.
TryGetPolicy(string, out HumanNumberFormatOptions) Method Attempts to retrieve a registered named policy.
GetPolicyNames() Method Returns an enumerable list of all currently registered named policies.

Financial Extensions

Advanced formatting for capital markets, check-writing, and complex financial reporting.

Method Target Description
ToHumanBps() decimal Converts value to basis points (e.g., 0.0125 → "125 bps").
ToHumanFraction() decimal Converts decimal to Treasury-style fractions (e.g., 10.03125 → "10 1/32").
ToHumanWords() decimal Spells out numbers for checks (e.g., "One Thousand Two Hundred and 50/100").
RoundToTick(decimal, TickRoundingMode) decimal Rounds to the nearest tick size (e.g., 0.05, 0.001) for market orders. Supports TickRoundingMode (Nearest, Up, Down).

Bytes & Roman

Technical and scientific formatting for data sizes and historic numeric representations.

Data Size (Bytes)

ToHumanBytes() EXT

Formats long/ulong counts into binary (KiB, MiB) or decimal (KB, MB) units.

useBinaryPrefixes bool (Default: true)
decimalPlaces int (Default: 2)

Roman Numerals

ToRoman() EXT

Converts integers between 1 and 3,999 into standard Roman numerals.

2024.ToRoman() // "MMXXIV"

ASP.NET Core Reference

Deep dive into the integration components for web applications and APIs.

Attributes & Enums

HumanNumberAttribute

Property Type Description
DecimalPlaces int Number of digits for this property.
PolicyName string? Named policy to apply.
IsCurrency bool Forces currency formatting.
OutputMode Enum Controls if JSON is transformed (SerializeAsHuman).

HumanNumberOutputMode

FormatOnly

Metadata only. Preserves numeric JSON serialization.

SerializeAsHuman

Transforms JSON value to string (e.g., 1500 → "1.5K").

DI & Service Reference

Service Collection Extensions

AddHumanNumbersDefaults()

The recommended setup. Configures Core and JSON converters with safe defaults.

AddHumanNumbersJsonGlobal()

Opinionated global mode: formats EVERY numeric type in EVERY JSON response.

HumanNumbersOptions

Configuration options for ASP.NET Core integration registration:

Property Type Default Description
DefaultDecimalPlaces int 2 Default number of decimal places to include in formatted outputs.
DefaultErrorMode HumanNumbersErrorMode SafeFallback Configures default behavior (SafeFallback vs Strict) on formatting errors.
EnableLogging bool true Determines whether to log formatting errors automatically to standard ILogger.
EnableAutoFormatting bool false Enables MVC global response formatting action filters.
AutoFormatMode AutoFormatMode OptInAttributeOnly Determines the auto-formatting behavior level (Off, OptIn, OptOut, Global).
RespectDataAnnotations bool true If true, respects standard formatting data annotation properties like [DisplayFormat].
DefaultPolicyName string "Default" The named policy to fall back on when resolving format options.

IHumanNumberService

Inject this interface to format numbers manually within your business logic or services.

Method Description
Format(decimal value, int? decimalPlaces = null, string? culture = null) Formats a value according to the current policy, with optional decimal and culture overrides.
FormatCurrency(decimal value, string? currencyCode = null, int? decimalPlaces = null) Formats a currency value using the specified ISO code and decimal precision overrides.

Razor TagHelpers

<hn-number> Renders as <span>
value decimal (Required)
decimal-places int?
class string?
<hn-currency> Renders as <span>
value decimal (Required)
currency-code string? (USD, EUR...)
decimal-places int?

Minimal API Results

Extension methods for the IResultExtensions (Results) object.

// Available via Microsoft.AspNetCore.Http.Results.Extensions
Results.Extensions.Human(obj, decimalPlaces: 2);
Results.Extensions.HumanOk(obj, options => { ... });