Arex388.TimeZones
4.0.0
dotnet add package Arex388.TimeZones --version 4.0.0
NuGet\Install-Package Arex388.TimeZones -Version 4.0.0
<PackageReference Include="Arex388.TimeZones" Version="4.0.0" />
<PackageVersion Include="Arex388.TimeZones" Version="4.0.0" />
<PackageReference Include="Arex388.TimeZones" />
paket add Arex388.TimeZones --version 4.0.0
#r "nuget: Arex388.TimeZones, 4.0.0"
#:package Arex388.TimeZones@4.0.0
#addin nuget:?package=Arex388.TimeZones&version=4.0.0
#tool nuget:?package=Arex388.TimeZones&version=4.0.0
Arex388.TimeZones
Offline time zone discovery for .NET Standard 2.0 — by coordinate, IANA id, or Windows id — plus the two conversions consumers actually need: AtTimeZone (keep the wall clock, resolve the zone's offset) and InTimeZone (keep the instant, show it in the zone). Everything is computed from TZDB; nothing is read from the operating system, so the answers are the same on Windows, Linux, and invariant-globalization containers. It is a free replacement for the Google Maps Time Zone API.
It is a facade over four packages, each doing one thing: GeoTimeZone turns a coordinate into an IANA id; NodaTime supplies TZDB — offsets, transitions, daylight-savings rules; TimeZoneConverter maps IANA ids to Windows ids and back; TimeZoneNames supplies the localized names and abbreviations from CLDR.
Install and register
dotnet add package Arex388.TimeZones
services.AddTimeZones();
// or, with options
services.AddTimeZones(o => {
o.DefaultLanguageCode = "de";
});
Then inject ITimeZones. It is a singleton. A registered TimeProvider supplies "now" for the overloads that take no instant; without one, TimeProvider.System does. AddMemoryCache() is no longer required — the library caches only what is static (the per-language catalog of zones and names) and computes offsets per call.
The two operations
Every time zone problem is one of two operations, and they are not interchangeable:
AtTimeZonekeeps the wall clock and resolves its offset in the zone. Use it when the date and time are what the user meant and only the zone is missing — a shift that starts at 09:00 in New York.InTimeZonekeeps the instant and changes the wall clock to the zone's. Use it when the value already identifies a point in time and you want to show it somewhere else.
The same input, on the day US clocks spring forward:
var wallClock = new DateTimeOffset(2026, 3, 8, 9, 0, 0, TimeSpan.Zero);
timeZones.AtTimeZone(wallClock, "America/New_York"); // 2026-03-08 09:00 -04:00 — same clock reading, the zone's offset
timeZones.InTimeZone(wallClock, "America/New_York"); // 2026-03-08 05:00 -04:00 — same instant, the zone's clock reading
AtTimeZone is lenient: a wall clock that does not exist (the spring-forward gap) is shifted forward by the gap, and one that occurs twice (the fall-back hour) resolves to its earlier occurrence. TryAtTimeZone tells you which happened:
timeZones.TryAtTimeZone(new DateTime(2026, 3, 8, 2, 30, 0), "America/New_York", out var result, out var resolution);
// result: 2026-03-08 03:30 -04:00, resolution: TimeZoneResolution.Skipped
timeZones.TryAtTimeZone(new DateTime(2026, 11, 1, 1, 30, 0), "America/New_York", out result, out resolution);
// result: 2026-11-01 01:30 -04:00, resolution: TimeZoneResolution.Ambiguous
Both accept a DateTime (its Kind is ignored) or a DateTimeOffset (its offset is ignored — only the clock reading matters). Nullable-lifting overloads (DateTime?, DateTimeOffset? → null in, null out) live in TimeZonesExtensions, along with DateTimeOffset.AtTimeZone(TimeSpan), which swaps the offset and nothing else. All return null for an unknown id.
Lookups
timeZones.GetTimeZoneByCoordinate(40.7128, -74.0060); // America/New_York, as of now
timeZones.GetTimeZoneByCoordinate(new Coordinate(40.7128, -74.0060), at);
timeZones.GetTimeZoneByIanaId("America/New_York", at); // ids match case-insensitively; aliases such as US/Eastern work
timeZones.GetTimeZoneByWindowsId("Eastern Standard Time"); // the canonical zone — America/New_York
timeZones.GetTimeZoneByWindowsId("Eastern Standard Time", "CA"); // for a territory — America/Toronto
timeZones.GetTimeZonesByWindowsId("Eastern Standard Time"); // every zone that maps to it
timeZones.GetTimeZones(at); // every zone with a Windows mapping, ordered by IANA id
Every lookup has an as-of-now form and an at form taking a DateTimeOffset instant (its offset is irrelevant beyond identifying the instant), and takes an optional language code for the names. Single lookups return null when the id is unknown or the zone has no Windows mapping; open ocean resolves to the nautical zone (Etc/GMT±N; UTC within 7.5° of the prime meridian). Argument errors throw: a null id, a coordinate outside ±90 / ±180 (NaN and infinity included), or a language code TimeZoneNames does not know.
TimeZone is a record:
IanaId— as looked up, in TZDB's casing; an alias is preserved.CanonicalIanaId/IsAlias—US/Eastern→America/New_York.WindowsId— nevernull; zones without a mapping are excluded.Name— localized and daylight-savings-aware,Eastern Daylight Time; never empty. Use it for display.Abbreviation— the CLDR abbreviation for the current daylight-savings state,EDT;nullwhen CLDR has none, never an IANA id.UtcOffset— aTimeSpan.IsDaylightSavings— from TZDB, normalized so that zones modelled with negative savings (Europe/Dublin, Africa/Casablanca) report summer as daylight savings.At— the instant the snapshot was computed for, in UTC.
Data sources and currency
- TZDB comes from NodaTime;
ITimeZones.TzdbVersionreports the version in use. To pick up a tzdata release before a package update, download the matching.nzdfrom nodatime.org/tzdb and setTimeZonesOptions.DateTimeZoneProviderto aDateTimeZoneCacheoverTzdbDateTimeZoneSource.FromStream(...). - Windows mappings and localized names come from CLDR via TimeZoneConverter and TimeZoneNames. A zone with no Windows mapping (
Antarctica/Trollis the only one today) is excluded from every result. - SQL Server's
AT TIME ZONEand .NET'sTimeZoneInforead the Windows registry, which is updated by Windows Update on its own schedule, so they can disagree with TZDB briefly after a rule change. This library never reads the registry.
Sample output
The first ten rows of GetTimeZones() at 2026-07-15T12:00:00Z, with the default language, from TZDB: 2026c (mapping: 48.2):
| IANA Id | Windows Id | Name | Abbreviation | Offset | DST |
|---|---|---|---|---|---|
| Africa/Abidjan | Greenwich Standard Time | Greenwich Mean Time | GMT | +00:00 | False |
| Africa/Accra | Greenwich Standard Time | Greenwich Mean Time | GMT | +00:00 | False |
| Africa/Addis_Ababa | E. Africa Standard Time | East Africa Time | EAT | +03:00 | False |
| Africa/Algiers | W. Central Africa Standard Time | Central European Standard Time | CET | +01:00 | False |
| Africa/Asmara | E. Africa Standard Time | East Africa Time | EAT | +03:00 | False |
| Africa/Asmera | E. Africa Standard Time | East Africa Time | EAT | +03:00 | False |
| Africa/Bamako | Greenwich Standard Time | Greenwich Mean Time | GMT | +00:00 | False |
| Africa/Bangui | W. Central Africa Standard Time | West Africa Standard Time | WAT | +01:00 | False |
| Africa/Banjul | Greenwich Standard Time | Greenwich Mean Time | GMT | +00:00 | False |
| Africa/Bissau | Greenwich Standard Time | Greenwich Mean Time | GMT | +00:00 | False |
Migrating from 3.x
4.0.0 is a breaking release. Every consumer changes; the table maps each 3.0.1 call to its 4.0.0 equivalent.
| 3.0.1 | 4.0.0 |
|---|---|
value.AtTimeZone(timeZone) (obsolete) |
timeZones.AtTimeZone(value, ianaId) — and the result is now correct across DST |
GetTimeZoneByIanaId(id, at) + value.AtTimeZone(tz.UtcOffsetTs) |
timeZones.AtTimeZone(value, id) |
tz.UtcOffsetTs |
tz.UtcOffset (TimeSpan) |
tz.UtcOffset (string) |
tz.UtcOffset.ToString(@"hh\:mm") with a sign, or NodaTime OffsetPattern |
GetTimeZoneByCoordinate(decimal, decimal) |
cast to double |
GetTimeZones(Instant) |
GetTimeZones(instant.ToDateTimeOffset()) |
DateTimeOffset? at-time argument |
.Value (the parameter is non-nullable) |
services.AddMemoryCache().AddTimeZones() |
services.AddTimeZones() — AddMemoryCache() no longer required (harmless) |
DateTime.AtTimeZone(...) |
removed; use DateTimeOffset or timeZones.AtTimeZone(dateTime, ianaId) |
IEnumerable<TimeZone> results |
IReadOnlyList<TimeZone> |
Abbreviation always a string |
string? — null when CLDR has no abbreviation; use Name for display |
Also: the public extension methods moved from namespace System to Arex388.TimeZones.TimeZonesExtensions, so a using Arex388.TimeZones; is needed where AtTimeZone(TimeSpan) is called, and a consumer's own System.DateTimeOffsetExtensions no longer collides with the library's.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- GeoTimeZone (>= 6.1.0)
- Microsoft.Bcl.TimeProvider (>= 8.0.1)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.2)
- NodaTime (>= 3.3.3)
- TimeZoneConverter (>= 7.2.0)
- TimeZoneNames (>= 7.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.0.0 | 88 | 8/27/2026 |
| 3.0.1 | 248 | 10/19/2025 |
| 3.0.0 | 201 | 6/27/2025 |
| 2.0.8 | 191 | 6/27/2025 |
| 2.0.7 | 203 | 6/27/2025 |
| 2.0.6 | 258 | 2/19/2025 |
| 2.0.5 | 230 | 10/14/2024 |
| 2.0.4 | 246 | 9/13/2024 |
| 2.0.3 | 513 | 2/14/2024 |
| 2.0.2 | 257 | 2/12/2024 |
| 2.0.1 | 275 | 10/9/2023 |
| 2.0.0 | 295 | 9/20/2023 |
| 1.0.24 | 257 | 9/18/2023 |
| 1.0.23 | 391 | 3/31/2023 |
| 1.0.22 | 419 | 3/24/2023 |
| 1.0.21 | 439 | 2/14/2023 |
| 1.0.20 | 513 | 12/28/2022 |
| 1.0.19 | 546 | 10/31/2022 |
| 1.0.18 | 597 | 9/26/2022 |
| 1.0.17 | 615 | 9/7/2022 |