Do you remember the “falsehoods programmers believe about X” meme that became popular among software blogs a few years ago? The first one was about names, but several others soon followed, covering topics such as addresses, geography, and online shopping.
My favorite was the one about time. I hadn’t thought deeply about time and its intricacies up until that point, and I was intrigued by how a fundamental domain could be such a fertile ground for misunderstandings.
Now even though I like the post, I have a problem with it: it lists wrong assumptions, and then it basically stops there. The reader is likely to leave the article wondering
- Why are these assumptions falsehoods?
- How likely is it that I’ll get in trouble due to one of these assumptions?
- What’s the proper way of dealing with these issues?
The article is interesting food for thought, but I think it’d make sense to provide more actionable information.
That’s what today’s post is about. I’m going to show you four common mistakes C#/.NET developers make when dealing with time. And that’s not all. I’ll also show what you should do to avoid them and make your code safer and easier to reason about.
C# Datetime Mistake 1: Naively Calculating Durations
Consider the code below:
public void StartMatch() { DateTime start = DateTime.Now; match.StartTime = start; } public void EndMatch() { DateTime end = DateTime.Now; match.EndTime = end; TimeSpan duration = match.EndTime - match.StartTime; Console.WriteLine("Duration of the match: {0}", duration); }
Will this code work? It depends on where and when it’s going to run.
When you use DateTime.Now, the DateTime you get represents the current date and time local to your machine (i.e., it has the Kind property set to Local).
If you live in an area that observes DST (Daylight Saving Time), you know there’s one day in the year when all clocks must be moved forward a certain amount of time (generally one hour, but there are places that adjust by other offsets).
Of course, there’s also the day when the opposite happens.
Now picture this: today is March 12th, 2017, and you live in New York City.
You start using the program above. The StartMatch()
method runs at exactly 01:00 AM. One hour and 15 minutes later, the EndMatch()
method runs. The calculation is performed, and the following text is shown:
Duration of the match: 00:02:15
I bet you’ve correctly guessed what just happened here: when clocks were about to hit 2 AM, DST just kicked in and moved them straight to 3 AM. Then EndMatch
got back the current time, effectively adding a whole hour to the calculation. If the same had happened at the end of DST, the result would’ve been just 15 minutes!
Sure, the code above is just a toy example, but what if it were a payroll application? Would you like to pay an employee the wrong amount?
What to Do?
When calculating the duration of human activities, use UTC for the start and end dates. That way, you’ll be able to unambiguously point to an instant in time. Instead of using the Now property on DateTime, use UtcNow to retrieve the date time already in UTC to perform the calculations:
DateTime start = DateTime.UtcNow; // things happen DateTime end = DateTime.UtcNow; ImeSpan duration = end - start;
What if the DateTime objects you already have are set to Local? In that case, you should use the ToUniversalTime() method to convert them to UTC:
var start = DateTime.Now; // local time var end = DateTime.Now; // local time var duration = end.ToUniversalTime() - start.ToUniversalTime(); // converting to UTC
A Little Warning About ToUniversalTime()
The usage of ToUniversalTime() and its sibling, ToLocalTime() can be a little tricky. The problem is that these methods make assumptions about what you want based on the value of the Kind property of your date, and that can cause unexpected results.
When calling ToUniversalTime(), one of the following things will happen:
- If Kind is set to UTC, then the same value is returned.
- On the other hand, if it’s set to Local, the corresponding value in UTC is returned.
- Finally, if Kind is set to Unspecified, then it’s assumed the datetime is meant to be local, and the corresponding UTC datetime is returned.
The problem we have here is that local times don’t roundtrip. They’re local as long as they don’t leave the context of your machine. If you save a local datetime to a database and then retrieve it back, the information that’s supposed to be local is lost: now it’s unspecified.
So, the following scenario can happen:
- You retrieve the current date and time using
DateTime.UtcNow
. - You save it to the database.
- Another part of the code retrieves this value and, unaware that it’s supposed to already be in UTC, calls
ToUniversalTime() on it
. - Since the datetime is unspecified, the method will treat it as Local and perform an unnecessary conversion, generating a wrong value.
How do you prevent this?
It’s a recommended practice to use UTC to record the time when an event happened. My suggestion here is to follow this advice and also to make it explicit that you’re doing so.
Append the “UTC” suffix to every database column and class property that holds a UTC datetime. Instead of Created, change it to CreatedUTC and so on. It’s not as pretty, but it’s definitely more clear.
Datetime Mistake 2: Not Using UTC When It Should Be Used (and Vice Versa)
We could define this as a universal rule: use UTC to record the time when events happened. When logging, auditing, and recording all types of timestamps in your application, UTC is the way to go.
So, use UTC everywhere! …Right?
Nope, not so fast.
Let’s say you need to be able to reconstruct the local datetime to the user’s perspective of when something happened, and the only information you have is a timestamp in UTC. That’s a piece of bad luck.
In cases like this, it’d make more sense to either (a) store the datetime in UTC along with the user’s time zone or (b) use the DateTimeOffset type, which will record the local date along with the UTC offset, enabling you to reconstruct the UTC date from it when you need it.
Another common use case where UTC is not the right solution is scheduling future local events.
You wouldn’t want to wake up one hour later or earlier in the days of DST transitions, right?
That’s exactly what would happen if you’d set your alarm clock by UTC.
Datetime Mistake 3: Not Validating User Input
Let’s say you’ve created a simple Windows desktop app that lets users set reminders for themselves. The user enters the date and time at which they want to receive the reminder, clicks a button, and that’s it.
Everything seems to be working fine until a user from Brazil emails you, complaining the reminder she set for October 15th at 12:15 AM didn’t work. What happened?
DST Strikes Back
The villain here is good old Daylight Saving Time again. In 2017, DST in Brazil started at midnight on October 15th. (Remember that Brazil is in the southern hemisphere.) So, the date-time combination the user supplied simply didn’t exist in her time zone!
Of course, the opposite problem is also possible. When DST ends and clocks turn backward by one hour, this generates ambiguous times.
What Is the Remedy?
How do you deal with those issues as a C# developer? The TimeZoneInfo
class has got you covered. It not only represents a time zone but it also provides methods to check for a datetime validity:
TimeZoneInfo tz = TimeZoneInfo.Local; // getting the current system timezone DateTime dateTime = GetDateTimeFromUserInput(); // or another external untrusted source if (tz.IsAmbiguousTime(dateTime)) { // do something } if (tz.IsInvalidTime(dateTime)) { // do something } // seems good to go!
What should you do then? What should replace the “do something” comments in the snippets above?
You could show the user a message saying the input date is invalid. Or you could preemptively choose another date for the user.
Let’s talk about invalid times first. Your options: move forward or backward.
It’s somewhat of an arbitrary decision, so which one should you pick?
For instance, the Google Calendar app on Android chooses the former. And it makes sense when you think about it. That’s exactly what your clocks already did due to DST. Why shouldn’t you do the same?
And what about ambiguous times?
You also have two options: choose between the first and second occurrences. Then again, it’s somewhat arbitrary, but my advice is to pick the first one. Since you have to choose one, why not make things simpler?
Datetime Mistake 4: Mistaking an Offset for a Time Zone
Consider the following timestamp: 1995-07-14T13:05:00.0000000-03:00.
When asked what the -03:00 at the end is called, many developers answer, “a time zone.”
Here’s the thing.
They probably correctly assume that the number represents the offset from UTC.? Also, they’d probably see that you can get the corresponding time in UTC from the offset. (Many developers fail to understand that in a string like this, the offset is already applied: to get the UTC time, you should invert the offset sign. Only then should you add it to the time.)
The mistake is in thinking that the offset is all there is to a time zone. It’s not.? A time zone is a geographical area, and it consists of many pieces of information, such as
- One or more offsets. (DST is a thing, after all.)
- The dates when DST transitions happen. (These can and do change whenever governments feel like it.)
- The amount of time applied when transitions happened. (It’s not one hour everywhere.)
- The historical records of changes to the above rules.
In short: don’t try to guess a time zone by the offset. You’ll be wrong most of the time.
Bonus Mistake: Using DateTime.Now / Not Caring About The Correct ‘Kind’
When facing the problems caused by incorrect handling of date and time issues, many developers will say that the only correct solution is to use UTC for everything.
However, as you’ve learned by reading this post, UTC isn’t always the answer. Employing UTC when you shouldn’t is as big of a problem as not using it when you should.
Similarly, many developers think that using the DateTimeOffset type is the solution for all date/time issues they might have. Sure, this type is definitely useful and it does help prevent many of the most common date/time problems. By all means, use DateTimeOffset whenever it makes sense to.
However, this is no one-size-fits-all solution, either. Often, you might find yourself stuck with the DateTime type. Think about a scenario where your team or organization consumes a library that someone else made and maintains. I’m sure you can think of other examples besides.
Anyway, the gist of it is: yes, DateTimeOffset is great, but sometimes you’re just stuck with using the ‘less than ideal type.’
The best we can do during such situations is to use techniques and best practices at our disposal so we can mitigate the existing risks. In the case of the DateTime type, one of the biggest potential issues have to do with the use of the ‘DateTime.Now’ property.
The Issue
It’d be out of the scope for this post to give a detailed explanation of this property especially considering we have a whole post on the subject.
The gist of it is this, though: DateTime.Now doesn’t really encourage you to think deeply about what kind of temporal information you’re trying to express.
You end up with a DateTime object that has its ‘Kind’ property set to ‘Local.’ Most of the time it?d make more sense to have it set to ‘UTC’ or even ‘Unspecified.’
The compiler will gladly allow you to take this ‘Local’ datetime and combine it to other values configured with different kinds, even though it make more sense for the code to fail compiling.
DateTime.Now is also not unit-testing friendly. If some portion of your code includes a call to DateTime.Now, it means that it depends on an external data source it doesn’t control.
What To Do Instead?
Instead of using DateTime.Now, consider using one of the alternatives, such as DateTime.UtcNow or DateTimeOffset.Now. For testability, introduce an interface that exposes a method called ‘Now()’, ‘GetCurrentDate()’ or something similar that returns a DateTime object, representing the current date and time.
For testing, implement the interface returning always a fixed value or other specified value. For production, you should also implement the interface, but in such a way that it returns the system date and time.
It’s About Time…You Learn About Time!
This list is by no means exhaustive. I only wanted to give you a quick start in the fascinating and somewhat bizarre world of datetime issues. There are plenty of valuable resources out there for you to learn from, such as the time zone tag on Stack Overflow or blogs such as Jon Skeet’s and Matt Johnson’s,?who are authors of the popular NodaTime library.
And of course, always use the tools at your disposal. For instance, SubMain’s CodeIt.Right has a rule to force you to specify an IFormatProvider in situations where it’s optional, which can save you from nasty bugs when parsing dates.
Learn more how CodeIt.Right can help you automate code reviews and improve the quality of your code.
12 Comments. Leave new
[…] by /u/brunellus28 [link] […]
Haha, in Turkey the government decided to stay in summer time whole year for the last 3 years. For the first 2 winter I had two Windows 10 Pc’s, both updated all the time, showing different clocks. Whatever I did in the settings it didn’t fixed until I install a fresh Windows. This was just Windows, don’t ask about all the funny things happening in apps and websites.
This kind of things really happening in real life. Next year the government is rolling it back to the classical system. It will be more fun.
Great analysis! Thanks.
[…] 4 Common C# Datetime Mistakes and How to Avoid Them […]
Yo can just use DateTimeOffset for moments in time and avoid the hassle of conversion (ToUniversalTime).
I agree with Gebb. Most of the DateTime mistakes this article helps avoid are assuaged by using DateTimeOffset.
real world samples using DateTimeOffset ?
The recommendation for #1 should be using `StopWatch| I believe
[…] Four common mistakes C#/.NET developers make when dealing with time and what should do to avoid them and make your code safer and easier to reason about can be found?here. […]
Thanks for your information, one question i have is that i have invalid entries in MS sql using Datetime.Now, is it possible that its server time issue from the hosting provider?
Sorry i meant Dateime.UtcNow
Shouldn’t duration be 02:15:00 (2 hours 15 minutes) rather than 00:02:15 (2 minutes, 15 seconds) in example 1
I got the snip example I needed anyway. Thanks