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

TypeRepresentsExample
DateOnlyA calendar date with no timeA birthday, a due date
TimeOnlyA time of day with no dateOpening hours, an alarm
DateTimeA date and time, optionally marked as UTC or localTimestamps inside one system
DateTimeOffsetA date and time plus its offset from UTCAn exact moment, unambiguous everywhere
TimeSpanA length of time2 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

C#
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}");
Output
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:

C#
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");
Output
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

C#
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);
Output
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:

C#
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")));
Output
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
PatternMeaning
yyyy MM dd4-digit year, 2-digit month, 2-digit day
MMM / MMMMAbbreviated / full month name
ddd / ddddAbbreviated / full day name
HH / hh tt24-hour / 12-hour clock with AM/PM
mm ss fffMinutes, 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:

C#
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");
}
Output
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:

C#
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)}");
}
Output
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:

C#
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:

C#
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;
}
Output
Merry Christmas!

Try it yourself

Calculate someone's age in whole years on a given date, taking care with birthdays later in the year.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
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