So I started out reading the documentation for .NET date and time handling and I felt I needed a drink. If this is what .NET developers have to deal with, then no wonder so many of them have issues.
It starts with the DateTime
structure. It’s purpose is to represent a Date and Time; however this structure possesses both the timestamp and the fact that it’s either a UTC time (i.e. no TZ information whatsoever), or a local time.
Obtaining a value for ‘now’ involves using the static properties DateTime.Now
or DateTime.UtcNow
. Once you’ve instantiated a DateTime
value, you can’t actually change it, so for example doing DateTime.Now.ToUniversalTime()
won’t actually change the structure, it returns a new instance of DateTime
, which is converted based on the system’s current timezone settings, which means that you can actually get an incorrect value when the time straddles a daylight savings change.
There is an entire Microsoft document on best practices for dealing with DateTime
values. It’s a bit tricky. Firstly, try to only deal with dates and times in UTC
where possible. There is a major gotcha in the XmlSerializer
code in the .NET 1.0/1.1 framework where it always treats the DateTime
as localtime, even if you’ve passed in a UtcTime. This seems to have been fixed in later revisions of .NET; so hopefully you won’t encounter it.
Obtaining a DateTime object from a String is a matter of using the DateTime.parse()
, you just have to ensure that the parse-string format matches the input string value. The default parse method uses the thread’s current Culture, which means you have to pay attention to things like the pesky mm/dd/yyyy vs dd/mm/yyyy conventions when you’re parsing. If you want to specify the formatting that you’re trying to parse in a manual fashion, you need to use the parse(String stringToParse, IFormatProvider provider)
style, which allows you to use another Culture’s parsing format, or define your own.
If you’re planning on performing operations on a DateTime item, such as adding a few hours, days, etc to the original value bear in mind that in order to be safe, you should perform the operation with the timezone set to UTC (which doesn’t have any DST adjustments), and then swap it back to local time. This double trip is intended to avoid errors around ‘spring forward’ and ‘fall back’ times. Even if you don’t think you’re going to encounter those times it’s better safe than sorry.
Because a DateTime
only stores a time in either ‘local’ or UTC, when you choose to persist this information you are best off storing the information in UTC and keeping an adjacent element for the timezone, should you actually choose to store it. The simplest way to store it is as the result of the Ticks
property. This value is an integer, and is in ten-millionths of a second since 12:00 midnight, January 1, 0001 A.D. (C.E.) in the Gregorian calendar.
The DateTime structure has the ability to extract the Year, Month, Day, Hour, Minute, Second and Milliseconds from the value. It allows the extraction of the ‘Ticks’ value, but this is an absolute value, not an extraction. All these elements are in terms of the Gregorian calendar. It doesn’t matter if your current culture does not use the Gregorian calendar, it always returns the data in terms of the Gregorian calendar. I don’t know the reason for this choice, considering that it understands the current timezone; but it’s most likely due to the culture being a property of the current thread rather than a more ‘systemmy’ property.
If you want to extract based on a different calendar, such as the Persian calendar, you need to extract them via an instance of the calendar. The Persian Calendar is easily instanced from System.Globalization.PersianCalendar, but if you need the calendar for a specific region, you should instantiate a CultureInfo using new System.Globalization.CultureInfo(String CultureCode, bool userOverride)
, and then extract it’s Calendar property. In this case I can do:
var date = DateTime.utcNow; CultureInfo cinfo = new System.Globalization.CultureInfo("EN-ie", false); // get the English-speaking, Ireland culture int year = cinfo.Calendar.GetYear(date); // This year is based on the calendar from Ireland (Gregorian), but you get the idea as to how to use it for other calendars.
The Calendar class deals with the breaking of a Date & Time into pieces. Those pieces are defined in terms of the calendar – this can be confusing if you’re only used to a single calendar, as many people are. I live in a region that uses the Gregorian calendar exclusively, so I deal with dates and times in those terms. The Gregorian calendar was formalized by Pope Gregory in 1582 (by that calendar’s reckoning), which was, in itself a correction to the Julian calendar. You can, in fact, get dates and times in terms of the Julian calendar, which was off because it insisted on a leap year every 4 years, with no exceptions – bear in mind that the Gregorian calendar has two exceptions to the rule – no leap years on years divisible by 100, but it does have a leap year if it’s also divisible by 400 – so, for example 1900 was not a leap year, but 2000 was a leap year, while 2100 will not be a leap year.
So with the .NET environment we have the DateTime, which represents a point in time, we then have the Calendar, which is a representation of the DateTime in terms of a particular calendar i.e it’s representation in terms of the year, month, day, day of week. There can me more delineations of the date & time – it’s up to the calendar to define them.
There are many ways to make an instance of a DateTime, possibly the simplest of which is to use the Now
static property, but you can also construct them explicitly, using a variety of constructors, The simplest is the DateTime(ticks)
, which makes one in terms of the number of ticks since 0-time. But you can construct them made up of just the year, month and day; combined with an hour, minute and second; or even subsequently combined with a millisecond value. These can be specified as ‘local’ or UTC, and can also be specified in terms of a calendar. Once you’ve constructed the DateTime
instance, it will no longer be represented in terms of that calendar, but will be represented in terms of the Gregorian calendar; e.g.
var cal = new System.Globalization.JapaneseCalendar(); DateTime aDateTime = new DateTime(2014, 1, 1, cal); System.Diagnostics.Debug.Assert(aDateTime.Year == 2014);
This assertion will trigger, because it’s no longer in terms of the Japanese Calendar.
Parsing of DateTime values can be done using DateTime.parse()
, which takes a string, and turns it into a DateTime. it uses the current Culture; unless it can be parsed as ISO 8601 – so when I’m here in little old ireland, if I put in a string of 1/2/2015, it tells me February 1, 2015, but if I’m in the US, it represents January 2, 2015. So you have to be careful when parsing DateTime values. This is where the IFormatProvider
parameter comes in – this interface is generally not manually created by the user, but is, instead extracted from an instance of a culture e.g.
IFormatProvider if_us = new System.Globalization.CultureInfo("EN-us", false); IFormatProvider if_ie = new System.Globalization.CultureInfo("EN-ie", false); DateTime ieDate = DateTime.Parse("1/2/2015", if_ie); DateTime usDate = DateTime.Parse("1/2/2015", if_us); System.Diagnostics.Debug.Assert(ieDate.Year == usDate.Year); System.Diagnostics.Debug.Assert(ieDate.Month != usDate.Month); System.Diagnostics.Debug.Assert(ieDate.Day != usDate.Day); System.Diagnostics.Debug.Assert(ieDate.Month == usDate.Day); System.Diagnostics.Debug.Assert(ieDate.Day == usDate.Month);
None of these assertions trip, because we know/understand how the string is parsed by these cultures.
The next thing you generally need to do is deal with displaying times in different time zones. You already have a DateTime
instance, which is either local, or UTC, and you want to turn it into a local time. So what you need is a TimeZoneInfo, and then going back to a DateTime that displays in that format:
TimeZoneInfo irelandtz = TimeZoneInfo.FindSystemTimeZoneById("GMT Standard Time"); TimeZoneInfo newyorktz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time"); var now = DateTime.UtcNow; DateTime irelandnow = TimeZoneInfo.ConvertTimeFromUtc(now, irelandtz); DateTime newyorknow = TimeZoneInfo.ConvertTimeFromUtc(now, newyorktz); Console.WriteLine(" {0} {1}", irelandnow, irelandtz.IsDaylightSavingTime(irelandnow) ? irelandtz.DaylightName : irelandtz.StandardName); Console.WriteLine(" {0} {1}", newyorknow, newyorktz.IsDaylightSavingTime(newyorknow) ? newyorktz.DaylightName : newyorktz.StandardName);
Now this code is awful, you are not actually picking the timezone by location – it’s by a name of timezone, rather than the location that specifies the timezone – e.g. Europe/Dublin and America/NewYork. There is a much better alternative for dealing with DateTime values, and that’s to use the NodaTime, which gives a little bit better an experience:
Instant now = SystemClock.Instance.Now; DateTimeZone dublin = DateTimeZoneProviders.Tzdb["Europe/Dublin"]; DateTimeZone newyork = DateTimeZoneProviders.Tzdb["America/New_York"]; Console.WriteLine(new ZonedDateTime(now, dublin).ToString()); //2015-02-07T20:48:20 Europe/Dublin (+00) Console.WriteLine(new ZonedDateTime(now, newyork).ToString()); //2015-02-07T15:48:20 America/New_York (-05)