It's seems AFTime is one of my favorite blog topics. I suppose you could consider this blog a part of a series ... that was started 5 years ago! Here were my earlier posts:
My first blog - It's About Time! (a discussion about sub-second times)
At UC SF 2017, I met lots of people and engaged in interesting discussions. There were 2 notable questions about time. One from a student in a lab, and the other from a work colleague. To reword their questions:
- When I create an AFTime, sometimes it's Utc and sometimes it Local. How can I know which is which ahead of time?
- I am trying to write a custom method to parse a relative time string. Do you have any tips on the best way to do this?
On the surface, these 2 questions seem to only be related to AFTime in general. But actually the same answer applies to both as it depends on the same solution: the AFTime constructor. I thought that the first question has been answered a dozen times before. Apparently it must be answered again.
There is a general rule of thumb that goes:
DateTimes are UTC; Strings are Local
Overall it's a decent rule of thumb, but not exactly correct. There are some finer details and exceptions to understand. Before we jump right to the straight-forward solutions, let's take a brief sidetrack to have a better understanding of the objects involved.
Differences between AFTime and DateTime
If you ask 10 different developers to describe the difference between AFTime and DateTime, you would get 10 different replies. For the topic being discussed, there are 3 relevant differences that I choose to discuss.
The first pertains to DateTime's Kind property and its notion of Utc, Local, and Unspecified. While AFTime doesn't have a Kind property, it is quite aware of UtcTime and LocalTime but most importantly does not support any notion of Unspecified. What this means is that if you pass anything to AFTime that falls into the Unspecified category, then AFTime must decide what to do. One possible decision could be to reject it by throwing an exception - but it doesn't do that. Instead AFTime makes an assumption of how unspecified time zones should be treated, yet this treatment varies depending upon the type of object being passed to AFTime.
In short, DateTime objects passed to AFTime will treat Unspecified as Utc, and String objects lacking time zone info will be treated as Local.
The second key difference between AFTime and DateTime is the MinValue for each. While they both share the same MaxValue, AFTime.MinValue is 1/1/1970 whereas DateTime.MinValue is 1/1/0001 (in the Gregorian calendar). This coupled with the above means there is some validation performed on the time that is input to AFTime. That is to say at the very least the input time may be clamped to 1/1/1970 on the low end.
The 3rd critical difference is that while DateTime.Parse allows for a typical time string to be passed in, AFTime supports PI relative time formats such as "*-8h".
Constructors always using Utc
There are 2 AFTime constructors that will have input that is always UTC-based.
The Double overload will accept seconds for a UTC-based DateTime. The Int64 will accept ticks for a UTC-based DateTime. Using either of these 2 constructors always expect the input value to be UTC-based. It is YOUR responsibility to make sure the Double or Int64 you pass in is also UTC-based. If you have code that isn't working correctly, look for a bug in YOUR application.
This code has a hard-to-find bug:
DateTime dTime = DateTime.Now; AFTime aTime = new AFTime(dTime.Ticks);
Because dTime is Local but AFTime was expecting something UTC-based. The bug is on your end and the easy fix is up to you:
AFTime aTime = new AFTime(dTime.ToUniversalTime().Ticks);
DateTime does allow Local
The short rule of thumb "DateTimes are UTC" isn't totally correct in that you may pass a DateTime input with a Local DateTimeKind.
To clarify, a DateTime input to AFTime is not always treated as Utc. If a DateTime with Kind of Local is passed in, then that Local time is honored as one would expect. The only fuzzy area where there is a maybe is if a DateTime with Unspecified is passed in. For those cases, the Kind is assumed to be Utc. In other words, the Kind is simply changed to Utc and not converted to Utc. You may think of this pseudo-code as happening when you pass a DateTime to AFTime:
// Unspecified is changed to Utc // Local is converted to Utc if (time.Kind == DateTimeKind.Unspecified) time = DateTime.SpecifyKind(time, DateTimeKind.Utc); else if (time.Kind == DateTimeKind.Local) time = time.ToUniversalTime(); // Actually more complex than this because dates before 1900 throw an exception. if (time < AFTime.MinValue.UtcTime) time = AFTime.MinValue; // Internally time is used because its UTC-based and clamped to 1/1/1970
That's not the actual code that runs in the AFTime constructor, but it's appropriate example to understand some of its inner workings. The internal value is stored in a DateTime object with Kind of Utc and clamped on the low end to 1/1/1970 (AFTime.MinValue).
Bottom line: if the DateTime specifies Utc or Local, it will be honored. If the Kind is Unspecified, it is considered to be Utc.
String does allow UTC, time zones, and time zone offsets
A String input to AFTime will be treated as Local unless there is time zone specified or time zone offset information contained within the string. If you include it, it will be honored. If you omit it, it's treated as Local. Just as you should expect.
So these time strings would be Local times as they lack any time zone offset:
Whereas these time strings would be set to UTC-based times:
It's nice to know that the last 2 time strings would easily be parsed by DateTime.Parse. Whether the returned DateTime is Utc or Local, the same AFTime logic in the previous section applies. This answers the first question I received at UC: When I create an AFTime, sometimes it's Utc and sometimes it Local. How can I know which is which ahead of time?
Relative Time Format strings
See the Remarks for AFTime Constructor (String)
Notice that our string examples looked like typical date and time strings. A critical difference that AFTime has over DateTime is its support of Relative Time Format strings. Relative Time Format strings are Local based. This should not be surprising given that concepts as Today and Yesterday are Local in context. My Today in Houston is very different from someone else's Today in Europe.
For my colleague, Thyagarajan Ramachandran also known as Thyag, who said "I am trying to write a custom method to parse a relative time string. Do you have any tips on the best way to do this?" My best advice was: Don't. There is no need to reinvent-the-wheel to parse a relative time format string yourself. The AFTime Constructor (String) already does this. Save yourself lots of coding and debugging time, and instead rely on AFTime. To think of it another way, if your goal is to code a method that parses a relative time string in a manner that is 100% compatible with AFTime, why not just let AFTime perform all the heavy lifting because in the simplest of wisdom: whatever AFTime does is 100% compatible with itself!
I mentioned to Thyag the fact that AFTime supports multiple relative time formats such as "t+5h-30m" to have my local Today at 4:30 AM. This initially surprised him and he admitted his method did not take this into account. That news immediately means his custom method was not fully compatible with AFTime's parsing capabilities. Sometimes reinventing-the-wheel is not such an easy task!
To make your head spin even more, at the bottom of the AFTime Constructor (String) help is a quite curious example of "Sat, 01 Nov 2008 19:35:00 GMT + 2y+5d-12h+30.55s" to fully demonstrate relative time formats. Note only can you have multiple relative time intervals, it can accept floating point values (see 30.55s).
There are also some interesting corner cases with AFTime and input time strings that Thyagarajan Ramachandran (Thyag) discovered.
For example, new AFTime() returns AFTime.MinValue.
Whereas, new AFTime(null) or new AFTime(timestring) where timestring is null or empty returns AFTime.Now.
A time string of "-12" is equivalent to "*-12h". That is to say if the initial time portion is omitted, it is assumed to be "*". Likewise if time units are omitted, it is assumed to be "h".
There's also another assumption for something omitted though it's not apparent in the previous examples. If there is no number specified, it is assumed to be zero. This leads to the quite curious but very valid instances of:
new AFTime("-") is same as "*-0h"
new AFTime("+") is same as "*+0h"
Again, both instances are quite legal and would return AFTime.Now.
I would say that sufficiently answers the 2nd question I heard at UC SF 2017. If you have anymore regarding AFTime, drop me a line.