SolTechnology.DateTimeZone
2.0.0
dotnet add package SolTechnology.DateTimeZone --version 2.0.0
NuGet\Install-Package SolTechnology.DateTimeZone -Version 2.0.0
<PackageReference Include="SolTechnology.DateTimeZone" Version="2.0.0" />
<PackageVersion Include="SolTechnology.DateTimeZone" Version="2.0.0" />
<PackageReference Include="SolTechnology.DateTimeZone" />
paket add SolTechnology.DateTimeZone --version 2.0.0
#r "nuget: SolTechnology.DateTimeZone, 2.0.0"
#:package SolTechnology.DateTimeZone@2.0.0
#addin nuget:?package=SolTechnology.DateTimeZone&version=2.0.0
#tool nuget:?package=SolTechnology.DateTimeZone&version=2.0.0
<p align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset="./docs/logo-dark.png"> <img alt="SolTechnology / DateTimeZone" src="./docs/logo.png" width="300"> </picture> </p>
<p align="center"> A point in time, surviving clock changes </p>
<p align="center"> <a href="https://www.nuget.org/packages/SolTechnology.DateTimeZone"><img src="https://img.shields.io/nuget/v/SolTechnology.DateTimeZone?label=NuGet&logo=nuget&color=blue"></a> <a href="https://github.com/Sol-Technology/SolTechnology.DateTimeZone/actions/workflows/build.yml"><img src="https://img.shields.io/github/actions/workflow/status/Sol-Technology/SolTechnology.DateTimeZone/build.yml?label=Build&logo=github"></a> <a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-2F7D7A.svg"></a> </p>
Docs
Why DateTimeZone
DateTimeOffset remembers the offset, not the zone. Add a week to a 10:00 meeting in Warsaw across the last Sunday of October and the result still carries +02:00, an offset Warsaw no longer has; on a Warsaw clock it is 09:00. DateTimeZone is a DateTimeOffset plus a TimeZoneInfo, kept consistent, so the offset is always the one the zone has at that point in time. A date and time with no zone recorded is a separate type, UnzonedDateTime, never a silent UTC.
| DateTimeZone | DateTimeOffset | DateTime | NodaTime | |
|---|---|---|---|---|
| Keeps the point in time | ✔️ | ✔️ | only Kind.Utc |
✔️ |
| Keeps the zone | ✔️ | offset only | ❌ | ✔️ |
Offset stays right after AddDays across a DST change |
✔️ | ❌ | ❌ | ✔️ |
| Clock change (DST): you choose how a skipped or repeated time is read | ✔️ | ❌ | ❌ | ✔️ |
| Date and time without a zone is a separate type | UnzonedDateTime |
❌ | Kind.Unspecified |
LocalDateTime |
| Text keeps the zone | RFC 9557 [Europe/Warsaw] |
❌ | ❌ | own patterns |
Built on DateTimeOffset, TimeZoneInfo, DateOnly, TimeOnly |
✔️ | ✔️ | ✔️ | own types, own tzdb |
| Dependencies | 0, 88 KB | 0 | 0 | 1 package, 524 KB with its own tzdb (3.3.6) |
What it means for your code:
- One type for "when and where" – the zone travels with the value through methods, JSON and logs, so nobody has to remember which
DateTimewas in which zone - Arithmetic you can reason about –
AddHours(24)moves the point in time,AddDays(1)moves the clock; across a DST night they differ and the names say so - No guessing –
Kind.Unspecifiedis rejected, and on the night of a clock change you say whether 02:30 means the first one or the second one, instead of whateverTimeZoneInfofelt like
NodaTime is the complete solution. This one is for code that already lives on BCL types and wants the zone to stay attached, without adopting a second time model.
Features
- Create
using SolTechnology.Time;
var meeting = new DateTimeZone(new DateOnly(2026, 10, 24), new TimeOnly(10, 0), Zone.EuropeWarsaw);
meeting.ToString(); // 2026-10-24T10:00:00+02:00[Europe/Warsaw]
// Zone has a named value for every IANA zone, so a typo does not compile. It holds the ID, not a number,
// so it is safe across versions. A TimeZoneInfo works too; Zones.Resolve("Europe/Warsaw") is for IDs from configuration.
<br/>
- Two kinds of arithmetic
meeting.AddDays(7); // 2026-10-31T10:00:00+01:00[Europe/Warsaw] same clock time, new offset
meeting.AddHours(24); // 2026-10-25T09:00:00+01:00[Europe/Warsaw] 24 hours later, the night had 25
<br/>
- Same point in time, another zone
meeting.ToTimeZone(Zone.AsiaTokyo); // 2026-10-24T17:00:00+09:00[Asia/Tokyo]
meeting == meeting.ToUtc(); // true, equality is by point in time
<br/>
- Clock change (DST): a time the clock skipped, or showed twice
// 29 March, clocks go from 02:00 straight to 03:00: there is no 02:30. Read with the previous offset: 03:30+02:00
new DateTimeZone(new DateOnly(2026, 3, 29), new TimeOnly(2, 30), Zone.EuropeWarsaw);
// 25 October, clocks go back from 03:00 to 02:00: 02:30 happens twice. The new offset means the second one: 02:30+01:00
new DateTimeZone(new DateOnly(2026, 10, 25), new TimeOnly(2, 30), Zone.EuropeWarsaw, ClockChange.NewOffset);
// ClockChange.Throw: an exception instead of a guess
<br/>
- No zone recorded? Say so
var local = UnzonedDateTime.Parse("2026-10-24T10:00:00");
local.InZone(Zone.EuropeWarsaw); // becomes a point in time only with a zone
local.AssumeUtc(); // or when you know it is UTC
<br/>
- Text and JSON
DateTimeZone.Parse("2026-10-24T10:00:00+02:00[Europe/Warsaw]");
JsonSerializer.Serialize(meeting); // "2026-10-24T10:00:00+02:00[Europe/Warsaw]", converters are built in
Extended list: <br>
- Creating a value from wall time,
DateTimeOffset,DateTime,TimeProvider - Equality and ordering by point in time,
SameRepresentationwhen the zone matters - Arithmetic: point in time vs calendar
- Clock change:
ClockChange.PreviousOffset,NewOffset,Throw, Samoa 2011 - UnzonedDateTime for Avro
local-timestamp-*, Icebergtimestamp, ParquetisAdjustedToUTC=false - Zones:
Zone.EuropeWarsaw, IANA and Windows IDs, aliases, fixed offsets - Text format and JSON
- Limits: formats that drop the zone, no tzdata in chiseled images, Windows history
- Upgrading from 1.x
Limits worth knowing before you start
- Zones come from the operating system. Chiseled and distroless images have no tzdata; the exception says so.
- Avro
timestamp-*, ParquetisAdjustedToUTC=trueand Icebergtimestamptzstore only the point in time. The zone does not survive a round trip through them. - 1.x calculated offsets without DST. 2.0 is a rewrite with a different API, do not stay on 1.x.
The rest is in Limits.
License
MIT. Free for everyone, see LICENSE.
Contribution
If you have an idea, hit a zone that behaves differently than you expected, or found a better way to do something, leave an issue or a pull request.
Related Work
– Apache Avro serializer for .NET, from the same author
- soltechnology.dev – the SolTechnology libraries
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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 is compatible. 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. |
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.