Skip to main content
Version: 4.0 (next)

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:

Program.cs
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

  • precision limits the number of returned non-empty units.
  • countEmptyUnits: true counts gaps after the first non-empty unit toward that limit.
  • maxUnit and minUnit constrain the decomposition.
  • toWords: true replaces digits with number words where the locale supports them.
  • collectionSeparator: null uses 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:

  • distinct means 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 sameRenderedAs another verified case, or explicitly notApplicable.
  • 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.