ZetCode

Date and time in C#

last modified October 18, 2026

In this article we show how to work with date and time in C#.

A datetime represents an instant in time. The string representation of a datetime is culture-specific and depends both on the conventions used for displaying date and time and on its calendar. A calendar is a system of organizing datetime units.

C# has several types for dates and times:

C# DateTime

The DateTime value type represents dates and times with values ranging from 00:00:00 (midnight), January 1, 0001 Anno Domini (Common Era) through 11:59:59 P.M., December 31, 9999 A.D. (C.E.) in the Gregorian calendar. The value is stored as a number of ticks, one tick being 100 nanoseconds.

The examples were run on a machine in the Central European time zone, with the English (US) culture. Your output will differ in the time zone and the format.

C# today's date

In our first example, we get today's date and time.

Program.cs
DateTime now = DateTime.Now;
Console.WriteLine(now.ToString("F"));

DateOnly today = DateOnly.FromDateTime(now);
TimeOnly time = TimeOnly.FromDateTime(now);

Console.WriteLine(today);
Console.WriteLine(time.ToString("HH:mm"));
Console.WriteLine(DateTime.Today.ToString("d"));

The example prints the current date and time in several forms.

DateTime now = DateTime.Now;

With the Now property of DateTime, we get the current date and time in local time. DateTime.Today returns only the date, with the time set to midnight.

Console.WriteLine(now.ToString("F"));

With the ToString method, we format the date. The F specifier creates a full date and time pattern.

DateOnly today = DateOnly.FromDateTime(now);
TimeOnly time = TimeOnly.FromDateTime(now);

When we need only a date or only a time, DateOnly and TimeOnly express this more precisely than a DateTime with an unused half. They are covered in a later section.

$ dotnet run
Sunday, October 18, 2026 8:09:55 PM
10/18/2026
20:09
10/18/2026

C# DateTime properties

DateTime represents an instant in time. Its properties provide various aspects of the date and time. In this example we use a fixed value so that the output is reproducible.

Program.cs
string[] months = {"January", "February", "March", "April", "May",
    "June", "July", "August", "September", "October", "November", "December"};

DateTime now = new DateTime(2026, 10, 18, 14, 9, 12, 410);

Console.WriteLine("Today's date: {0}", now.Date);
Console.WriteLine("Today is {0} day of {1}", now.Day, months[now.Month - 1]);
Console.WriteLine("Today is {0} day of {1}", now.DayOfYear, now.Year);
Console.WriteLine("Today's time: {0}", now.TimeOfDay);
Console.WriteLine("Hour: {0}", now.Hour);
Console.WriteLine("Minute: {0}", now.Minute);
Console.WriteLine("Second: {0}", now.Second);
Console.WriteLine("Millisecond: {0}", now.Millisecond);
Console.WriteLine("The day of week: {0}", now.DayOfWeek);
Console.WriteLine("Kind: {0}", now.Kind);
Console.WriteLine("Ticks: {0}", now.Ticks);

The example examines the properties of the DateTime object.

DateTime now = new DateTime(2026, 10, 18, 14, 9, 12, 410);

A DateTime object is created from the year, month, day, hour, minute, second, and millisecond. The month and day are one-based.

Console.WriteLine("Today's date: {0}", now.Date);

The Date property gets the date component, with the time set to midnight.

Console.WriteLine("Today is {0} day of {1}", now.Day, months[now.Month - 1]);

The Day property gets the day of the month. The Month property returns the month component, a value between 1 and 12. The month name is better taken from the culture with ToString("MMMM") than from our own array; the array is here only for illustration.

Console.WriteLine("Today is {0} day of {1}", now.DayOfYear, now.Year);

The DayOfYear property gets the day of the year, and Year gets the year.

Console.WriteLine("Today's time: {0}", now.TimeOfDay);

The TimeOfDay property gets the time of day as a TimeSpan.

Console.WriteLine("Kind: {0}", now.Kind);

The Kind property indicates whether the time is based on local time, Coordinated Universal Time (UTC), or neither. A value created with a constructor is Unspecified.

$ dotnet run
Today's date: 10/18/2026 12:00:00 AM
Today is 18 day of October
Today is 291 day of 2026
Today's time: 14:09:12.4100000
Hour: 14
Minute: 9
Second: 12
Millisecond: 410
The day of week: Sunday
Kind: Unspecified
Ticks: 639279293524100000

C# add and subtract DateTime

DateTime has methods for time arithmetic. Because the structure is immutable, each method returns a new value.

Program.cs
DateTime dt = new DateTime(2019, 2, 22, 14, 0, 0);

Console.WriteLine(dt.AddSeconds(55).ToString("F"));
Console.WriteLine(dt.AddMinutes(30).ToString("F"));
Console.WriteLine(dt.AddHours(72).ToString("F"));
Console.WriteLine(dt.AddDays(65).ToString("F"));
Console.WriteLine(dt.AddDays(-65).ToString("F"));
Console.WriteLine(dt.AddMonths(3).ToString("F"));
Console.WriteLine(dt.AddYears(4).ToString("F"));

// month arithmetic clamps the day to the end of the month
Console.WriteLine(new DateTime(2026, 1, 31).AddMonths(1).ToString("d"));
Console.WriteLine(new DateTime(2024, 2, 29).AddYears(1).ToString("d"));

