So what's wrong with the DateTime structure?

Original author: Jon Skeet
  • Transfer
Notes:
1. In the previous article,time zone ” I translated as “time zone”, since it was a question of US time zones with a specific name. In this case, it is more correct to use the " time zone ". A more correct translation is used here.

2. A small sidebar from Wikipedia will give you an idea of ​​what UTC is and how it differs from GMT -

Coordinated Universal Time (UTC) - the standard by which society regulates hours and time. It differs by an integer number of seconds from atomic time and by a fractional number of seconds from UT1 universal time.

UTC was introduced instead of the obsolete Greenwich Mean Time (GMT). A new UTC timeline has been introduced because the GMT scale is an uneven scale and is related to the Earth's daily rotation. The UTC scale is based on a uniform atomic time scale (TAI) and is more convenient for civilian use.

Time zones around the globe are expressed as positive and negative offsets from UTC.

It should be remembered that UTC time is not translated either in winter or in summer. Therefore, for those places where there is a switch to daylight saving time, the offset relative to UTC changes.


Now we continue to deal with structures serving entities such as date and time.

Shortly after posting a tweet about Noda Time, they started asking me what was the point of using Noda Time - people believed that the support for dates and time in .NET was quite good. Of course, I did not see their code, but I suspect that almost any code base dealing with dates will become clearer if it uses Noda Time, and also, quite possibly, will become more correct thanks to the approach by which Noda Time forces you to accept some solutions not obvious in .NET. In this article, we will discuss the drawbacks of the .NET API for working with dates and times. My attitude towards this topic seems somewhat biased, but I hope that this article does not look disrespectful to the team working on BCL (Base Class Library - approx. Translator)) - because, among other things, they work in conditions that force them to take into account interaction with COM, etc.

What does DateTime mean?

When I come across a question on the Stack Overflow website that says that DateTime is not doing what is expected of it, I often find myself in a state of reflection - what exactly should the indicated value represent? The answer is simple - date and time, right? But everything is much more complicated as soon as you start to deal with the problem more carefully. For example, suppose the clock did not tick between calls to two properties in the code below. So what value will the mystery variable take as a result?

DateTime utc = DateTime.UtcNow;
DateTime local = DateTime.Now;
bool mystery = local == utc;


I honestly don’t know what the execution of this code will lead to. There are three versions, each of which has its more or less sane justification:

  • The value will always be true : two values ​​are associated with the same moment in time, only one is expressed locally, and the second is universal
  • The value will always be false : two values ​​represent two different date values, i.e. automatically unequal
  • And again true - if your local time zone is synchronized with UTC ( Coordinated Universal Time ), i.e. then when time zones are not taken into account at all - both values ​​will be equal

I don't care much which answer is correct - the non-obviousness of the logic of code behavior is a sign of deeper problems. In fact, everything returns to the DateTime.Kind property, which allows DateTime to represent three types of values:

DateTimeKind.Utc : UTC date and time
DateTimeKind.Local : date and time local to the system in which
DateTimeKind.Unspecified code is executed : M um, slyly. Depends on what you do with it.

The value of a property has a different effect on different operations. For example, if you apply the ToUniversalTime () method to an “unspecified” DateTime value, the method will make the assumption that you are trying to convert a local value. On the other hand, if you apply the ToLocalTime () method to the “unspecified” DateTime value, you will be made to assume that you originally had the value in UTC. This is one model of behavior.

If you create a DateTimeOffset from DateTime and TimeSpan, the behavior is slightly different:
  • with the UTC value everything is simple - we transfer UTC, we want to receive representation "UTC + the specified offset"
  • the local value is true only sometimes: the constructor checks if the offset from UTC in the specified local time in the time zone used by the system by default matches the offset specified by you
  • an unspecified value (“unspecified”) is always true and represents local time in some unspecified time zone so that the offset is correct at that time.

I don’t know about you, but for me this state of affairs causes a slight semantic hysteria. It’s the same as having a “numeric” type that contains a sequence of numbers, but you must use another property in order to find out the decimal or hexadecimal digit, and sometimes the answer is “Well, what do you think?”.

Of course, in .NET 1.1, the DateTimeKind property was completely absent. This does not mean that the problem did not exist. This means that a confusing behavior that tries to give meaning to a type that stores different kinds of values ​​and does not try to be anything consistent. It was based on the assumption that the date value is permanently Unspecified.

Does using a DateTimeOffset structure fix the problem?

Good. Now we know that we don’t really like the DateTime type. Will DateTimeOffset help us? Yes, in part. A value of type DateTimeOffset has a clear meaning: it contains local date and time with a specified offset from UTC. Maybe now I should digress and explain to you what I mean by "local" date and time, as well as (temporal) moments.

