How does DateTimeOffset deal with daylight saving time? - c#

I am storing schedules in the database as a day of the week, hour and minute. When the data is read we create a DateTime object for the next occurrence of that day, hour and minute, but I need to modify this to be DST-aware. I am able to modify the database if necessary.
I know that DateTimeOffset stores a UTC date/time and an offset. I also know from this MSDN blog entry that DateTimeOffset should be used to "Work with daylight saving times".
What I'm struggling to understand is exactly how DateTimeOffset "work(s) with daylight saving times". My understanding, little that there is, is that daylight saving times are a political decision and cannot be inferred from purely an offset. How can it be that this structure is DST friendly if it only stores an offset and not a named timezone or country?

DateTimeOffset itself isn't really DST-aware, but TimeZoneInfo is. A DateTimeOffset represents a fixed instant in time - so you get to a DateTimeOffset via something that is time zone aware. In other words, if I asked for a DateTimeOffset now in the UK, I'd end up with something with an offset of +1 hour from UTC. If I asked for a DateTimeOffset for some time in December in the UK, I'd end up with something with an offset of 0 hours from UTC.
If you change your database to include the offset and you create the DateTimeOffset from the user's chosen DateTime (which should be of kind "unspecified") and their time zone, then that should give you the correct offset taking DST into account.
One thing to be aware of though: if I schedule something now for "2 years time" and you determine the offset now, that offset may not be correct in the future - for example, the government could change when DST applies, and obviously that's not going to change what's stored in your database.

Related

What timezone is the time part of the DateTime string along with an offset represents?

I have DateTime string as 2020-01-21T16:17:01.2202038-05:00, I would like to know 16:17:01.2202038 represents the local time or UTC time?
How can I preserve the local time in C#?
The offset is -05:00, which indicates that the date and time given (2020-01-21T16:17:01.2202038) are 5 hours behind UTC. In other words, it is the local time of some time zone that is UTC-5 at that point in time.
This is not necessarily the same as the local time of the computer where you are evaluating the code, nor is the offset guaranteed to be the same for other points in time for the same time zone.
In .NET, you're much better off representing such data with the DateTimeOffset type, rather than the DateTime type.
By the way, this format is defined as part of the ISO 8601 and RFC 3339 specifications.
A DateTime value does not have TZ offset data. It has a flag for “local” or “UTC” (or unspecified). See the DateTime.Kind property. Internally the actual value of the DateTime is an offset from UTC, stored independently of the Kind.
If a (non-0) TZ is displayed when a DateTime is converted to a string, the DateTime represents the local TZ offset and the TZ-local time.
This TZ offset is calculated on-the-fly depending on the actual DateTime value and current computer (really thread) settings. For example, it will change offsets “automatically” depending if the date is in Standard or Daylight time.* Since the calculation is done with limited data (ticks from UTC and a flag) the TZ is not necessarily “honest” across TZ shifts: the absolute time in UTC is correct, but the currently emitted TZ offset can change as it is computed on currently
active settings.
*Historical offsets can be inaccurate if these rules have changed multiple times, as implementations usually only store so much history.

Get eastern time in c# without converting local time

