Skip to main content

Durations

These extensions make TimeSpan code shorter and easier to read. Write 20.Minutes() instead of TimeSpan.FromMinutes(20), 2.Hours().Before(now) instead of now - TimeSpan.FromHours(2), and turn a duration into text such as "1 day, 2 hours" for logs and UI. Reach for them wherever a timeout, delay, expiry or elapsed time appears in your code.

The results below show what TimeSpan.ToString() prints, for example 01:30:00 or 14.00:00:00.

At a glance​

MethodWhat it does
Milliseconds()Creates a duration of that many milliseconds, from an int or a double.
Seconds()Creates a duration of that many seconds, from an int or a double.
Minutes()Creates a duration of that many minutes, from an int or a double.
Hours()Creates a duration of that many hours, from an int or a double.
Days()Creates a duration of that many days, from an int or a double.
Weeks()Creates a duration of that many weeks (7 days each), from an int or a double.
Before(moment)Gets the DateTime or DateTimeOffset that is this long before a moment.
After(moment)Gets the DateTime or DateTimeOffset that is this long after a moment.
ToHumanString(maxUnits)Describes the duration in words: "1 day, 2 hours, 30 minutes".
RoundTo(interval)Rounds to the nearest multiple of an interval; a midpoint rounds away from zero.
FloorTo(interval)Rounds down (toward negative infinity) to a multiple of an interval.
CeilingTo(interval)Rounds up (toward positive infinity) to a multiple of an interval.

All of these live in the System namespace, so you do not need an extra using.

Creating durations​

Milliseconds(), Seconds(), Minutes(), Hours(), Days(), Weeks()​

Each method turns a number into a TimeSpan of that many units. Every unit has two overloads: one on int for whole amounts and one on double for fractions. Weeks() counts seven days per week.

Timeouts and lifetimes from whole numbers
TimeSpan retryDelay = 250.Milliseconds(); // → 00:00:00.2500000
TimeSpan requestTimeout = 45.Seconds(); // → 00:00:45
TimeSpan sessionTimeout = 20.Minutes(); // → 00:20:00
TimeSpan cacheLifetime = 6.Hours(); // → 06:00:00
TimeSpan invoiceDueIn = 30.Days(); // → 30.00:00:00
TimeSpan trialPeriod = 2.Weeks(); // → 14.00:00:00
Fractions from double literals
TimeSpan debounce = 1.5.Seconds(); // → 00:00:01.5000000
TimeSpan grace = 0.5.Minutes(); // → 00:00:30
TimeSpan meetingLength = 1.5.Hours(); // → 01:30:00
TimeSpan shift = 0.5.Days(); // → 12:00:00
TimeSpan halfWeek = 0.5.Weeks(); // → 3.12:00:00
TimeSpan precise = 1.5.Milliseconds(); // → 00:00:00.0015000

The results are ordinary TimeSpan values, so they work with the usual operators:

Combining durations
TimeSpan slaWindow = 1.Days() + 4.Hours(); // → 1.04:00:00
TimeSpan doubleShift = 8.Hours() * 2; // → 16:00:00
bool isLongRunning = 95.Minutes() > 1.5.Hours(); // → true

The int methods call TimeSpan.FromMilliseconds, FromSeconds, FromMinutes, FromHours and FromDays with the value, and the double methods do the same with the double. Weeks() passes seven times the value to FromDays. They accept and reject the same values as those factories: a result outside the TimeSpan range throws.

Receivers are not widened

C# does not apply numeric conversions to the receiver of an extension method. These methods exist on int and double only, so a long, float or decimal variable does not compile with them. Convert first: ((int)retryCount).Seconds() or ((double)delayMs).Milliseconds().

Negative literals

In C#, -5.Minutes() means -(5.Minutes()). That gives the right TimeSpan (-00:05:00), but the minus also applies to anything you chain after it: -5.Minutes().After(now) tries to negate a DateTime and does not compile. Put the number in parentheses: (-5).Minutes().

tip

On a C# 14 compiler you can also write TimeSpan.FromWeeks(2), the factory that TimeSpan itself lacks. See TimeSpan.FromWeeks.

Placing a duration in time​

Before and After​

Before(moment) subtracts the duration from a moment and After(moment) adds it. Each has an overload for DateTime and one for DateTimeOffset. They read like a sentence, which helps in expiry, reminder and scheduling code. The library never reads the clock itself, so you pass "now" in.

