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
| Method | What 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.
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
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:
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.
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().
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().
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.
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)
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
DateTime since = 7.Days().Before(now);
List<Invoice> recentInvoices = invoices.Where(invoice => invoice.IssuedAt >= since).ToList();
Before(moment)is exactlymoment - spanandAfter(moment)is exactlymoment + span. ADateTimekeeps itsKind, and aDateTimeOffsetkeeps itsOffset.- A negative span reverses the direction:
(-1).Hours().After(now)is one hour beforenow. - A result outside the
DateTimeorDateTimeOffsetrange throwsArgumentOutOfRangeException, 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.
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.
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.
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
maxUnitsdefaults toint.MaxValue, which shows every unit. A value below 1 throwsArgumentOutOfRangeExceptionwith the parameter namemaxUnits.- 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.MinValueworks 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 underde-DE.
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.
| Value | RoundTo(1.Minutes()) | FloorTo(1.Minutes()) | CeilingTo(1.Minutes()) |
|---|---|---|---|
| 100 seconds | 00:02:00 | 00:01:00 | 00:02:00 |
| 90 seconds | 00:02:00 | 00:01:00 | 00:02:00 |
| 80 seconds | 00:01:00 | 00:01:00 | 00: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 seconds | 00:02:00 | 00:02:00 | 00:02:00 |
Rules shared by all three:
- The
intervalmust be positive. Zero or a negative interval throwsArgumentOutOfRangeExceptionwith the parameter nameinterval. - A value that is already a multiple of the interval, including
TimeSpan.Zero, comes back unchanged. - A result outside the
TimeSpanrange throwsOverflowExceptioninstead of wrapping around. For example,TimeSpan.MaxValue.CeilingTo(1.Hours())throws, whileTimeSpan.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.
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
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
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.
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.
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
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 thatToHumanString()writes. - Dates:
DateTimehelpers, including rounding a moment to an interval. - Await anything:
await 500.Milliseconds();andawait foreachover aTimeSpan. - foreach and deconstruction: unpack a
TimeSpaninto(hours, minutes, seconds).