I know there are bunch of solutions on converting timezone to timezone, but what I'd like to know whether we can get eastern time without making conversion from our local time.
Well yes and no, if you represent using GMT always then you don't need to convert until you need to show locality.
Time is always in GMT and services should run under the GMT Timezone because of this reason among others.
Receiving and Storing a time in anything other than GMT / UTC requires conversions.
Please note the Timezone of the server should also be set to GMT for things to work as I indicated.
When you set things up like this it thus becomes much easier to reference and compare times and dates in different calendar date formats also.
This is the reason why DateTime offset made its way into the framework and sql server. if not careful when the Timezone on a server changes so does all stored and loaded dates in local format.
A date comparison is a date comparison. DateTime just wraps a memory structure which is defined in ticks and makes methods to access commonly used parts of the memory e.g. day or year or Time or TimeOfDay etc.
Furthermore conversion is only possible if you know both the source and destination offsets and then the calculation is always as given is given by -1 * (sourceOffset - destOffset)
Where the part in parenthesis represents the time zone difference.
Where the DateTime you want to convert is in UTC and called theDate
DateTime eastern = TimeZoneInfo
.ConvertTimeFromUtc(
theDate,
TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"))
If theDate has a Kind of Local this will throw, only accepting Kind values of Utc or Unspecified. Of course, moving from local to UTC is easy and we could even check and do this conversion when required, but since you say you want this conversion "without making conversion from our local time" I'm assuming you have your times in UTC and hence having the exception would be better as it would warn you that times that should be UTC are being treat as local and a bug could pop up elsewhere.
This will use Eastern Daylight Time when theDate is a time when EDT is in effect, as that is the normal rule for EST. To use EST even when it is the summer you can create your own TimeZoneInfo:
TimeZoneInfo
.ConvertTimeFromUtc(
theDate,
TimeZoneInfo.CreateCustomTimeZone(
"Eastern Standard Time No Daylight Savings",
new TimeSpan(-5, 0, 0),
"Eastern Standard Time No Daylight Savings",
"Eastern Standard Time No Daylight Savings"))

UTC to local time conversion for previously saved datetimes if rules for timezone change

I'm storing a product in db. All dates (sql server datetime) are UTC and along with the dates I store the time zone id for that product. User enters the dates when product is available "from" and "until" in the listing.
So I do something like:
// Convert user's datetime to UTC
var userEnteredDateTime = DateTime.Parse("11/11/2014 9:00:00");
// TimeZoneInfo id will be stored along with the UTC datetime
var tz = TimeZoneInfo.FindSystemTimeZoneById("FLE Standard Time");
// following produces: 9/11/2014 7:00:00 AM (winter time - 1h back)
var utcDateTime = TimeZoneInfo.ConvertTimeToUtc(userEnteredDateTime, tz);
and save the record. Let's assume user did this on the 1st of August, while his time zone offset to UTC is still +03:00, nevertheless the saved date for the future listing has the correct +02:00 value because conversion took into consideration the "winter" time for that period.
Question is what datetime value will I get if I will attempt to convert that product's "from" and "until" date to product's local time zone on 11/11/2014 if, for example, due to some new rules the transition to winter time was abandoned, thus the time zone is still +03:00 instead of +02:00?
// Convert back
var userLocalTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, tz);
will I get 10AM or correct 9AM because OS/.NET patch will handle this?
Thank you!
P.S.: TimeZoneInfo has ToSerializedString() method, if I rather store this value instead of timezone id, will this guarantee that via UTC datetime + serialized timezoneinfo I will always be able to convert to the user's original datetime input?
In the scenario you describe, you would get 10:00 AM. The time zone conversion function would not have any idea that the value was originally entered as 9:00 AM, because you only saved the UTC time of 7:00 AM.
This illustrates one of the cases where the advice "always store UTC" is flawed. When you're working with future events, it doesn't always work. The problem is that governments change their mind about time zones often. Sometimes they give reasonable notice (ex. United States, 2007) but sometimes they don't (ex. Egypt, 2014).
When you made the original conversion from local time to UTC, you intentionally decided to trust that the time zone rules would not change. In other words, you decided that you would assign the event to the universal timeline based solely on the time zone rules as you knew them at that time.
The way to avoid this is simple: Future events should be scheduled in local time. Now, I don't mean "local to your computer", but rather "local to the user", so you will need to know the user's time zone, and you should also store the ID of the time zone somewhere.
You'll also need to decide what you want to do if the event falls into the spring-forward or fall-back transition for daylight saving time. This is especially important for recurrence patterns.
Ultimately though, you'll need to figure out when to run the event. Or in your case, you'll need to decide if the event has passed or not. There are a few different ways you can accomplish this:
Option 1
You can calculate the corresponding UTC value for each local time and keep it in a separate field.
On some cycle (daily, weekly, etc) you can recalculate upcoming UTC values from their local values and your current understanding of the time zone rules. Or, if you apply time zone updates manually, you can choose to recalculate everything at that time.
Option 2
You can store the values as a DateTimeOffset type instead of a DateTime. It will contain the original local time, and the offset that you calculated based on the time zone rules as you knew them at time of entry.
DateTimeOffset values can easily be coerced back to UTC, so they tend to work very well for this. You can read more in DateTime vs DateTimeOffset.
Just like in option 1, you would revisit the values periodically or after time zone data updates, and adjust the offsets to align with the new time zone data.
This is what I usually recommend, especially if you're using a database that has support for DateTimeOffset types, such as SQL Server or RavenDB.
Option 3
You can store the values as a local DateTime.
When querying, you would calculate the current time in the target time zone and compare against that value.
DateTime now = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, targetTZ);
bool passed = now >= eventTime;
The down side of this option is that you may have to make lots queries if you have events in lots of different time zones.
You may also have issues with values close to the fall-back DST transition, so be careful if you use this approach.
I recommend against the idea of serializing the time zone itself. If the time zone has changed, then it has changed. Pretending that it hasn't isn't a good workaround.

Is there any case where DateTimeOffset might not be reliable?