Order reminders and token expiry (DateTime)
DateTime now = new DateTime(2024, 5, 15, 12, 0, 0, DateTimeKind.Utc);

DateTime reminderAt = 2.Hours().Before(now); // → 2024-05-15 10:00:00 (Utc)
DateTime tokenExpiresAt = 30.Minutes().After(now); // → 2024-05-15 12:30:00 (Utc)
Delivery dates keep their offset (DateTimeOffset)
DateTimeOffset orderPlacedAt = new DateTimeOffset(2024, 5, 15, 12, 0, 0, TimeSpan.FromHours(3));

DateTimeOffset deliveryDue = 1.Weeks().After(orderPlacedAt); // → 2024-05-22 12:00:00 +03:00
DateTimeOffset cutoff = 1.Days().Before(orderPlacedAt); // → 2024-05-14 12:00:00 +03:00
Filtering recent records
DateTime since = 7.Days().Before(now);
List<Invoice> recentInvoices = invoices.Where(invoice => invoice.IssuedAt >= since).ToList();
  • Before(moment) is exactly moment - span and After(moment) is exactly moment + span. A DateTime keeps its Kind, and a DateTimeOffset keeps its Offset.
  • A negative span reverses the direction: (-1).Hours().After(now) is one hour before now.
  • A result outside the DateTime or DateTimeOffset range throws ArgumentOutOfRangeException, as the built-in operators do.

Describing a duration​

ToHumanString​

ToHumanString() writes a duration in English words, largest unit first: days, hours, minutes, seconds and milliseconds. Units with a zero count are left out. Use it in logs, status pages and e-mails, where 1.02:30:00 is hard to read.

Describing durations
string slaText = new TimeSpan(1, 2, 30, 0).ToHumanString(); // → "1 day, 2 hours, 30 minutes"
string checkout = 90.Minutes().ToHumanString(); // → "1 hour, 30 minutes"
string upload = 1.5.Seconds().ToHumanString(); // → "1 second, 500 milliseconds"
string uptime = new TimeSpan(2, 1, 1, 1).ToHumanString(); // → "2 days, 1 hour, 1 minute, 1 second"
string retention = 400.Days().ToHumanString(); // → "400 days"
string nothing = TimeSpan.Zero.ToHumanString(); // → "0 seconds"
string clockSkew = (-5).Minutes().ToHumanString(); // → "minus 5 minutes"

The optional maxUnits parameter keeps only that many units, counting from the largest one present. Smaller units are dropped, not rounded, so 1.23:59:59 with maxUnits 1 is "1 day", not "2 days". Units with a zero count do not use up a slot.

Short descriptions for a dashboard
TimeSpan buildTime = new TimeSpan(1, 2, 30, 0);
string twoUnits = buildTime.ToHumanString(2); // → "1 day, 2 hours"

string oneUnit = new TimeSpan(1, 23, 59, 59).ToHumanString(1); // → "1 day"
string skipsZeroHours = new TimeSpan(1, 0, 5, 0).ToHumanString(2); // → "1 day, 5 minutes"
string fewerThanMax = 30.Seconds().ToHumanString(3); // → "30 seconds"

ToHumanString() and ToTimeSpan() are a pair: what one writes, the other reads back to the same value.

Round trip through text
TimeSpan original = new TimeSpan(0, 3, 0, 7, 12);
string text = original.ToHumanString(); // → "3 hours, 7 seconds, 12 milliseconds"
TimeSpan parsed = text.ToTimeSpan(); // → 03:00:07.0120000
bool same = parsed == original; // → true
  • maxUnits defaults to int.MaxValue, which shows every unit. A value below 1 throws ArgumentOutOfRangeException with the parameter name maxUnits.
  • Weeks are not used: 14 days is "14 days".
  • Anything below one millisecond is dropped. A duration shorter than 1 ms, positive or negative, is "0 seconds".
  • A negative duration starts with "minus ". TimeSpan.MinValue works too and starts with "minus 10675199 days".
  • The words are always English, and numbers use invariant digits with no group separators, whatever the current culture: 1234.Days().ToHumanString() is "1234 days", also under de-DE.
Round trip limits

A round trip through ToTimeSpan() is exact when the duration has no part below one millisecond. Use maxUnits only for display, because the dropped units are lost.