// differences are TimeSpan values
var diff = new DateTime(2026, 12, 24) - new DateTime(2026, 10, 18);
Console.WriteLine($"{diff.TotalDays} days until Christmas Eve");

Console.WriteLine(DateTime.DaysInMonth(2026, 2));
Console.WriteLine(DateTime.DaysInMonth(2024, 2));

The example presents the methods of the DateTime structure.

Console.WriteLine(dt.AddSeconds(55).ToString("F"));

The AddSeconds method returns a new DateTime with the specified number of seconds added.

Console.WriteLine(dt.AddDays(65).ToString("F"));
Console.WriteLine(dt.AddDays(-65).ToString("F"));

The AddDays method adds days. We can provide both positive and negative values; there is no separate subtract method.

Console.WriteLine(new DateTime(2026, 1, 31).AddMonths(1).ToString("d"));
Console.WriteLine(new DateTime(2024, 2, 29).AddYears(1).ToString("d"));

Month and year arithmetic clamps the day to the last day of the target month, so January 31 plus a month is February 28, and February 29 plus a year is February 28.

var diff = new DateTime(2026, 12, 24) - new DateTime(2026, 10, 18);

Subtracting two datetimes gives a TimeSpan.

$ dotnet run
Friday, February 22, 2019 2:00:55 PM
Friday, February 22, 2019 2:30:00 PM
Monday, February 25, 2019 2:00:00 PM
Sunday, April 28, 2019 2:00:00 PM
Wednesday, December 19, 2018 2:00:00 PM
Wednesday, May 22, 2019 2:00:00 PM
Wednesday, February 22, 2023 2:00:00 PM
2/28/2026
2/28/2025
67 days until Christmas Eve
28
29

C# UTC time and DateTimeKind

Our planet is a sphere that revolves around its axis. The Earth rotates towards the east, so the Sun rises at different times in different locations. The Earth rotates once in about 24 hours, so the world was divided into time zones. In each time zone, there is a different local time, which is often further modified by daylight saving time.

There is a pragmatic need for one global time. It helps to avoid confusion about time zones and daylight saving time. UTC (Coordinated Universal Time) was chosen as the primary time standard. Unlike local time, UTC does not change with the seasons. As a rule, programs should store and exchange times in UTC (or with an offset) and convert them to local time only when displaying.

Program.cs
DateTime local = DateTime.Now;
DateTime utc = DateTime.UtcNow;
DateTime unspecified = new DateTime(2026, 10, 18, 12, 0, 0);

Console.WriteLine($"{local.Kind}  {utc.Kind}  {unspecified.Kind}");

Console.WriteLine($"UTC time   {utc:HH:mm:ss}");
Console.WriteLine($"Local time {local:HH:mm:ss}");

// conversions depend on the Kind
DateTime asUtc = DateTime.SpecifyKind(unspecified, DateTimeKind.Utc);
Console.WriteLine(asUtc.ToLocalTime().ToString("HH:mm"));

DateTime asLocal = DateTime.SpecifyKind(unspecified, DateTimeKind.Local);
Console.WriteLine(asLocal.ToUniversalTime().ToString("HH:mm"));

// an Unspecified value is treated as local by ToUniversalTime and as UTC by ToLocalTime
Console.WriteLine(unspecified.ToUniversalTime().ToString("HH:mm"));
Console.WriteLine(unspecified.ToLocalTime().ToString("HH:mm"));

The example prints the current UTC time and the local time, and shows how the Kind of a DateTime affects conversions.

DateTime utc = DateTime.UtcNow;

With the UtcNow property, we get the UTC time. Its Kind is Utc. For DateTime.Now it is Local.

DateTime asUtc = DateTime.SpecifyKind(unspecified, DateTimeKind.Utc);
Console.WriteLine(asUtc.ToLocalTime().ToString("HH:mm"));

SpecifyKind labels a value without changing its numbers. The label decides how the conversions work: a UTC value converted to local time moves two hours forward (in the Central European summer time).

Console.WriteLine(unspecified.ToUniversalTime().ToString("HH:mm"));
Console.WriteLine(unspecified.ToLocalTime().ToString("HH:mm"));

An Unspecified value is a trap: ToUniversalTime assumes it is local, and ToLocalTime assumes it is UTC. For this reason, prefer DateTimeOffset for values that represent instants.

$ dotnet run
Local  Utc  Unspecified
UTC time   18:09:55
Local time 20:09:55
14:00
10:00
10:00
14:00

C# DateTimeOffset

A DateTimeOffset stores a date, a time, and the offset from UTC. It identifies one exact instant and cannot be misinterpreted. It does not store the time zone; the offset is only the difference at that instant.

Program.cs
var utcNow = DateTimeOffset.UtcNow;
var local = DateTimeOffset.Now;

Console.WriteLine(local.Offset);
Console.WriteLine(utcNow.Offset);

var meeting = new DateTimeOffset(2026, 10, 18, 15, 0, 0, TimeSpan.FromHours(2));
Console.WriteLine(meeting);
Console.WriteLine(meeting.UtcDateTime.ToString("HH:mm"));
Console.WriteLine(meeting.ToOffset(TimeSpan.FromHours(-5)));

// two values with different offsets are equal when they are the same instant
var other = new DateTimeOffset(2026, 10, 18, 13, 0, 0, TimeSpan.Zero);
Console.WriteLine(meeting == other);

Console.WriteLine(meeting.ToString("o"));

The example creates offset-aware values and converts them.

var meeting = new DateTimeOffset(2026, 10, 18, 15, 0, 0, TimeSpan.FromHours(2));
Console.WriteLine(meeting.UtcDateTime.ToString("HH:mm"));
Console.WriteLine(meeting.ToOffset(TimeSpan.FromHours(-5)));

We create a meeting at 15:00 in UTC+2. The same instant is 13:00 in UTC and 08:00 at UTC-5; ToOffset shows it with another offset.

Console.WriteLine(meeting == other);

Comparisons use the instant, so 15:00+02:00 equals 13:00+00:00.

$ dotnet run
02:00:00
00:00:00
10/18/2026 3:00:00 PM +02:00
13:00
10/18/2026 8:00:00 AM -05:00
True
2026-10-18T15:00:00.0000000+02:00

C# localized date

The ToString method accepts a CultureInfo, which allows us to display the date and time in a specific culture.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

DateTime now = new DateTime(2026, 10, 18, 14, 12, 2);

foreach (var name in new[] { "en-US", "sk-SK", "de-DE", "fr-FR", "ja-JP" })
{
    var ci = new CultureInfo(name);
    Console.WriteLine($"{name}: {now.ToString("F", ci)}");
}

// short date patterns differ in the order of the parts
Console.WriteLine(now.ToString("d", new CultureInfo("en-US")));
Console.WriteLine(now.ToString("d", new CultureInfo("en-GB")));
Console.WriteLine(now.ToString("d", CultureInfo.InvariantCulture));

The example prints a date in several cultures.

Console.OutputEncoding = System.Text.Encoding.UTF8;

To output accented and non-Latin characters correctly, we set the console output encoding to UTF-8. On Linux and macOS this is already the default.

Console.WriteLine(now.ToString("d", CultureInfo.InvariantCulture));

The invariant culture is neutral, based on English, and does not change with the machine's settings. Use it for text that is parsed by programs. The culture-specific output is for people.

$ dotnet run
en-US: Sunday, October 18, 2026 2:12:02 PM
sk-SK: nedeľa 18. októbra 2026 14:12:02
de-DE: Sonntag, 18. Oktober 2026 14:12:02
fr-FR: dimanche 18 octobre 2026 14:12:02
ja-JP: 2026年10月18日日曜日 14:12:02
10/18/2026
18/10/2026
10/18/2026

C# Unix time

The Unix time is the number of seconds since the Unix epoch, 00:00:00 UTC on January 1, 1970. It is widely used in computing, in APIs, tokens, and file formats. DateTimeOffset converts to and from Unix time.

Program.cs
long unixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
Console.WriteLine(unixTime > 1_700_000_000);

var epoch = DateTimeOffset.FromUnixTimeSeconds(0);
Console.WriteLine(epoch);

var dt = DateTimeOffset.FromUnixTimeSeconds(1_700_000_000);
Console.WriteLine(dt);
Console.WriteLine(dt.ToUnixTimeMilliseconds());

var back = new DateTimeOffset(2023, 11, 14, 22, 13, 20, TimeSpan.Zero);
Console.WriteLine(back.ToUnixTimeSeconds());

The example converts between Unix time and datetimes.

long unixTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();

We get the current Unix time with ToUnixTimeSeconds. There is also ToUnixTimeMilliseconds, which many web APIs and JavaScript use. The value is always based on UTC, so the machine's time zone does not matter.

$ dotnet run
True
1/1/1970 12:00:00 AM +00:00
11/14/2023 10:13:20 PM +00:00
1700000000000
1700000000

C# TimeSpan with dates

A TimeSpan structure represents a time interval. Subtracting two datetimes gives one. See the TimeSpan tutorial for more.

Program.cs
string startTime = "7:00 AM";
string endTime = "8:30 PM";

var inv = System.Globalization.CultureInfo.InvariantCulture;
TimeSpan elapsed = DateTime.Parse(endTime, inv).Subtract(DateTime.Parse(startTime, inv));

Console.WriteLine($"Time elapsed: {elapsed}");

DateTime today = new DateTime(2026, 10, 18);
DateTime borodinoBattle = new DateTime(1812, 9, 7);

TimeSpan diff = today - borodinoBattle;
Console.WriteLine($"{diff.TotalDays} days have passed since the Battle of Borodino.");

// age in years
DateOnly birth = new DateOnly(1990, 12, 24);
DateOnly day = new DateOnly(2026, 10, 18);
int age = day.Year - birth.Year;
if (birth > day.AddYears(-age)) age--;
Console.WriteLine(age);

In the example, we subtract two time values, count the days since a historical event, and compute an age.

TimeSpan elapsed = DateTime.Parse(endTime, inv).Subtract(DateTime.Parse(startTime, inv));

The Subtract method subtracts two datetimes. A time without a date is parsed as a time on the current day, which cancels out in the subtraction.

Console.WriteLine($"{diff.TotalDays} days have passed since the Battle of Borodino.");

The TotalDays property has the number of days of the elapsed time. The output is for October 18, 2026.

int age = day.Year - birth.Year;
if (birth > day.AddYears(-age)) age--;

There is no method for a person's age. We subtract the years, and if the birthday has not yet occurred this year, we subtract one more. Dividing the days by 365.25 is not exact.

$ dotnet run
Time elapsed: 13:30:00
78203 days have passed since the Battle of Borodino.
35

C# format time

A date and time format string defines the text representation of a DateTime or DateTimeOffset value that results from a formatting operation. There are two types of format specifiers: standard and custom. A standard format string consists of a single character; a custom format string consists of two or more characters.

Program.cs
DateTime now = new DateTime(2026, 10, 18, 14, 16, 12, 948);

Console.WriteLine(now.ToString("d"));
Console.WriteLine(now.ToString("D"));
Console.WriteLine(now.ToString("f"));
Console.WriteLine(now.ToString("F"));
Console.WriteLine(now.ToString("g"));
Console.WriteLine(now.ToString("G"));
Console.WriteLine(now.ToString("M"));
Console.WriteLine(now.ToString("o"));
Console.WriteLine(now.ToString("R"));
Console.WriteLine(now.ToString("s"));
Console.WriteLine(now.ToString("t"));
Console.WriteLine(now.ToString("T"));
Console.WriteLine(now.ToString("u"));
Console.WriteLine(now.ToString("Y"));

The example prints a date using the standard format specifiers.

$ dotnet run
10/18/2026
Sunday, October 18, 2026
Sunday, October 18, 2026 2:16 PM
Sunday, October 18, 2026 2:16:12 PM
10/18/2026 2:16 PM
10/18/2026 2:16:12 PM
October 18
2026-10-18T14:16:12.9480000
Sun, 18 Oct 2026 14:16:12 GMT
2026-10-18T14:16:12
2:16 PM
2:16:12 PM
2026-10-18 14:16:12Z
October 2026
SpecifierMeaning
d, Dshort and long date pattern
t, Tshort and long time pattern
f, Ffull date and time (without and with seconds)
g, Ggeneral date and time (without and with seconds)
Mmonth and day
Yyear and month
oround-trip (ISO 8601) pattern
ssortable pattern, ISO 8601 without fractions
uuniversal sortable pattern with a Z suffix
RRFC 1123 pattern (HTTP date)
Table: Standard date and time format specifiers

Only o, s, u, and R are culture-independent, so these are the ones to use in files and protocols. The others change with the culture. Note that the R and u formats print the number as is and just append "GMT" or "Z"; they do not convert the time. Call ToUniversalTime first if the value is local. The o format appends the offset (or Z) only when the Kind is local or UTC, as in this unspecified value there is none.

Custom format specifiers allow us to create customized date and time patterns.

Program.cs
DateTime now = new DateTime(2026, 10, 18, 14, 15, 36);

Console.WriteLine(now.ToString("ddd MMM %d, yyyy"));
Console.WriteLine(now.ToString("hh:mm:ss tt"));
Console.WriteLine(now.ToString("yyyy-MM-dd HH:mm:ss"));
Console.WriteLine(now.ToString("dd/MM/yy"));
Console.WriteLine(now.ToString("dddd, d MMMM yyyy"));
Console.WriteLine(now.ToString("H 'h' m 'min'"));
Console.WriteLine(now.ToString("yyyyMMdd_HHmmss"));
Console.WriteLine($"{now:yyyy-MM-dd}");
Console.WriteLine(now.ToString("MM/dd/yyyy"));                                   // '/' is the culture's date separator
Console.WriteLine(now.ToString("MM/dd/yyyy", System.Globalization.CultureInfo.InvariantCulture));

The example prints dates and times using custom specifiers.

Console.WriteLine(now.ToString("ddd MMM %d, yyyy"));

The ddd specifier is the abbreviated name of the day of the week, MMM is the abbreviated name of the month, and d is the day of the month, from 1 through 31. A single-character custom specifier must be preceded with the % character, otherwise it would be taken as a standard format string. yyyy is the year as a four-digit number.

Console.WriteLine(now.ToString("hh:mm:ss tt"));

The hh specifier is the hour on a 12-hour clock from 01 to 12, HH would be the 24-hour clock, mm is the minute, ss is the second, and tt is the AM/PM designator.

Console.WriteLine(now.ToString("H 'h' m 'min'"));

Literal text is enclosed in apostrophes. Without them, letters such as h would be read as specifiers.

Console.WriteLine(now.ToString("MM/dd/yyyy"));

In a custom format string, / stands for the date separator of the current culture, and : for the time separator. In a culture that uses a period, the output would be 10.18.2026. Pass CultureInfo.InvariantCulture to get the characters literally.

$ dotnet run
Sun Oct 18, 2026
02:15:36 PM
2026-10-18 14:15:36
18/10/26
Sunday, 18 October 2026
14 h 15 min
20261018_141536
2026-10-18
10/18/2026
10/18/2026

C# parse time

The Parse method converts the string representation of a date and time to its DateTime equivalent. Since the interpretation of a date string depends on the culture, we should always pass the culture, and for fixed formats use ParseExact. For input that may be wrong, use the TryParse variants.

Program.cs
using System.Globalization;

var inv = CultureInfo.InvariantCulture;

string dateString = "11/5/2019";

DateTime dt = DateTime.Parse(dateString, new CultureInfo("en-US"));
Console.WriteLine($"{dt:d MMMM, yyyy}");

DateTime dt2 = DateTime.Parse(dateString, new CultureInfo("en-GB"));
Console.WriteLine($"{dt2:d MMMM, yyyy}");

// ParseExact removes the guessing
DateTime dt3 = DateTime.ParseExact("2019-11-05 14:30", "yyyy-MM-dd HH:mm", inv);
Console.WriteLine(dt3);

// TryParse for user input
foreach (var s in new[] { "2026-02-30", "2026-02-28", "hello", "" })
{
    if (DateTime.TryParseExact(s, "yyyy-MM-dd", inv, DateTimeStyles.None, out var d))
    {
        Console.WriteLine($"'{s}' -> {d:D}");
    }
    else
    {
        Console.WriteLine($"'{s}' is not a valid date");
    }
}

// ISO 8601 with offset
DateTimeOffset dto = DateTimeOffset.Parse("2026-10-18T14:30:00+02:00", inv);
Console.WriteLine(dto.UtcDateTime);

// the round-trip format keeps the Kind
DateTime utc = new DateTime(2026, 10, 18, 12, 0, 0, DateTimeKind.Utc);
string text = utc.ToString("o");
DateTime again = DateTime.Parse(text, inv, DateTimeStyles.RoundtripKind);
Console.WriteLine($"{text} {again.Kind}");

The program parses dates in several ways.

string dateString = "11/5/2019";

DateTime dt = DateTime.Parse(dateString, new CultureInfo("en-US"));
DateTime dt2 = DateTime.Parse(dateString, new CultureInfo("en-GB"));

The same text means November 5 in the US and May 11 in Great Britain. The culture decides.

DateTime dt3 = DateTime.ParseExact("2019-11-05 14:30", "yyyy-MM-dd HH:mm", inv);

ParseExact accepts only the given format, which removes the guessing.

if (DateTime.TryParseExact(s, "yyyy-MM-dd", inv, DateTimeStyles.None, out var d))

TryParseExact returns false for text that does not match, including impossible dates such as February 30, instead of throwing an exception.

DateTime again = DateTime.Parse(text, inv, DateTimeStyles.RoundtripKind);

The round-trip format (o) together with DateTimeStyles.RoundtripKind restores the Kind of the original value, which is why UTC values written to text and read back remain UTC.

$ dotnet run
5 November, 2019
11 May, 2019
11/5/2019 2:30:00 PM
'2026-02-30' is not a valid date
'2026-02-28' -> Saturday, February 28, 2026
'hello' is not a valid date
'' is not a valid date
10/18/2026 12:30:00 PM
2026-10-18T12:00:00.0000000Z Utc

The DateTimeOffset.Parse of the ISO 8601 text with the offset +02:00 gives 12:30 in UTC, shown in the penultimate line.

C# DateOnly and TimeOnly

The DateOnly structure represents a calendar date without a time, such as a birthday or a due date. TimeOnly represents a time of day, such as an opening time, without a date or a time zone. They avoid the bugs that come from a forgotten time part or from implicit time zone conversions.

Program.cs
DateOnly d = new DateOnly(2026, 10, 18);
TimeOnly t = new TimeOnly(14, 30);

Console.WriteLine(d);
Console.WriteLine(t);
Console.WriteLine(d.AddDays(30));
Console.WriteLine(d.DayOfWeek);
Console.WriteLine(t.AddHours(10));              // wraps around midnight
Console.WriteLine(t.IsBetween(new TimeOnly(9, 0), new TimeOnly(17, 0)));

DateTime combined = d.ToDateTime(t);
Console.WriteLine(combined);

Console.WriteLine(DateOnly.Parse("2026-12-24").DayNumber - d.DayNumber);
Console.WriteLine(DateOnly.ParseExact("24.12.2026", "dd.MM.yyyy"));

var open = new TimeOnly(22, 0);
var close = new TimeOnly(2, 0);
Console.WriteLine(close - open);                // the span from 22:00 to 02:00 the next day

The example uses both types.

Console.WriteLine(t.AddHours(10));

A TimeOnly wraps around midnight: 14:30 plus ten hours is 00:30.

Console.WriteLine(DateOnly.Parse("2026-12-24").DayNumber - d.DayNumber);

The DayNumber property counts days since January 1, 0001, which makes the difference of two dates a simple subtraction.

Console.WriteLine(close - open);

Subtracting two times of day gives the time span from the first to the second, going through midnight if needed: from 22:00 to 02:00 is four hours.

$ dotnet run
10/18/2026
2:30 PM
11/17/2026
Sunday
12:30 AM
True
10/18/2026 2:30:00 PM
67
12/24/2026
04:00:00

C# Time zones

A time zone is a region throughout which the same standard time is used. The relationship between UTC and local time is described by the offset (also called the bias):

UTC = local time - offset

The offset can change during the year because of daylight saving time (DST). TimeZoneInfo is the class for working with time zones in C#.

Program.cs
TimeZoneInfo localZone = TimeZoneInfo.Local;

Console.WriteLine("Id: {0}", localZone.Id);
Console.WriteLine("Standard name: {0}", localZone.StandardName);
Console.WriteLine("Daylight name: {0}", localZone.DaylightName);
Console.WriteLine("Bias: {0}", localZone.BaseUtcOffset);
Console.WriteLine("Supports DST: {0}", localZone.SupportsDaylightSavingTime);

var summer = new DateTime(2026, 7, 1, 12, 0, 0);
var winter = new DateTime(2026, 1, 1, 12, 0, 0);
Console.WriteLine(localZone.GetUtcOffset(summer));
Console.WriteLine(localZone.GetUtcOffset(winter));
Console.WriteLine(localZone.IsDaylightSavingTime(summer));

The program prints information about the local time zone.

TimeZoneInfo localZone = TimeZoneInfo.Local;

Using the Local property, we get the local time zone.

Console.WriteLine("Standard name: {0}", localZone.StandardName);
Console.WriteLine("Daylight name: {0}", localZone.DaylightName);
Console.WriteLine("Bias: {0}", localZone.BaseUtcOffset);

The StandardName gives the standard name of the time zone and the DaylightName its daylight saving name. The BaseUtcOffset is the offset from UTC without DST.

Console.WriteLine(localZone.GetUtcOffset(summer));
Console.WriteLine(localZone.GetUtcOffset(winter));

GetUtcOffset returns the actual offset at a given time, including DST: two hours in July and one hour in January.

$ dotnet run
Id: Europe/Bratislava
Standard name: Central European Standard Time
Daylight name: Central European Summer Time
Bias: 01:00:00
Supports DST: True
02:00:00
01:00:00
True

On Linux and macOS, the id of a zone is an IANA name; on Windows, the ids are names such as "Central Europe Standard Time". Since .NET 6, both kinds can be used on all platforms, given ICU on Windows, and the TimeZoneInfo.TryConvertIanaIdToWindowsId method converts between them.

C# convert between time zones

The FindSystemTimeZoneById method finds a time zone, and ConvertTimeFromUtc and ConvertTime convert times between zones, taking DST into account.

Program.cs
// IANA ids (America/New_York) work on Linux and macOS, and on Windows since .NET 6 with ICU
DateTime utc = new DateTime(2026, 10, 18, 12, 0, 0, DateTimeKind.Utc);

foreach (var id in new[] { "UTC", "Europe/Bratislava", "America/New_York", "Asia/Tokyo", "Australia/Sydney" })
{
    TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(id);
    DateTime converted = TimeZoneInfo.ConvertTimeFromUtc(utc, zone);
    Console.WriteLine($"{id,-20} {converted:yyyy-MM-dd HH:mm}  UTC{zone.GetUtcOffset(utc):hh\\:mm}");
}

// convert a wall-clock time in one zone to another
var ny = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
var bratislava = TimeZoneInfo.FindSystemTimeZoneById("Europe/Bratislava");

DateTime call = new DateTime(2026, 10, 20, 9, 0, 0);       // 09:00 in New York
Console.WriteLine(TimeZoneInfo.ConvertTime(call, ny, bratislava));

// the ids that exist on this system
Console.WriteLine(TimeZoneInfo.GetSystemTimeZones().Count > 100);
Console.WriteLine(TimeZoneInfo.TryConvertIanaIdToWindowsId("Europe/Bratislava", out var win) ? win : "?");

The program shows the same instant in five time zones, and converts a time in New York to Bratislava.

TimeZoneInfo zone = TimeZoneInfo.FindSystemTimeZoneById(id);
DateTime converted = TimeZoneInfo.ConvertTimeFromUtc(utc, zone);

The method throws a TimeZoneNotFoundException if the id is unknown, so use TryFindSystemTimeZoneById for ids that come from user input.

Console.WriteLine(TimeZoneInfo.ConvertTime(call, ny, bratislava));

The call is at 09:00 in New York on October 20; the clocks in New York still have DST while Bratislava has already left it, so the difference is six hours and the result is 15:00.

$ dotnet run
UTC                  2026-10-18 12:00  UTC00:00
Europe/Bratislava    2026-10-18 14:00  UTC02:00
America/New_York     2026-10-18 08:00  UTC04:00
Asia/Tokyo           2026-10-18 21:00  UTC09:00
Australia/Sydney     2026-10-18 23:00  UTC11:00
10/20/2026 3:00:00 PM
True
Central Europe Standard Time

The GetSystemTimeZones method returns a sorted collection of all time zones available on the system. A container image without time zone data (tzdata) has few or none of them, and the lookup fails; the .NET runtime images and the full SDK images include the data.

C# daylight saving time pitfalls

Daylight saving time makes some local times invalid, others ambiguous, and some days longer or shorter than 24 hours.

Program.cs
var zone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Bratislava");

// 2026-03-29 02:30 does not exist in Central Europe: the clocks jump from 02:00 to 03:00
var gap = new DateTime(2026, 3, 29, 2, 30, 0);
Console.WriteLine(zone.IsInvalidTime(gap));

// 2026-10-25 02:30 exists twice: the clocks go back from 03:00 to 02:00
var overlap = new DateTime(2026, 10, 25, 2, 30, 0);
Console.WriteLine(zone.IsAmbiguousTime(overlap));

foreach (var offset in zone.GetAmbiguousTimeOffsets(overlap))
{
    Console.WriteLine(offset);
}

// a day is not always 24 hours
var day1 = new DateTime(2026, 10, 25, 0, 0, 0);
var day2 = day1.AddDays(1);

var start = new DateTimeOffset(day1, zone.GetUtcOffset(day1));
var end = new DateTimeOffset(day2, zone.GetUtcOffset(day2));

Console.WriteLine(end - start);
Console.WriteLine(day2 - day1);

The program examines the changes in the Central European time zone in 2026.

var gap = new DateTime(2026, 3, 29, 2, 30, 0);
Console.WriteLine(zone.IsInvalidTime(gap));

When the clocks go forward from 02:00 to 03:00, the local time 02:30 never happens.

var overlap = new DateTime(2026, 10, 25, 2, 30, 0);
Console.WriteLine(zone.IsAmbiguousTime(overlap));

When the clocks go back from 03:00 to 02:00, the local time 02:30 happens twice, once with the offset +02:00 and once with +01:00. A local time alone cannot tell which one is meant, which is another reason to store UTC or a DateTimeOffset.

Console.WriteLine(end - start);
Console.WriteLine(day2 - day1);

October 25, 2026 is 25 hours long in Central Europe. The subtraction of instants shows the real elapsed time, 1 day and 1 hour, while the subtraction of the local DateTime values reports exactly one day. If the exact duration matters, convert to UTC first.

$ dotnet run
True
True
01:00:00
02:00:00
1.01:00:00
1.00:00:00

The two offsets of the ambiguous time are 01:00:00 and 02:00:00 (listed with the standard offset first).

C# time zone information from format specifiers

The offset can also be formatted from a DateTime or DateTimeOffset with the zzz specifier. For a DateTime it uses the time zone of the machine, so it only makes sense for local values; DateTimeOffset holds a real offset.

DateTimeOffset.Now.ToString("zzz")   // +02:00
DateTime.Now.ToString("%K")          // +02:00 (for a local DateTime)
DateTime.UtcNow.ToString("%K")       // Z

Calendar

A calendar is a system of organizing datetime units. Cultures use different calendars, and some cultures can use several of them. The .NET calendar classes such as PersianCalendar, HebrewCalendar, HijriCalendar, and ThaiBuddhistCalendar convert dates and format them. Internally, a DateTime always uses the Gregorian calendar; the calendar affects only the conversion to and from text and the calendar-specific methods.

Program.cs
using System.Globalization;

Console.OutputEncoding = System.Text.Encoding.UTF8;

var perCi = new CultureInfo("fa-IR");
Console.WriteLine($"Default calendar: {perCi.Calendar}");

DateTime now = new DateTime(2026, 10, 18, 13, 35, 18);

foreach (var oc in perCi.OptionalCalendars)
{
    perCi.DateTimeFormat.Calendar = oc;
    Console.WriteLine(now.ToString("F", perCi));
}

var hc = new HebrewCalendar();
var d1 = new DateTime(5776, 10, 7, hc);

Console.WriteLine($"{d1:d}");
Console.WriteLine($"{hc.GetYear(d1)}/{hc.GetMonth(d1)}/{hc.GetDayOfMonth(d1)}");

var thai = new ThaiBuddhistCalendar();
Console.WriteLine(thai.GetYear(now));

The Persian culture uses the Persian calendar as the default and the Gregorian and Hijri calendars as optional ones. We display one datetime in all supported calendars, then work with the Hebrew and the Thai Buddhist calendar.

foreach (var oc in perCi.OptionalCalendars)
{
    perCi.DateTimeFormat.Calendar = oc;
    Console.WriteLine(now.ToString("F", perCi));
}

We go through the array of supported calendars, set DateTimeFormat.Calendar to the current one, and print the datetime.

var hc = new HebrewCalendar();
var d1 = new DateTime(5776, 10, 7, hc);

The calendar is passed as the last argument of the DateTime constructor. The year 5776 in the Hebrew calendar is 2016 in the Gregorian one, and GetYear, GetMonth, and GetDayOfMonth of the calendar return the Hebrew components.

$ dotnet run
Default calendar: System.Globalization.PersianCalendar
1405 مهر 26, یکشنبه 13:35:18
2026 اکتبر 18, یکشنبه 13:35:18
1448 جمادی‌الاول 7, یکشنبه 13:35:18
6/13/2016
5776/10/7
2569

C# Leap year

A leap year is a year containing an additional day. The reason for the extra day in the calendar is the difference between the astronomical and the calendar year (about 365.2425 days).

Program.cs
int[] years = [2000, 2002, 2004, 2008, 2012, 2016, 2020, 2024, 2026, 1900, 1800, 1600];

foreach (int year in years)
{
    string answer = DateTime.IsLeapYear(year) ? "is a leap year" : "is not a leap year";
    Console.WriteLine($"{year} {answer}");
}

// the rule: divisible by 4, except centuries, unless divisible by 400
static bool IsLeap(int y) => y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
Console.WriteLine(years.All(y => IsLeap(y) == DateTime.IsLeapYear(y)));

