Dates and Times
Dates and times are deceptively tricky: time zones, daylight saving, months of different lengths, and formats that vary by country. .NET gives you a type for each job — DateOnly, TimeOnly, DateTime, DateTimeOffset and TimeSpan — plus tools for formatting, parsing and converting between time zones.
Choosing a type
| Type | Represents | Example |
|---|---|---|
DateOnly | A calendar date with no time | A birthday, a due date |
TimeOnly | A time of day with no date | Opening hours, an alarm |
DateTime | A date and time, optionally marked as UTC or local | Timestamps inside one system |
DateTimeOffset | A date and time plus its offset from UTC | An exact moment, unambiguous everywhere |
TimeSpan | A length of time | 2 hours 30 minutes |
A good rule: store and exchange exact moments as DateTimeOffset (or DateTime in UTC), and use DateOnly/TimeOnly when there's genuinely no time or date part.
Creating dates and times
var launch = new DateTime(2026, 7, 16, 13, 32, 0); var birthday = new DateOnly(1990, 2, 28); var opening = new TimeOnly(9, 30); var meeting = new DateTimeOffset(2026, 3, 10, 15, 0, 0, TimeSpan.FromHours(-5)); Console.WriteLine(launch); Console.WriteLine(birthday); Console.WriteLine(opening); Console.WriteLine(meeting); Console.WriteLine(meeting.UtcDateTime); Console.WriteLine($"{launch.Year} {launch.Month} {launch.Day} {launch.DayOfWeek} day {launch.DayOfYear}");
7/16/2026 1:32:00 PM 2/28/1990 9:30 AM 3/10/2026 3:00:00 PM -05:00 3/10/2026 8:00:00 PM 2026 7 16 Thursday day 197
DateTime.Now, DateTime.UtcNow, DateTimeOffset.Now and DateOnly.FromDateTime(DateTime.Today) give the current moment or date. (This lesson uses fixed dates so the output stays the same.)
Date arithmetic
Add or subtract with methods like AddDays and AddMonths. They return new values — dates, like strings, are immutable. Subtracting two DateTimes gives a TimeSpan:
var start = new DateTime(2026, 1, 31, 9, 0, 0); Console.WriteLine(start.AddDays(10)); Console.WriteLine(start.AddMonths(1)); // Feb has no 31st: clamps to the 28th Console.WriteLine(start.AddHours(-12)); var end = new DateTime(2026, 3, 1, 17, 30, 0); TimeSpan gap = end - start; Console.WriteLine(gap); Console.WriteLine($"{gap.Days} days, {gap.Hours} hours, {gap.Minutes} minutes"); Console.WriteLine($"{gap.TotalHours} hours in total"); var due = new DateOnly(2026, 12, 24); var today = new DateOnly(2026, 10, 8); Console.WriteLine($"{due.DayNumber - today.DayNumber} days to go");
2/10/2026 9:00:00 AM 2/28/2026 9:00:00 AM 1/30/2026 9:00:00 PM 29.08:30:00 29 days, 8 hours, 30 minutes 704.5 hours in total 77 days to go
TimeSpan
TimeSpan film = TimeSpan.FromMinutes(137); TimeSpan ads = new(0, 18, 30); TimeSpan total = film + ads; Console.WriteLine(film); Console.WriteLine(total); Console.WriteLine(total.TotalMinutes); Console.WriteLine($"{(int)total.TotalHours}h {total.Minutes:D2}m"); Console.WriteLine(total > TimeSpan.FromHours(2.5)); Console.WriteLine(TimeSpan.Parse("01:45:00").TotalSeconds);
02:17:00 02:35:30 155.5 2h 35m True 6300
Formatting
Standard format letters give culture-aware results; custom patterns give exact layouts. Use "o" (round-trip, ISO 8601) for anything stored or sent to another system:
var d = new DateTime(2026, 11, 5, 18, 7, 9); Console.WriteLine(d.ToString("d")); Console.WriteLine(d.ToString("D")); Console.WriteLine(d.ToString("t")); Console.WriteLine(d.ToString("yyyy-MM-dd HH:mm:ss")); Console.WriteLine(d.ToString("ddd d MMM yyyy, h:mm tt")); Console.WriteLine(d.ToString("o")); Console.WriteLine(d.ToString("D", new System.Globalization.CultureInfo("fr-FR")));
11/5/2026 Thursday, November 5, 2026 6:07 PM 2026-11-05 18:07:09 Thu 5 Nov 2026, 6:07 PM 2026-11-05T18:07:09.0000000 jeudi 5 novembre 2026
| Pattern | Meaning |
|---|---|
yyyy MM dd | 4-digit year, 2-digit month, 2-digit day |
MMM / MMMM | Abbreviated / full month name |
ddd / dddd | Abbreviated / full day name |
HH / hh tt | 24-hour / 12-hour clock with AM/PM |
mm ss fff | Minutes, seconds, milliseconds |
Watch the case: MM is months but mm is minutes.
Parsing
Parse accepts many formats but depends on the current culture — 03/04/2026 is March 4th in the US and April 3rd in the UK. When you know the exact format, use ParseExact or TryParseExact with the invariant culture:
using System.Globalization; DateTime a = DateTime.Parse("2026-10-08T14:30:00"); Console.WriteLine(a); DateOnly b = DateOnly.ParseExact("08/10/2026", "dd/MM/yyyy", CultureInfo.InvariantCulture); Console.WriteLine(b.ToString("MMMM d")); foreach (string text in new[] { "2026-02-29", "2028-02-29" }) { bool ok = DateOnly.TryParseExact(text, "yyyy-MM-dd", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateOnly date); Console.WriteLine(ok ? $"{text} is a {date.DayOfWeek}" : $"{text} is not a valid date"); }
10/8/2026 2:30:00 PM October 8 2026-02-29 is not a valid date 2028-02-29 is a Tuesday
UTC and time zones
Local times are ambiguous: the same clock time happens at different moments in different places, and daylight saving shifts them twice a year. Record moments in UTC and convert to a user's time zone only for display. TimeZoneInfo handles the rules, including daylight saving:
var flightDeparts = new DateTimeOffset(2026, 7, 1, 9, 0, 0, TimeSpan.Zero); // 09:00 UTC foreach (string zoneId in new[] { "Europe/Lisbon", "America/New_York", "Asia/Tokyo", "Australia/Sydney" }) { var zone = TimeZoneInfo.FindSystemTimeZoneById(zoneId); DateTimeOffset local = TimeZoneInfo.ConvertTime(flightDeparts, zone); Console.WriteLine($"{zoneId,-17} {local:yyyy-MM-dd HH:mm} (UTC{local.Offset.TotalHours:+0;-0}) DST: {zone.IsDaylightSavingTime(local)}"); }
Europe/Lisbon 2026-07-01 10:00 (UTC+1) DST: True America/New_York 2026-07-01 05:00 (UTC-4) DST: True Asia/Tokyo 2026-07-01 18:00 (UTC+9) DST: False Australia/Sydney 2026-07-01 19:00 (UTC+10) DST: False
The IANA zone names used here ("Europe/Lisbon") work on every operating system in modern .NET, including Windows.
Measuring elapsed time
To time code, use Stopwatch, not DateTime.Now — it uses a high-resolution clock that isn't affected by system clock changes:
var watch = System.Diagnostics.Stopwatch.StartNew(); Thread.Sleep(250); Console.WriteLine($"{watch.ElapsedMilliseconds} ms"); // about 250
Testable time with TimeProvider
Code that calls DateTime.Now directly is hard to test: the answer changes every run. Accepting a TimeProvider lets production code use the real clock (TimeProvider.System) and tests supply a fixed one:
var fixedClock = new FixedTime(new DateTimeOffset(2026, 12, 24, 20, 0, 0, TimeSpan.Zero)); var greeter = new Greeter(fixedClock); Console.WriteLine(greeter.Greeting()); class Greeter(TimeProvider time) { public string Greeting() => time.GetUtcNow() switch { { Month: 12, Day: 24 or 25 } => "Merry Christmas!", { Hour: < 12 } => "Good morning", _ => "Hello", }; } class FixedTime(DateTimeOffset now) : TimeProvider { public override DateTimeOffset GetUtcNow() => now; }
Merry Christmas!
Try it yourself
Calculate someone's age in whole years on a given date, taking care with birthdays later in the year.
Born 1990-10-08: 36 years old Born 1990-10-09: 35 years old Born 2000-02-29: 26 years old Born 2010-01-01: 16 years old