Rounding a duration​

These three methods snap a duration to a multiple of an interval, such as the nearest second for a log line or the next 15 minutes for billing. They work on ticks, so any positive interval works, not only whole units.

ValueRoundTo(1.Minutes())FloorTo(1.Minutes())CeilingTo(1.Minutes())
100 seconds00:02:0000:01:0000:02:00
90 seconds00:02:0000:01:0000:02:00
80 seconds00:01:0000:01:0000:02:00
-80 seconds-00:01:00-00:02:00-00:01:00
-90 seconds-00:02:00-00:02:00-00:01:00
-100 seconds-00:02:00-00:02:00-00:01:00
120 seconds00:02:0000:02:0000:02:00

Rules shared by all three:

  • The interval must be positive. Zero or a negative interval throws ArgumentOutOfRangeException with the parameter name interval.
  • A value that is already a multiple of the interval, including TimeSpan.Zero, comes back unchanged.
  • A result outside the TimeSpan range throws OverflowException instead of wrapping around. For example, TimeSpan.MaxValue.CeilingTo(1.Hours()) throws, while TimeSpan.MaxValue.FloorTo(1.Hours()) works.

RoundTo​

RoundTo(interval) returns the nearest multiple of interval. A value exactly halfway rounds away from zero, so positive and negative durations round the same way: +90s becomes +2min and -90s becomes -2min. Use it to show elapsed times without noise.

Clean elapsed times for logs
TimeSpan elapsed = TimeSpan.FromMilliseconds(83_412);
TimeSpan shown = elapsed.RoundTo(1.Seconds()); // → 00:01:23

TimeSpan halfway = 1500.Milliseconds().RoundTo(1.Seconds()); // → 00:00:02
TimeSpan negative = (-90).Seconds().RoundTo(1.Minutes()); // → -00:02:00
Nearest quarter hour for a timesheet
TimeSpan shortCall = 52.Minutes().RoundTo(15.Minutes()); // → 00:45:00
TimeSpan longCall = 53.Minutes().RoundTo(15.Minutes()); // → 01:00:00
TimeSpan tooShort = 10.Minutes().RoundTo(1.Hours()); // → 00:00:00
TimeSpan almostHour = 50.Minutes().RoundTo(1.Hours()); // → 01:00:00
tip

Combine rounding with ToHumanString() for readable output: elapsed.RoundTo(1.Seconds()).ToHumanString() is "1 minute, 23 seconds".

FloorTo​

FloorTo(interval) returns the largest multiple of interval that is not greater than the value. For positive durations this cuts off the remainder. For negative durations it moves further from zero (-100s becomes -2min). Use it to count only completed blocks of time.

Completed hours of a support session
TimeSpan sessionLength = new TimeSpan(2, 45, 0);
TimeSpan billedHours = sessionLength.FloorTo(1.Hours()); // → 02:00:00

TimeSpan almostTwoMinutes = 119.Seconds().FloorTo(1.Minutes()); // → 00:01:00
TimeSpan justBelowZero = (-1).Seconds().FloorTo(1.Minutes()); // → -00:01:00

CeilingTo​

CeilingTo(interval) returns the smallest multiple of interval that is not less than the value. For positive durations any remainder rounds up. For negative durations it moves toward zero (-100s becomes -1min). Use it when every started block counts, such as parking or billing in 15-minute steps.

Billing every started 15 minutes
TimeSpan parked = 47.Minutes();
TimeSpan billed = parked.CeilingTo(15.Minutes()); // → 01:00:00

TimeSpan oneSecondOver = 61.Seconds().CeilingTo(1.Minutes()); // → 00:02:00
TimeSpan justBelowZero = (-1).Seconds().CeilingTo(1.Minutes()); // → 00:00:00
Rounding dates

To round a DateTime, for example to the nearest 15 minutes on the clock, use the DateTime versions of RoundTo, FloorTo and CeilingTo described in Dates.

See also​

  • Parsing: ToTimeSpan() reads "1d 2h 30m", "90s" and the text that ToHumanString() writes.
  • Dates: DateTime helpers, including rounding a moment to an interval.
  • Await anything: await 500.Milliseconds(); and await foreach over a TimeSpan.
  • foreach and deconstruction: unpack a TimeSpan into (hours, minutes, seconds).