Format durations and ages
Orientation
Call TimeSpan.Humanize for elapsed time and ToAge for a human-readable age approximation. Precision controls how many units appear; minimum and maximum units define the allowed range. Keep calendar-sensitive calculations in date types and use this API only after the application has a duration.
Example
This program shows two-unit precision, locale-aware list punctuation, localized compact symbols, invariant duration parsing, and an age rendered with number words. It also shows fractional seconds without exposing a separate millisecond unit:
using System.Globalization;
using Humanizer;
var culture = CultureInfo.GetCultureInfo("en-US");
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
var elapsed = TimeSpan.FromMinutes(125)
.Humanize(precision: 2, culture: culture);
var compact = TimeSpan.FromSeconds(62)
.Humanize(precision: 2, culture: culture, collectionSeparator: null);
var symbols = TimeSpan.FromMilliseconds(3_603_001)
.HumanizeToSymbols(precision: 3, culture: culture);
var fractionalSeconds = TimeSpan.FromMilliseconds(1500)
.HumanizeWithFractionalSeconds(
precision: 1,
maxFractionalDigits: 3,
roundingMode: MidpointRounding.ToEven,
culture: culture,
maxUnit: TimeUnit.Second);
var parsedCompact = "1h 30m 15.25s".DehumanizeTimeSpan();
var parsedStandard = "1.02:03:04.0050060".DehumanizeTimeSpan();
var parsedInvalid = "1,5h".TryDehumanizeTimeSpan(out var invalid);
var age = TimeSpan.FromDays(750)
.ToAge(culture, toWords: true);
AssertEqual("2 hours, 5 minutes", elapsed);
AssertEqual("1 minute and 2 seconds", compact);
AssertEqual("1h, 3s, 1ms", symbols);
AssertEqual("1.5 seconds", fractionalSeconds);
AssertEqual(new TimeSpan(0, 1, 30, 15, 250), parsedCompact);
AssertEqual(TimeSpan.FromTicks(937_840_050_060), parsedStandard);
AssertEqual(false, parsedInvalid);
AssertEqual(TimeSpan.Zero, invalid);
AssertEqual("two years old", age);
Console.WriteLine($"{elapsed}; {fractionalSeconds}; {symbols}; {age}");
static void AssertEqual<T>(T expected, T actual)
{
if (!EqualityComparer<T>.Default.Equals(expected, actual))
throw new InvalidOperationException($"Expected '{expected}', got '{actual}'.");
}
Control the decomposition
precisionlimits the number of returned non-empty units.countEmptyUnits: truecounts gaps after the first non-empty unit toward that limit.maxUnitandminUnitconstrain the decomposition.toWords: truereplaces digits with number words where the locale supports them.collectionSeparator: nulluses the localized collection formatter; any supplied string is used as a literal separator.
The default maximum unit is Week. Ask for Month or Year explicitly when those approximate units are acceptable.
Use compact symbols
In Humanizer 4, HumanizeToSymbols applies the same precision, empty-unit,
range, culture, and separator controls but emits localized TimeUnit symbols
such as 1h, 3s, 1ms.
Keep sub-second precision in seconds
In Humanizer 4, use HumanizeWithFractionalSeconds or
HumanizeToSymbolsWithFractionalSeconds when milliseconds should appear as a
fraction of the seconds unit. These APIs always use Second as the inclusive
minimum unit. Supply the precision, maximum unit, maximum fractional digits,
and either MidpointRounding.ToEven or
MidpointRounding.AwayFromZero explicitly.
The entire duration is rounded before it is decomposed, so a value such as
59.9996 seconds rounded to three fractional digits becomes 1 minute.
The digit count is a maximum from zero through seven; trailing zeros are
trimmed. Numeric text and CLDR seconds grammar follow the requested culture
for every supported locale, including same-language parent inheritance.
Fractional requests never fall back to English. With the default strategy,
genuinely fractional terminal values require IFractionalTimeSpanFormatter
from the configured formatter. A custom
IFractionalTimeSpanHumanizeStrategy may own its fractional formatting.
Apply grammatical case
In Humanizer 4, HumanizeWithCase selects a locale-authored grammatical
case form for each duration unit. A singular form may include a localized word
or article, and a locale may encode a count in the inflected unit form:
var german = CultureInfo.GetCultureInfo("de-DE");
var duration = TimeSpan.FromDays(7).HumanizeWithCase(
GrammaticalCase.Dative,
culture: german);
Console.WriteLine($"in {duration}"); // in einer Woche
The method returns a bare duration phrase and never adds a preposition.
Nominative also uses the case-aware locale data, so an authored singular
one-word or article can differ from ordinary Humanize. The API deliberately
has no toWords option: singular forms are complete locale-authored phrases,
while counted forms follow the locale's verified duration grammar. Existing
case-free Humanize, including Humanize(toWords: true), continues to use the
legacy duration surface unchanged.
Case coverage is explicit for every locale:
distinctmeans the locale declares an evidenced duration-case inventory and explicitly realizes every case and unit in that inventory.- Each case or unit realization is either authored, intentionally
sameRenderedAsanother verified case, or explicitlynotApplicable. - Root duration-case classifications are
distinct,not-applicable, or inherited from a same-language, same-script parent. - release validation rejects missing or unsupported applicable roots, cases, and units.
An unavailable requested case or unit throws NotSupportedException; it
never falls back to English or invents morphology. Availability can differ by
unit. For example, Malayalam has verified case forms through seconds but only
a nominative millisecond form. A custom strategy or formatter must implement
the corresponding grammatical-case capability interface, or every
HumanizeWithCase call fails clearly, including Nominative.
The checked-in duration forms use explicit long-duration case data from CLDR 48 at the audited commit where available, plus the per-locale grammar, corpus, dictionary, and native-review sources recorded in each locale file. See Unicode LDML for the underlying locale-data model.
Parse invariant durations
In Humanizer 4, DehumanizeTimeSpan and TryDehumanizeTimeSpan accept
standard invariant TimeSpan text or compact tokens using ms, s, m, h,
d, and w. Compact tokens may have one leading sign, optional whitespace,
fractional values that resolve to whole ticks, repeated units, and units in any
order. A week is seven days.
Use TryDehumanizeTimeSpan at an input boundary. It returns false and
TimeSpan.Zero for unsupported syntax or an out-of-range value.
DehumanizeTimeSpan throws FormatException for the same failures and
ArgumentNullException for null.
Use ToAge deliberately
ToAge adds the culture’s age phrase to a duration decomposition. It is convenient for an already computed elapsed span, but it is not a birthday calculator.
Pitfall
Months and years are approximated from a duration and do not know calendar month length or leap years. Negative TimeSpan values are rendered with positive unit counts, so preserve direction elsewhere when it matters. A precision of zero or less returns an empty string.
Compact duration input is intentionally culture-invariant and is not a parser
for localized humanized output. For example, 1.5h is valid even under a
comma-decimal culture, while 1,5h, one hour, and 1 hour are rejected.
Version notes
TimeSpan.Humanize is available across the documented corpus. ToAge begins
in 3.0.1; HumanizeWithCase, HumanizeToSymbols, DehumanizeTimeSpan, and
TryDehumanizeTimeSpan are Humanizer 4 APIs. The verified executable runs
against the selected package; older snapshots replace or exclude calls they
do not support.