Local date and time are not tied to a specific time zone. Is the present moment later or earlier “16:00 March 13, 2012”? Depends on where you are in this world (this, by the way, I still do not take into account non-ISO calendars). Therefore, DateTimeOffset contains a component that is independent of the time zone (this is “16:00 ...”), but also an offset from UTC - which indicates the possibility of its conversion at a timeon the time line. If you ignore the theory of relativity, then all people on the planet perceive the current moment at the same time. If I snap my fingers (infinitely fast), then any event in the universe will happen before or after this event. Whether you were in a particular time zone or not, it doesn’t matter. In this regard, the moments are global, in comparison with the local date and time, which each individual can observe at a particular moment in time.

(Are you still here? Let's continue - I hope that the previous paragraph was the hardest in this article. Although it describes a very important concept.)

So, DateTimeOffset refers to a (global) point in time, but also works with local date and time. This means that this structure is not an ideal type for representing local dates and times - but DateTime is not. DateTime with the DateTimeKind.Local property set is not really local for the same reasons - it is tied to the default time zone on the system in which it is used. A DateTime of the form DateTimeKind.Unspecified is a little better in some cases - for example, when creating a DateTimeOffset - but the semantics look strange in other cases, as described above. As a result, neither DateTimeOffset nor DateTime are good types for displaying truly local date and time.

The DateTimeOffset structure is also not a good choice for binding to a specific time zone, since it has no idea which time zone gave the corresponding offset first. In .NET 3.5 there is a quite adequate class TimeZoneInfo, but there is no type that says "local time in a specific time zone". Therefore, having a variable of type DateTimeOffset, you know the specific time in a certain time zone, but you do not know what the local time will be a minute later, since the offset for this zone could change (usually due to daylight saving time).

What about dates and times?

Prior to this, we only discussed values ​​that store "date and time." And what about the types in which only the date or only the time is stored? More often, of course, you only need to store the date, but there are times when you need to store only time.

And yet, yes, you can use the DateTime type to store the date — hell, yes there is even a DateTime.Date property that returns a date for a specific date and time ... but only as another DateTime value with a time set at midnight. This is not the same as having a separate type that is easy to identify as “only date” (or “only time” - .NET uses TimeSpan to do this, which again does not seem to me really right).

And what about time zones? Here TimeZoneInfo looks pretty decent.

As I said, TimeZoneInfo is not bad. True, he has two big problems and a few smaller ones.

Firstly, Windows time zone identifiers are taken as a basis. On the one hand, it is logical, but on the other, it is not something that the rest of the world uses. All non-Windows systems that I have seen use the Olson time zone database (also known as tz or zoneinfo), respectively, it has its own identifiers. Perhaps you saw them - "Europe / London" or "America / Los_Angeles" - these are the identifiers of Olson. Work with a web service that provides geoinformation - chances are that it uses Olson identifiers. Work with another calendar system - chances are that it also uses Olson identifiers. There are also problems here. For example, with the stability of identifiers thatThe Unicode Consortium is trying to solve using CLDR ... but at least you have a good chance. It would be great if TimeZoneInfo offered some way to establish a correspondence between two identifier schemes, or it would be implemented somewhere else in .NET. (Noda Time knows about both sets of identifiers, although mapping is not yet available to everyone. This will be fixed before the final release.)

Secondly, this class is based on the use of DateTime and DateTimeOffset, i.e. you should be careful when using it - if you set one type of DateTime and pass another, then you may have problems. The class is fairly well documented, but to be honest, explaining this kind of thing is inherently difficult without confusing the situation by using conflicting terms.

There are problems with ambiguous or erroneous values ​​of local dates and times. They occur during the transition to summer / winter time: if the time is moved forward (for example, from 1:00 to 2:00), then there is a chance of getting the wrong local time (for example, on this day 1:30 will not come). If the clock is set back (for example, from 2:00 to 1:00), this leads to ambiguity: 1.30 happens twice. You can explicitly check with TimeZoneInfo when a particular value is incorrect or ambiguous, but it’s easy to simply forget about this possibility. If you try to convert local time during UTC using the time zone, an exception will be thrown if the time is incorrect. But the ambiguous time will be taken as the standard default (and not as daylight saving time). This kind of solution does not allow developers to even take into account the features used.

Too complicated.

Now you may very well think: “I have inflated an elephant from a fly. I don’t want to think about it - why are you trying to make things so complicated? I have been using the .NET API for years and have not experienced any problems. ” If you thought so, I can offer three options:

  • You are much, much smarter than me, and understand all these difficulties on an intuitive level. You always use the correct view for a variable of type DateTime; where necessary - use DateTimeOffset and always do the right thing with incorrect or ambiguous local date / time. Without a doubt, you also write non-blocking multi-threaded code with shared access to the state of the object in the most efficient and at the same time reliable way. So what the hell are you reading all this, let me know?
  • You came across these problems, but for the most part forgot about them - in the end they took only 10 minutes of your life while you were experimenting to get an acceptable result (or at least to pass the test; although such tests could well be conceptually incorrect) . You may have been puzzled by this problem, but decided that the problem was with you, not the API.
  • You have not encountered this problem, because you consider testing the code a boring task, since it works in the same time zone, on computers that are always turned off at night (i.e., they are not affected by winter / summer transitions). You are, to some extent, lucky, but still you forget about the time zone.

No jokes, but the problem is really real. And if you had never before thought about the difference between the “local” time and the “global” moment before this article, then you did it. This is an important difference - it is similar to the difference between binary floating-point numbers and decimal floating-point numbers. Errors may not be obvious, difficult to diagnose, poorly explained, poorly corrected and re-occurring elsewhere in the program.

Processing the values ​​of variables storing date / time is really not easy. There are such unpleasant cases as days that do not start at midnight - due to the transition to summer / winter time (for example, Sunday, October 17, 2010, in Brazil it began at 1:00). If you are particularly unlucky, then you will have to work with multi-calendar systems (Gregorian, Julian, Coptic, Buddhist, etc.). If you were dealing with the dates and times of the beginning of the 20th century, you probably noticed very strange transitions of time zones as strictly longitude shifts transition to more “rounded” values ​​(for example, in Paris in 1911). You may encounter governments changing time zones with a warning about this a couple of weeks before the actual shift. You may also encounter a change in time zone identifiers (e.g.

All this, of course, lies on the surface of those relevant business rules that you are trying to implement. And they can also be difficult. Given all of these complexities, you should at least have an API that allows you to relatively clearly express what you mean.

So is Noda Time perfect?

Of course not. Noda Time has several problems:
1. Despite all of the above, I'm an amateur when it comes to the theory of date and time. The second of coordination confuses me. At the thought of the point of change of the Julian calendar to the Gregorian, the desire to cry embraces me, and therefore I have not yet realized it. As far as I know, not a single Noda Time project participant is an expert, although Stephen Colebourne, author of Joda Timeand the head of the JSR-310 lurked on the mailing list. (By the way, he attended the first Noda Time presentation. I asked if anyone in the room knew the difference between the Gregorian calendar and the ISO-8601 standard calendar. He raised his hand and gave the correct answer. I asked how it happened that he knew the answer and he replied: “I am Stephen Coleborn.” I almost fell).
2. We are not done yet. A wonderful API design is useless if there is no implementation.
3. There will certainly be errors - the BCL command code is constantly executed on hundreds of thousands of machines around the world. Errors will appear quickly.
4. We do not have resources - we are just an active group of developers working in pleasure. I'm not talking about this with regret (this is really great), but there are inevitable problems with the time that can be allocated to work on additional chips, documentation, etc.
5. We are not part of BCL. Want to use Noda Time in LINQ to SQL (or even NHibernate) queries? Good luck. Even if we succeed beyond my expectations, I don’t think that there are other open source projects that will depend on us for centuries. I must say that I am satisfied with the resulting design. We tried to maintain a balance between flexibility and simplicity of achieving any specific goal (with more efforts, of course). Somehow I will write another note about the design style used, comparing it with both the Joda Time design and the one used in .NET. The best result is the resulting set of types, each of which has its own very clear role. I will not bore you with the details - there will be documentation and other notes for this.

Oddly enough, it would be better for everyone if the team working on BCL takes note of this article and decides to radically redesign the API for .NET 6 (I assume that the ".NET 5" ship has already been launched). And while I am doing this, I am sure that there are many other projects that will give me pleasure - frankly, date and time issues are too important for the .NET community to lie solely on my shoulders for a long time.

conclusions

I hope that I convinced you that there are significant flaws in the .NET API. Perhaps I also convinced you that Noda Time deserves a closer acquaintance, but that was not the main goal. If you really understand the drawbacks of built-in types - especially the semantic ambiguity of DateTime - you should use these types in your code with extreme caution and accuracy. And only this will make me happy.

(Well, if you read this opus to the end, it means that I was not in vain spending my time translating - translator's note ).

Very free translation (c) Alien V.F. aka hDrummer, original here .

Also popular now: