ZetCode

C# TimeZoneInfo

last modified October 18, 2026

In this article we work with time zones in C# using TimeZoneInfo.

A time zone is a geographical region in which the same time is used. For instance, in Bratislava, Prague, or Budapest the Central European Time is used. A zone is more than a fixed offset from UTC: it has a name, a history of changes, and usually rules for daylight saving time (DST), when the offset changes during the year.

TimeZoneInfo is the main class for working with time zones in C#. It is an improvement over the old, limited TimeZone class. It allows us to enumerate the time zones available on the system, find a zone by its identifier, convert times between zones, create custom zones, and serialize a zone for later use.

The time zone data comes from the operating system. Windows has its own registry with ids such as Central Europe Standard Time. Linux and macOS use the IANA time zone database, with ids such as Europe/Bratislava. Since .NET 6 on Windows, and .NET 8 on Linux and macOS, both kinds of ids are accepted everywhere (Windows needs the ICU libraries, which are part of the system on current versions). The examples were run on Linux, so the names and display texts come from the IANA data and ICU; on Windows they differ.

Note: a container image without the tzdata package has no time zone data; TimeZoneInfo.Local is then UTC, and finding any other zone fails. Install tzdata in such images.

C# TimeZoneInfo.GetSystemTimeZones

The TimeZoneInfo.GetSystemTimeZones method returns a sorted collection of all the time zones available on the local system.

Program.cs
using System.Collections.ObjectModel;

ReadOnlyCollection<TimeZoneInfo> zones = TimeZoneInfo.GetSystemTimeZones();

Console.WriteLine($"{zones.Count} time zones");

foreach (TimeZoneInfo zone in zones.Take(5))
{
    Console.WriteLine($"{zone.Id,-24} {zone.DisplayName}");
}

Console.WriteLine("...");

// the zones with a UTC+01:00 base offset
var plusOne = zones.Where(z => z.BaseUtcOffset == TimeSpan.FromHours(1)).Select(z => z.Id).Take(5);
Console.WriteLine(string.Join(", ", plusOne));

The program counts the available time zones, prints a few of them, and filters the collection with LINQ.

$ dotnet run
419 time zones
Pacific/Niue             (UTC-11:00) Niue Time
Pacific/Midway           (UTC-11:00) Samoa Standard Time (Midway)
Pacific/Pago_Pago        (UTC-11:00) Samoa Standard Time (Pago Pago)
Pacific/Rarotonga        (UTC-10:00) Cook Islands Standard Time (Rarotonga)
America/Adak             (UTC-10:00) Hawaii-Aleutian Time (Adak)
...
Africa/Algiers, Europe/Amsterdam, Europe/Andorra, Europe/Belgrade, Europe/Berlin

The collection is sorted by the base UTC offset and then by the display name, not by the id. The number of zones depends on the system.

C# TimeZoneInfo.Local

The TimeZoneInfo.Local property returns a TimeZoneInfo object that represents the local time zone of the machine. TimeZoneInfo.Utc is the UTC zone.

Program.cs
TimeZoneInfo local = TimeZoneInfo.Local;

Console.WriteLine(local.DisplayName);
Console.WriteLine(local.Id);
Console.WriteLine(local.StandardName);
Console.WriteLine(local.DaylightName);
Console.WriteLine(local.BaseUtcOffset);
Console.WriteLine(local.SupportsDaylightSavingTime);
Console.WriteLine(TimeZoneInfo.Utc.Id);

In the program, we print several properties of the local time zone.

Console.WriteLine(local.DisplayName);
Console.WriteLine(local.Id);

The DisplayName is a text for the user interface, the Id identifies the zone in code.

Console.WriteLine(local.StandardName);
Console.WriteLine(local.DaylightName);
Console.WriteLine(local.BaseUtcOffset);

The StandardName and DaylightName are the names of the zone in winter and summer. The BaseUtcOffset is the offset from UTC without DST, so the actual offset in summer is one hour more.

$ dotnet run
(UTC+01:00) Central European Time (Bratislava)
Europe/Bratislava
Central European Standard Time
Central European Summer Time
01:00:00
True
UTC

On Windows, the same machine would report (UTC+01:00) Belgrade, Bratislava, Budapest, Ljubljana, Prague and Central Europe Standard Time.

C# TimeZoneInfo.FindSystemTimeZoneById

The TimeZoneInfo.FindSystemTimeZoneById method retrieves a TimeZoneInfo object based on the given identifier. It throws a TimeZoneNotFoundException when the zone does not exist, so for ids that come from users or configuration, the TryFindSystemTimeZoneById method is the better choice.

Program.cs
// IANA ids work everywhere with ICU; Windows ids are accepted on Windows and, since .NET 8, elsewhere too
foreach (var id in new[] { "Europe/Moscow", "Russian Standard Time", "Mars/Olympus" })
{
    if (TimeZoneInfo.TryFindSystemTimeZoneById(id, out TimeZoneInfo? tzi))
    {
        Console.WriteLine($"{tzi.Id}: {tzi.DisplayName} | {tzi.StandardName} | DST: {tzi.SupportsDaylightSavingTime}");
    }
    else
    {
        Console.WriteLine($"{id}: not found");
    }
}

try
{
    TimeZoneInfo.FindSystemTimeZoneById("Mars/Olympus");
}
catch (TimeZoneNotFoundException e)
{
    Console.WriteLine("TimeZoneNotFoundException: " + e.Message);
}

// converting ids
Console.WriteLine(TimeZoneInfo.TryConvertIanaIdToWindowsId("Europe/Bratislava", out var win) ? win : "?");
Console.WriteLine(TimeZoneInfo.TryConvertWindowsIdToIanaId("Central Europe Standard Time", out var iana) ? iana : "?");
Console.WriteLine(TimeZoneInfo.TryConvertWindowsIdToIanaId("Central Europe Standard Time", "SK", out var sk) ? sk : "?");

In the program, we look up a zone by its IANA id and by its Windows id, and try an id that does not exist. Finally, we convert between the two kinds of ids.

foreach (var id in new[] { "Europe/Moscow", "Russian Standard Time", "Mars/Olympus" })

Both ids of the Moscow zone lead to the same zone. The returned Id is the one that was asked for.

Console.WriteLine(TimeZoneInfo.TryConvertWindowsIdToIanaId("Central Europe Standard Time", "SK", out var sk) ? sk : "?");

One Windows zone maps to several IANA zones. Without a region, the conversion returns the representative zone (Europe/Budapest); with a two-letter region code, it returns the zone for the country.

$ dotnet run
Europe/Moscow: (UTC+03:00) Moscow Time | Moscow Standard Time | DST: True
Russian Standard Time: (UTC+03:00) Moscow Time | Moscow Standard Time | DST: True
Mars/Olympus: not found
TimeZoneNotFoundException: The time zone ID 'Mars/Olympus' was not found on the local computer.
Central Europe Standard Time
Europe/Budapest
Europe/Bratislava

Note DST: True for Moscow, although Russia has not used DST since 2014. The next section explains it.

C# TimeZoneInfo.ConvertTime

The TimeZoneInfo.ConvertTime method converts a time to the time in the given time zone. There are variants for special cases: ConvertTimeFromUtc, ConvertTimeToUtc, and ConvertTimeBySystemTimeZoneId.

Program.cs
// a fixed instant makes the output reproducible
var utc = new DateTime(2026, 10, 18, 12, 0, 0, DateTimeKind.Utc);

TimeZoneInfo moscow = TimeZoneInfo.FindSystemTimeZoneById("Europe/Moscow");
TimeZoneInfo local = TimeZoneInfo.Local;

DateTime inLocal = TimeZoneInfo.ConvertTimeFromUtc(utc, local);
DateTime inMoscow = TimeZoneInfo.ConvertTimeFromUtc(utc, moscow);

Console.WriteLine($"UTC:    {utc:yyyy-MM-dd HH:mm}");
Console.WriteLine($"Local:  {inLocal:yyyy-MM-dd HH:mm} ({local.Id})");
Console.WriteLine($"Moscow: {inMoscow:yyyy-MM-dd HH:mm}");

// from one zone to another
DateTime back = TimeZoneInfo.ConvertTime(inMoscow, moscow, local);
Console.WriteLine($"Moscow to local: {back:yyyy-MM-dd HH:mm}");

// to UTC
DateTime toUtc = TimeZoneInfo.ConvertTimeToUtc(inMoscow, moscow);
Console.WriteLine($"Moscow to UTC: {toUtc:yyyy-MM-dd HH:mm} {toUtc.Kind}");

// by id, without creating TimeZoneInfo objects first
DateTime tokyo = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(utc, "Asia/Tokyo");
Console.WriteLine($"Tokyo:  {tokyo:yyyy-MM-dd HH:mm}");

// DateTimeOffset keeps the offset
DateTimeOffset dto = TimeZoneInfo.ConvertTime(new DateTimeOffset(utc), moscow);
Console.WriteLine(dto);

We take a fixed instant in UTC and show it in several zones. A fixed instant makes the output reproducible.

DateTime inMoscow = TimeZoneInfo.ConvertTimeFromUtc(utc, moscow);

ConvertTimeFromUtc requires a source that is UTC (its Kind must not be Local). The result has the Unspecified kind, as it is the wall-clock time of the zone.

DateTime back = TimeZoneInfo.ConvertTime(inMoscow, moscow, local);

The three-argument ConvertTime converts from the source zone to the destination zone. We have to name the source zone, since a bare DateTime does not know in which zone it was created. The two-argument overload assumes the source is the local zone when the kind is Local or Unspecified, and UTC when the kind is Utc.

DateTimeOffset dto = TimeZoneInfo.ConvertTime(new DateTimeOffset(utc), moscow);

With a DateTimeOffset, the result carries the proper offset (+03:00), which makes it the safest type for passing around a converted time.

$ dotnet run
UTC:    2026-10-18 12:00
Local:  2026-10-18 14:00 (Europe/Bratislava)
Moscow: 2026-10-18 15:00
Moscow to local: 2026-10-18 14:00
Moscow to UTC: 2026-10-18 12:00 Utc
Tokyo:  2026-10-18 21:00
10/18/2026 3:00:00 PM +03:00

C# TimeZoneInfo and daylight saving time

The offset of a zone depends on the date. GetUtcOffset returns the real offset at a given time, and IsDaylightSavingTime tells whether DST is in effect.

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

var winter = new DateTime(2026, 1, 15, 12, 0, 0);
var summer = new DateTime(2026, 7, 15, 12, 0, 0);

Console.WriteLine($"base offset:   {zone.BaseUtcOffset}");
Console.WriteLine($"winter offset: {zone.GetUtcOffset(winter)}  DST: {zone.IsDaylightSavingTime(winter)}");
Console.WriteLine($"summer offset: {zone.GetUtcOffset(summer)}  DST: {zone.IsDaylightSavingTime(summer)}");

// the length of a local day around the changes
var zone2 = TimeZoneInfo.FindSystemTimeZoneById("Europe/Bratislava");
foreach (var day in new[] { new DateTime(2026, 3, 29), new DateTime(2026, 6, 1), new DateTime(2026, 10, 25) })
{
    DateTime startUtc = TimeZoneInfo.ConvertTimeToUtc(day, zone2);
    DateTime nextUtc = TimeZoneInfo.ConvertTimeToUtc(day.AddDays(1), zone2);
    Console.WriteLine($"{day:yyyy-MM-dd} lasts {(nextUtc - startUtc).TotalHours} hours");
}

The program shows the offsets of Bratislava in winter and in summer, and the length of three days around the clock changes.

Console.WriteLine($"base offset:   {zone.BaseUtcOffset}");
Console.WriteLine($"winter offset: {zone.GetUtcOffset(winter)}  DST: {zone.IsDaylightSavingTime(winter)}");
Console.WriteLine($"summer offset: {zone.GetUtcOffset(summer)}  DST: {zone.IsDaylightSavingTime(summer)}");

The base offset is always the standard one. In summer, the actual offset is two hours.

DateTime startUtc = TimeZoneInfo.ConvertTimeToUtc(day, zone2);
DateTime nextUtc = TimeZoneInfo.ConvertTimeToUtc(day.AddDays(1), zone2);

To measure a local day, we convert its start and the start of the next day to UTC and subtract. Local days are not always 24 hours long: the day when the clocks go forward has 23 hours and the day when they go back has 25.

$ dotnet run
base offset:   01:00:00
winter offset: 01:00:00  DST: False
summer offset: 02:00:00  DST: True
2026-03-29 lasts 23 hours
2026-06-01 lasts 24 hours
2026-10-25 lasts 25 hours

C# find the DST transitions

The GetAdjustmentRules method describes the DST rules, but their form depends on the platform and is awkward to use. A robust way to find the moments when the clocks change is to look for changes of the offset. The SupportsDaylightSavingTime property says whether a zone has ever had DST rules, not whether it uses them now.

Program.cs
// find the moments when the clocks change in a year, by scanning the offsets hour by hour
var zone = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");

var t = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var end = new DateTime(2027, 1, 1, 0, 0, 0, DateTimeKind.Utc);
var previous = zone.GetUtcOffset(t);

for (; t < end; t = t.AddHours(1))
{
    var offset = zone.GetUtcOffset(t);

    if (offset != previous)
    {
        Console.WriteLine($"{t:yyyy-MM-dd HH:mm} UTC: offset {previous} -> {offset}");
        previous = offset;
    }
}

// SupportsDaylightSavingTime is true for any zone that ever had DST rules, even if it does not use them now,
// so ask about a concrete date instead
var summer = new DateTime(2026, 7, 1, 12, 0, 0, DateTimeKind.Utc);
var winter = new DateTime(2026, 1, 1, 12, 0, 0, DateTimeKind.Utc);

foreach (var id in new[] { "Asia/Tokyo", "Asia/Kolkata", "America/Phoenix", "Europe/Berlin", "Australia/Sydney", "UTC" })
{
    var tz = TimeZoneInfo.FindSystemTimeZoneById(id);
    bool usesDst = tz.GetUtcOffset(summer) != tz.GetUtcOffset(winter);
    Console.WriteLine($"{id,-17} uses DST in 2026: {usesDst,-5} Supports: {tz.SupportsDaylightSavingTime}");
}

The program scans the year 2026 hour by hour for New York, and then checks which zones really change their offset.

for (; t < end; t = t.AddHours(1))
{
    var offset = zone.GetUtcOffset(t);

    if (offset != previous)
    {

We ask for the offset at each hour of the year (8,760 calls, which is fast) and print a line whenever it differs from the previous one. The loop reports the two transitions of the United States: on March 8 the clocks go forward, on November 1 they go back. The times are in UTC.

bool usesDst = tz.GetUtcOffset(summer) != tz.GetUtcOffset(winter);

Comparing the offset in July and in January is a simple test for the use of DST. Tokyo, Kolkata, and Phoenix report Supports: True, because they had DST rules at some point in the past, but they do not use DST in 2026. The southern hemisphere zones, such as Sydney, have DST in the other half of the year, but the offsets of July and January still differ.

$ dotnet run
2026-03-08 07:00 UTC: offset -05:00:00 -> -04:00:00
2026-11-01 06:00 UTC: offset -04:00:00 -> -05:00:00
Asia/Tokyo        uses DST in 2026: False Supports: True
Asia/Kolkata      uses DST in 2026: False Supports: True
America/Phoenix   uses DST in 2026: False Supports: True
Europe/Berlin     uses DST in 2026: True  Supports: True
Australia/Sydney  uses DST in 2026: True  Supports: True
UTC               uses DST in 2026: False Supports: False

C# meeting planner

A common task is to show a meeting in the local times of the participants. We store the meeting as a time in the organizer's zone, convert it to UTC, and then to each participant's zone.

Program.cs
// a meeting at 15:00 in Bratislava: what time is it for the others?
var zone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Bratislava");
var meeting = new DateTime(2026, 10, 27, 15, 0, 0);          // wall-clock time in Bratislava

DateTime utc = TimeZoneInfo.ConvertTimeToUtc(meeting, zone);
Console.WriteLine($"UTC: {utc:yyyy-MM-dd HH:mm}");

string[] ids = ["Europe/London", "America/New_York", "America/Los_Angeles", "Asia/Kolkata", "Asia/Tokyo", "Australia/Sydney"];

foreach (var id in ids)
{
    var other = TimeZoneInfo.FindSystemTimeZoneById(id);
    DateTime time = TimeZoneInfo.ConvertTimeFromUtc(utc, other);

    string day = time.Date == meeting.Date ? "" : time.Date > meeting.Date ? " (next day)" : " (previous day)";
    Console.WriteLine($"{id,-22} {time:HH:mm}{day}  UTC{other.GetUtcOffset(utc):hh\\:mm}");
}

// during the weeks when the US and Europe change their clocks on different dates,
// the difference between the two is not constant
foreach (var date in new[] { new DateTime(2026, 3, 20, 12, 0, 0), new DateTime(2026, 3, 30, 12, 0, 0), new DateTime(2026, 11, 2, 12, 0, 0) })
{
    var ny = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
    var diff = zone.GetUtcOffset(date) - ny.GetUtcOffset(date);
    Console.WriteLine($"{date:yyyy-MM-dd}: Bratislava is {diff.TotalHours} hours ahead of New York");
}

The meeting is on October 27, 2026, at 15:00 in Bratislava.

DateTime utc = TimeZoneInfo.ConvertTimeToUtc(meeting, zone);

The meeting time is converted to UTC, with the rules of the Bratislava zone for that date. A date in the future depends on the rules that are valid today; if a government changes the rules, a stored UTC time for a future meeting would give the wrong local time. For future events, it is better to store the local time and the zone id and convert when needed.

foreach (var date in new[] { ... })
{
    var ny = TimeZoneInfo.FindSystemTimeZoneById("America/New_York");
    var diff = zone.GetUtcOffset(date) - ny.GetUtcOffset(date);

The last loop shows that the difference between two zones is not constant. The United States change the clocks on the second Sunday of March and the first Sunday of November, while Europe changes on the last Sundays of March and October. In the weeks between, the gap between Bratislava and New York is five hours instead of six.

$ dotnet run
UTC: 2026-10-27 14:00
Europe/London          14:00  UTC00:00
America/New_York       10:00  UTC04:00
America/Los_Angeles    07:00  UTC07:00
Asia/Kolkata           19:30  UTC05:30
Asia/Tokyo             23:00  UTC09:00
Australia/Sydney       01:00 (next day)  UTC11:00
2026-03-20: Bratislava is 5 hours ahead of New York
2026-03-30: Bratislava is 6 hours ahead of New York
2026-11-02: Bratislava is 6 hours ahead of New York

C# invalid and ambiguous times

When the clocks go forward, some local times do not exist. When they go back, some local times happen twice. TimeZoneInfo can detect both cases.

Program.cs
// the clocks go back in Central Europe on 2026-10-25 at 03:00 local time
var zone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Bratislava");

foreach (var time in new[]
{
    new DateTime(2026, 3, 29, 2, 30, 0),     // does not exist
    new DateTime(2026, 10, 25, 2, 30, 0),    // exists twice
    new DateTime(2026, 6, 1, 2, 30, 0)       // normal
})
{
    Console.Write($"{time:yyyy-MM-dd HH:mm}: ");

    if (zone.IsInvalidTime(time))
    {
        Console.WriteLine("invalid (skipped by the clocks)");
    }
    else if (zone.IsAmbiguousTime(time))
    {
        var offsets = zone.GetAmbiguousTimeOffsets(time);
        Console.WriteLine("ambiguous: " + string.Join(" or ", offsets.Select(o => $"UTC{o:hh\\:mm}")));
    }
    else
    {
        Console.WriteLine($"fine, offset {zone.GetUtcOffset(time)}");
    }
}

// converting an invalid time throws
try
{
    TimeZoneInfo.ConvertTimeToUtc(new DateTime(2026, 3, 29, 2, 30, 0), zone);
}
catch (ArgumentException e)
{
    Console.WriteLine("ArgumentException: " + e.Message);
}

The program classifies three local times in Bratislava.

else if (zone.IsAmbiguousTime(time))
{
    var offsets = zone.GetAmbiguousTimeOffsets(time);

For an ambiguous time, GetAmbiguousTimeOffsets returns the possible offsets. An application must decide which one is meant, or ask the user. When the choice cannot be made, ConvertTimeToUtc assumes the standard time for an ambiguous time.

TimeZoneInfo.ConvertTimeToUtc(new DateTime(2026, 3, 29, 2, 30, 0), zone);

Converting an invalid time throws an ArgumentException, so input from users should be checked with IsInvalidTime first.

$ dotnet run
2026-03-29 02:30: invalid (skipped by the clocks)
2026-10-25 02:30: ambiguous: UTC01:00 or UTC02:00
2026-06-01 02:30: fine, offset 02:00:00
ArgumentException: The supplied DateTime represents an invalid time.  For example, when the clock is adjusted forward, any time in the period that is skipped is invalid. (Parameter 'dateTime')

C# custom time zones and serialization

We can create our own zone with CreateCustomTimeZone, for example for a fixed offset that is not available on the system. A zone can be serialized to a string and restored, which is useful for storing it with the data.

Program.cs
// a custom zone: a fixed offset with a name
TimeZoneInfo custom = TimeZoneInfo.CreateCustomTimeZone(
    id: "Lab Time", baseUtcOffset: TimeSpan.FromHours(5.5), displayName: "(UTC+05:30) Lab Time", standardDisplayName: "Lab Standard Time");

var utc = new DateTime(2026, 10, 18, 12, 0, 0, DateTimeKind.Utc);
Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(utc, custom));
Console.WriteLine(custom.DisplayName);

// serialization of a zone for storage or transfer
string serialized = custom.ToSerializedString();
Console.WriteLine(serialized);

TimeZoneInfo restored = TimeZoneInfo.FromSerializedString(serialized);
Console.WriteLine(restored.Equals(custom));

// comparing zones
var a = TimeZoneInfo.FindSystemTimeZoneById("Europe/Bratislava");
var b = TimeZoneInfo.FindSystemTimeZoneById("Europe/Prague");
Console.WriteLine(a.HasSameRules(b));
Console.WriteLine(a.Equals(b));

The program creates a zone with the offset UTC+05:30, serializes it, and compares two zones.

Console.WriteLine(a.HasSameRules(b));
Console.WriteLine(a.Equals(b));

The Bratislava and Prague zones have the same rules, so HasSameRules is true, but they are different zones with different ids, so Equals is false. For most conversions, either zone gives the same result.

$ dotnet run
10/18/2026 5:30:00 PM
(UTC+05:30) Lab Time
Lab Time;330;(UTC+05:30) Lab Time;Lab Standard Time;Lab Standard Time;;
True
True
False

Source

TimeZoneInfo class

In this article we have worked with time zones in C# using TimeZoneInfo.

Author

I am Jan Bodnar, a programmer and technical writer. I have been writing programming tutorials at ZetCode since 2007 and have published eight e-books. See about the author.

List all C# tutorials.