I understand that to refer a single point in time DateTimeOffset is better and more reliable way compared to DateTime as it replaces .Kind property by a more convenient thing that is the Offset to UTC.
Does this solve all the issues regarding to storing a single point in Date-Time or are there still some cases that I should be concerned about?
(If there are can you give me examples where DateTimeOffset can't be reliable?)
Thanks
Given a DateTimeOffset, there is never any confusion about what point in time that represents. So they are always reliable.
But there are some cases where a DateTimeOffset alone is still not sufficient. Here's an example of a common situation:
You are in New York, USA on March 10th 2013.
You find out about an event happening at 1:00 AM local time.
You record it as a DateTimeOffset with the value 2013-03-10T01:00:00-05:00.
Later, you find out that you were given incorrect information, the event actually occurred at 3:00 AM.
So you go to edit, and you change the value to 2013-03-10T03:00:00-05:00.
But this would be incorrect. On that particular day, daylight saving time starts, and so 3:00 AM is only one hour later than 1:00 AM. If you just advance the time, without considering that the offset may have changed, then you am referencing the wrong point in time.
It should have been 2013-03-10T03:00:00-04:00.
To overcome this situation, you must also know that the time was recorded in New York. You knew that in the first step, but then it was thrown out when you recorded it. Somewhere else in your application, you must hold on to this fact. Preferably, you would keep a time zone id, so that you could re-calculate the correct offset.
If using the TimeZoneInfo class in your application, then you would want to track the value of the .Id property, along with your DateTimeOffset. For New York, the time zone id would be "Eastern Standard Time". This is a bit confusing, because this same value is used regardless of whether DST is in effect or not. (There is no Windows time zone with an Id of "Eastern Daylight Time"). Also, there is no built-in class or struct that will pair a TimeZoneInfo with a DateTimeOffset. You have to do it yourself.
If you are using Noda Time (which I highly recommend). Then you can take advantage of the IANA time zone id of "America/New_York", and the ZonedDateTime object - which is designed for this exact situation.
You should also refer to DateTime vs DateTimeOffset. You should find the analogy there quite useful.
There are also some cases where DateTimeOffset is not appropriate. Maybe this one is obvious, but it's still worth mentioning.
When you are not refering to a single moment in time, but a relative point on the calendar.
This happens more often than you would think. For example:
In the United States, this year daylight saving time began on March 10th 2013 at 2:00 AM.
But it didn't happen at at the exact same moment. Each time zone has their own local 2:00 AM, so there are actually several different transition points on the instantaneous timeline.
(Aside, but worth mentioning, in Europe, DST ("Summer Time") happens all at once. The transition is based on the same UTC moment for Western, Central, and Eastern European time.)
There are other real-world examples where the same point on the calendar is not the same point in time, yet people tend to think of them as if they were.
Day boundaries ("today", "yesterday", "tomorrow")
Other whole named days ("this Wednesday", "last Friday")
Television Shows ("7PM Tuesday nights")
Telephone Calling Plans ("Free nights and weekends")
Countless others...
In Noda Time, you would use a LocalDateTime for these scenarios. Without Noda Time, you would use a DateTime with .Kind == Unspecified.

Storing date/times as UTC in database

I am storing date/times in the database as UTC and computing them inside my application back to local time based on the specific timezone. Say for example I have the following date/time:
01/04/2010 00:00
Say it is for a country e.g. UK which observes DST (Daylight Savings Time) and at this particular time we are in daylight savings. When I convert this date to UTC and store it in the database it is actually stored as:
31/03/2010 23:00
As the date would be adjusted -1 hours for DST. This works fine when your observing DST at time of submission. However, what happens when the clock is adjusted back? When I pull that date from the database and convert it to local time that particular datetime would be seen as 31/03/2010 23:00 when in reality it was processed as 01/04/2010 00:00.
Correct me if I am wrong but isn't this a bit of a flaw when storing times as UTC?
Example of Timezone conversion
Basically what I am doing is storing the date/times of when information is being submitted to my system in order to allow users to do a range report. Here is how I am storing the date/times:
public DateTime LocalDateTime(string timeZoneId)
{
var tzi = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
return TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi).ToUniversalTime().ToLocalTime();
}
Storing as UTC:
var localDateTime = LocalDateTime("AUS Eastern Standard Time");
WriteToDB(localDateTime.ToUniversalTime());
You don't adjust the date for DST changes based on whether you're currently observing them - you adjust it based on whether DST is observed at the instant you're describing. So in the case of January, you wouldn't apply the adjustment.
There is a problem, however - some local times are ambiguous. For example, 1:30am on October 31st 2010 in the UK can either represent UTC 01:30 or UTC 02:30, because the clocks go back from 2am to 1am. You can get from any instant represented in UTC to the local time which would be displayed at that instant, but the operation isn't reversible.
Likewise it's very possible for you to have a local time which never occurs - 1:30am on March 28th 2010 didn't happen in the UK, for example - because at 1am the clocks jumped forward to 2am.
The long and the short of it is that if you're trying to represent an instant in time, you can use UTC and get an unambiguous representation. If you're trying to represent a time in a particular time zone, you'll need the time zone itself (e.g. Europe/London) and either the UTC representation of the instant or the local date and time with the offset at that particular time (to disambiguate around DST transitions). Another alternative is to only store UTC and the offset from it; that allows you to tell the local time at that instant, but it means you can't predict what the local time would be a minute later, as you don't really know the time zone. (This is what DateTimeOffset stores, basically.)
We're hoping to make this reasonably easy to handle in Noda Time, but you'll still need to be aware of it as a possibility.
EDIT:
The code you've shown is incorrect. Here's why. I've changed the structure of the code to make it easier to see, but you'll see it's performing the same calls.
var tzi = TimeZoneInfo.FindSystemTimeZoneById("AUS Eastern Standard Time");
var aussieTime = TimeZoneInfo.ConvertTimeFromUtc(DateTime.UtcNow, tzi);
var serverLocalTime = aussieTime.ToLocalTime();
var utcTime = serverLocalTime.ToUniversalTime();
So, let's think about right now - which is 13:38 in my local time (UTC+1, in London), 12:38 UTC, 22:39 in Sydney.
Your code will give:
aussieTime = 22:39 (correct)
serverLocalTime = 23:39 (*not* correct)
utcTime = 22:39 (*not* correct)
You should not call ToLocalTime on the result of TimeZoneInfo.ConvertTimeFromUtc - it will assume that it's being called on a UTC DateTime (unless it's actually got a kind of DateTimeKind.Local, which it won't in this case).
So if you're accurately saving 22:39 in this case, you aren't accurately saving the current time in UTC.
It's good that you are attempting to store the dates and times as UTC. It is generally best and easiest to think of UTC as the actual date and time and local times are just pseudonyms for that. And UTC is absolutely critical if you need to do any math on the date/time values to get timespans. I generally manipulate dates internally as UTC, and only convert to local time when displaying the value to the user (if it's necessary).
The bug that you are experiencing is that you are incorrectly assigning the local time zone to the date/time values. In January in the UK it is incorrect to interpret a local time as being in a Summertime time zone. You should use the time zone that was in effect at the time and location that the time value represents.
Translating the time back for display depends entirely on the requirements of the system. You could either display the times as the user's local time or as the source time for the data. But either way, Daylight Saving/Summertime adjustments should be applied appropriately for the target time zone and time.
You could work around this by also storing the particular offset used when converting to UTC. In your example, you'd store the date as something like
31/12/2009 23:00 +0100
When displaying this to the user, you can use that same offset to convert, or their current local offset, as you choose.
This approach also comes with its own problems. Time is a messy thing.
The TimeZoneInfo.ConvertTimeFromUtc() method will solve your problem:
using System;
class Program {
static void Main(string[] args) {
DateTime dt1 = new DateTime(2009, 12, 31, 23, 0, 0, DateTimeKind.Utc);
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time");
Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(dt1, tz));
DateTime dt2 = new DateTime(2010, 4, 1, 23, 0, 0, DateTimeKind.Utc);
Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(dt2, tz));
Console.ReadLine();
}
}
Output:
12/31/2009 11:00:00 PM
4/2/2010 12:00:00 AM
You'll need .NET 3.5 or better and run on an operating system that keeps historical daylight saving time changes (Vista, Win7 or Win2008).
Correct me if I am wrong but isn't
this a bit of a flaw when storing
times as UTC?
Yes it is. Also, days of the adjustment will have either 23 or 25 hours so the idiom of the prior day at the same time being local time - 24 hours is wrong 2 days a year.
The fix is picking one standard and sticking with it. Storing dates as UTC and displaying as local is pretty standard. Just don't use a shortcut of doing calculations local (+- somthing) = new time and you are OK.
This is a huge flaw but it isn't a flaw of storing times in UTC (because that is the only reasonable thing to do -- storing local times is always a disaster). This is a flaw is the concept of daylight savings time.
The real problem is that the time zone information changes. The DST rules are dynamic and historic. They time when DST starting in USA in 2010 is not the same when it started in 2000. Until recently Windows did not even contain this historic data, so it was essentially impossible to do things correctly. You had to use the tz database to get it right. Now I just googled it and it appears that .NET 3.5 and Vista (I assume Windows 2008 too) has done some improvement and the System.TimeZoneInfo actually handles historic data. Take a look at this.
But basically DST must go.

Categories