We have an array of years and check whether they are leap years. The IsLeapYear method determines whether a year is a leap year in the Gregorian calendar.

static bool IsLeap(int y) => y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);

This is the rule: a year is a leap year if it is divisible by 4, except for century years, which must be divisible by 400. That is why 1900 and 1800 are not leap years and 1600 and 2000 are. The last check confirms that the manual rule agrees with the library.

$ dotnet run
2000 is a leap year
2002 is not a leap year
2004 is a leap year
2008 is a leap year
2012 is a leap year
2016 is a leap year
2020 is a leap year
2024 is a leap year
2026 is not a leap year
1900 is not a leap year
1800 is not a leap year
1600 is a leap year
True

C# week numbers and calendar calculations

The ISOWeek class works with ISO 8601 weeks, which start on Monday; the first week of a year is the one with its first Thursday. Common calculations, such as the first and last day of a month, are short.

Program.cs
using System.Globalization;

DateTime d = new DateTime(2026, 10, 18);

Console.WriteLine(ISOWeek.GetWeekOfYear(d));
Console.WriteLine(ISOWeek.GetYear(d));
Console.WriteLine(ISOWeek.ToDateTime(2026, 43, DayOfWeek.Monday).ToString("d"));
Console.WriteLine(ISOWeek.GetWeeksInYear(2026));

// first and last day of the month
var first = new DateTime(d.Year, d.Month, 1);
var last = first.AddMonths(1).AddDays(-1);
Console.WriteLine($"{first:d} - {last:d}");

// next Monday
int daysUntilMonday = ((int)DayOfWeek.Monday - (int)d.DayOfWeek + 7) % 7;
Console.WriteLine(d.AddDays(daysUntilMonday == 0 ? 7 : daysUntilMonday).ToString("D"));

// the days of a week from the culture's first day of the week
var fdow = CultureInfo.GetCultureInfo("en-US").DateTimeFormat.FirstDayOfWeek;
Console.WriteLine(fdow);

The program computes the week number, the month bounds, and the date of the next Monday.

Console.WriteLine(ISOWeek.GetWeekOfYear(d));
Console.WriteLine(ISOWeek.GetYear(d));

The week-based year can differ from the calendar year near the end of December and the beginning of January, so ISOWeek.GetYear must be used together with the week number.

var last = first.AddMonths(1).AddDays(-1);

The last day of a month is the day before the first day of the next month; this works without knowing the length of the month.

int daysUntilMonday = ((int)DayOfWeek.Monday - (int)d.DayOfWeek + 7) % 7;

The modulo expression gives the number of days until the next Monday; if today is Monday, we add a full week. In the United States culture, the week starts on Sunday, which is why the last line prints Sunday.

$ dotnet run
42
2026
10/19/2026
53
10/1/2026 - 10/31/2026
Monday, October 19, 2026
Sunday

The year 2026 has 53 ISO weeks.

C# TimeProvider

Code that calls DateTime.Now directly is hard to test, because the result changes with every run. Since .NET 8, the abstract TimeProvider class is the standard way to get the current time. In production, we use TimeProvider.System; in tests, we pass a fake that returns the time we want.

Program.cs
// TimeProvider (.NET 8) makes code that depends on "now" testable
var clock = new FakeClock(new DateTimeOffset(2026, 10, 18, 9, 0, 0, TimeSpan.Zero));

var service = new Greeter(clock);
Console.WriteLine(service.Greet());

clock.Advance(TimeSpan.FromHours(10));
Console.WriteLine(service.Greet());

// the real clock
Console.WriteLine(TimeProvider.System.GetUtcNow().Year >= 2026);

// measure elapsed time without Stopwatch.StartNew allocations
long start = TimeProvider.System.GetTimestamp();
await Task.Delay(100);
Console.WriteLine(TimeProvider.System.GetElapsedTime(start).TotalMilliseconds >= 100);

class Greeter(TimeProvider time)
{
    public string Greet() => time.GetUtcNow().Hour < 12 ? "Good morning" : "Good afternoon";
}

class FakeClock(DateTimeOffset start) : TimeProvider
{
    private DateTimeOffset now = start;

    public override DateTimeOffset GetUtcNow() => now;

    public void Advance(TimeSpan by) => now += by;
}

The program injects the clock into a class and uses a fake clock to control the time.

class FakeClock(DateTimeOffset start) : TimeProvider
{
    private DateTimeOffset now = start;

    public override DateTimeOffset GetUtcNow() => now;

    public void Advance(TimeSpan by) => now += by;
}

Overriding GetUtcNow is enough to define the clock. The test advances it by ten hours, and the greeting changes. The Microsoft.Extensions.TimeProvider.Testing package contains a ready-made FakeTimeProvider.

long start = TimeProvider.System.GetTimestamp();
Console.WriteLine(TimeProvider.System.GetElapsedTime(start).TotalMilliseconds >= 100);

GetTimestamp and GetElapsedTime measure intervals with the high-resolution timer, like Stopwatch, but through the same abstraction.

$ dotnet run
Good morning
Good afternoon
True
True

Source

Dates, times, and time zones

In this article we have worked with date and time in C#.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.