Millify is a small .NET library that turns large counts and measurements into short, readable labels (2.5K, 1.44M, 1Mi, …). Use it anywhere you would otherwise hand-roll suffix logic: analytics tiles, tables, logs, charts, and mobile or web API payloads.
The NuGet package is named millify. This repository is millify-dotnet; the API lives in the MillifyDotnet namespace.
Conceptually similar to the millify package for JavaScript.
- One-call formatting via
Millify.Shortenforlong,double,decimal, andBigInteger. - Decimal (1000) or binary (1024) scaling with sensible default suffixes, including IEC-style units for byte-style values.
- Split “scale then format” via
Millify.DecomposeandMillify.FormatScaled(orMillifiedNumber.ToFormattedString) when you need the numeric magnitude for tooltips, charts, tests, or accessibility text. - Culture-aware decimal separators (separator only; digit grouping stays off so labels stay compact).
- Optional
Span<char>output viaMillify.TryFormatfor fixed buffers and reduced allocations. - Tunable presentation: precision, “smart” precision by magnitude, trimming of insignificant fractional zeros, custom unit lists, spacing before the suffix, and casing.
Package ID: millify
dotnet add package millifyPackage Manager Console:
Install-Package millifyOr add a PackageReference in your project file (pin a version in production apps):
<PackageReference Include="millify" Version="1.1.0" />using MillifyDotnet;
Millify.Shorten(2500); // 2.5K
Millify.Shorten(1_500_000); // 1.5M
Millify.Shorten(-5_300_000); // -5.3M
Millify.Shorten(1234.5m); // 1.2K (default precision is 1)Default options trim trailing fractional zeros (for example 1.0M becomes 1M). Set TrimInsignificantZeros = false on MillifyOptions if you want a fixed-width fractional part.
var options = new MillifyOptions(
precision: 2,
lowercase: true,
spaceBeforeUnit: true,
units: new[] { "", "k", "m", "b", "t" });
Millify.Shorten(1_440_000, options); // "1.44 m"When ScaleBase is Binary and you do not pass units, suffixes default to IEC-style (Ki, Mi, Gi, …) with appropriate casing.
var bytes = new MillifyOptions(precision: 2, scaleBase: MillifyScaleBase.Binary);
Millify.Shorten(1_048_576, bytes); // 1MiCulture affects the decimal separator only (for example 1,2K with fr-FR). Thousands separators are not inserted.
using System.Globalization;
using MillifyDotnet;
var fr = new MillifyOptions(precision: 1, culture: CultureInfo.GetCultureInfo("fr-FR"));
Millify.Shorten(1234.5m, fr); // "1,2K" — comma as decimal separatorusing System.Globalization;
using MillifyDotnet;
var options = new MillifyOptions(precision: 2, culture: CultureInfo.GetCultureInfo("fr-FR"));
var parts = Millify.Decompose(9_990, options);
var label = parts.ToFormattedString(options); // same formatting rules as Shorten
var same = Millify.FormatScaled(parts, options);
// parts.ScaledValue — signed magnitude after scaling
// parts.UnitIndex — index into options.Units for the active suffixSpan<char> buffer = stackalloc char[32];
if (Millify.TryFormat(1234.5m, buffer, out var written))
{
var text = buffer[..written].ToString();
}Overloads exist for decimal, long, double, and BigInteger. On netstandard1.6 and netstandard2.0, the library references System.Memory so Span<char> is available.
Enable SmartPrecision to reduce fractional digits when the scaled value is large (for denser dashboards), while keeping finer detail when the scaled magnitude is small. Behavior is still capped by Precision.
var dense = new MillifyOptions(precision: 2, smartPrecision: true);
Millify.Shorten(9_990, dense); // 9.99K — full precision while scaled value < 10
Millify.Shorten(125_000_000, dense); // 125M — no fractional digits once scaled value ≥ 100| Member | Role |
|---|---|
Millify.Shorten(...) |
Format a number to a single string with suffix. |
Millify.Decompose(...) |
Return MillifiedNumber (scaled value + unit index) without final string formatting. |
Millify.FormatScaled(MillifiedNumber, ...) |
Format a decomposed value with the same rules as Shorten. |
MillifiedNumber.ToFormattedString(...) |
Instance helper equivalent to FormatScaled. |
Millify.TryFormat(..., Span<char>, ...) |
Write the same string as Shorten into a buffer; returns false if the buffer is too small. |
MillifyOptions |
Precision, casing, spacing, custom Units, ScaleBase, trimming, smart precision, Culture. |
MillifyScaleBase |
Decimal (1000) or Binary (1024) steps between tiers. |
| Option | Type | Default | Description |
|---|---|---|---|
Precision |
int |
1 |
Maximum fractional digits (F precision). Must be >= 1. |
Lowercase |
bool |
false |
When true, suffixes are lowercased. When false, decimal single-letter units are uppercased; binary ki-style units become Ki, Mi, etc. |
SpaceBeforeUnit |
bool |
false |
Inserts a space between the number and the suffix. |
Units |
string[] |
See below | Suffix per magnitude, index 0 for the unscaled tier. Must be non-empty and must not contain null entries. |
ScaleBase |
MillifyScaleBase |
Decimal (1000) |
Binary (1024) for IEC-style scaling. |
TrimInsignificantZeros |
bool |
true |
Removes trailing fractional zeros (for example 2.50K → 2.5K). |
SmartPrecision |
bool |
false |
When true, fractional digits are reduced for larger scaled magnitudes: scaled absolute value >= 100 → 0 decimals; [10, 100) → up to 1 (still capped by Precision); below 10 (including values < 1) → full Precision. |
Culture |
CultureInfo? |
null |
Supplies the decimal separator only (invariant when null). Digit grouping is disabled so only the separator changes (for example 1,2K in fr-FR). |
Constructor parameter order (all have defaults except where noted):
precision, lowercase, spaceBeforeUnit, units, scaleBase, trimInsignificantZeros, smartPrecision, culture.
When you do not pass units:
ScaleBase.Decimal:{ "", "k", "m", "g", "t", "p", "e", "z", "y" }(shown asK,M,G, … whenLowercaseisfalse).ScaleBase.Binary:{ "", "ki", "mi", "gi", "ti", "pi", "ei", "zi", "yi" }(shown asKi,Mi,Gi, … whenLowercaseisfalse).
Shorten and Decompose compute the scaled value as (decimal)abs(n) / (decimal)pow(scaleBase, unitIndex) when that fits in decimal. If the cast overflows, a double ratio is used; if the value is still not representable, an OverflowException is thrown with guidance to add more Units tiers so the value can be scaled further.
Units is validated when options are constructed and whenever the Units property is assigned: the array must contain at least one entry, and no element may be null.
The library targets:
netstandard1.6netstandard2.0netstandard2.1
netstandard1.6 and netstandard2.0 builds reference System.Memory for Span<char> support in TryFormat.
Tests in this repository target a current .NET SDK (see CI); your app only needs a TFM compatible with the standards above.
Issues and pull requests are welcome. The millify package ships this README from the repository root.
Build and test (from the repository root):
cd src
dotnet restore
dotnet build --configuration Release
dotnet test --configuration ReleaseCI runs the same steps under Ubuntu with the .NET 10 SDK. The solution file is src/millify.slnx.
Layout
src/millify— library source (MillifyDotnetnamespace)src/millify.Tests— xUnit tests
MIT. See LICENSE.