Almost all projects face problems caused by improper handling and storage of date and time. Even if the project is used in one time zone, anyway, after the transition to winter / summer time, you can get unpleasant surprises. In this case, few people are puzzled by the implementation of the correct mechanism from the start, because it seems that there can be no problems with this, since everything is trivial. Unfortunately, later reality shows that it is not.
Logically, you can select the following types of values ​​related to the date and time:
- Date and time - “material for medical analysis was collected on January 15, 2014 at 13:17:15”
- Date without time - for example, “a new contract comes into force on February 2, 2016”
- Time interval - “the report was generated in 3 minutes 15 seconds”
- Schedule of scheduled events - “the import of data from another system should occur every weekday at 10:00”
Consider each item separately, not forgetting the
general recommendations .
date and time
Suppose the laboratory that collected the material for analysis is in the time zone +2, and the central branch, in which the timely execution of analyzes is monitored, is in the +1 belt. The time given in the example was noted when the material was collected by the first laboratory. The question arises - what time should the central office see? It is obvious that the software of the central office should show January 15, 2014 12:17:15 - an hour less, since according to their hours the event occurred at this very moment.
')
Consider one of the possible chains of actions, through which the data from the client to the server and vice versa, allows you to always correctly display the date / time according to the current client time zone:
- The value is created on the client, for example, on March 2, 2016 at 15 : 13: 36, the client is in the time zone +2.
- The value is converted to a string representation for transmission to the server - “2016-03-02T 15 : 13: 36 + 02: 00”.
- Serialized data is transferred to the server.
- The server deserializes the time into a date / time object, leading it to its current time zone. For example, if the server is running at +1, then the object will contain March 2, 2016 14 : 13: 36.
- The server saves the data to the database, while it does not contain any information about the time zone - the most commonly used date / time types simply do not know anything about it. Thus, the database will be saved on March 2, 2016 14 : 13: 36 in the “unknown” time zone.
- The server reads data from the database and creates the corresponding object with the value March 2, 2016 14 : 13: 36. And since the server works in the time zone +1, then this value will be interpreted in the framework of the same time zone.
- The value is converted to a string representation for transmission to the client - “2016-03-02T 14 : 13: 36 + 01: 00”.
- Serialized data is sent to the client.
- The client deserializes the resulting value into a date / time object, leading it to its current time zone. For example, if it is -5, then the displayed value should be March 2, 2016 09 : 13: 36.
It seems to be all holistic, but let's think that in this process can go wrong. In fact, problems here can happen at almost every step.
- Time on the client can be formed without a time zone at all - for example, the DateTime type in .NET with DateTimeKind.Unspecified.
- The serialization mechanism can use a format that does not include a time zone offset.
- When deserializing into an object, the time zone offset can be ignored, especially in self-made deserializers, both on the server and on the client.
- When reading from a database, a date / time object can be formed without a time zone at all - for example, the DateTime type in .NET with DateTimeKind.Unspecified. Moreover, with DateTime in .NET, in practice this is exactly what happens if, immediately after reading, no other DateTimeKind is explicitly specified.
- If the application servers working with the common database are in different time zones, there will be a serious confusion in the time shifts. The date / time value written to the database by server A and read by server B will differ noticeably from the same initial value written by server B and read by server A.
- Transferring application servers from one zone to another will lead to incorrect interpretation of already stored date / time values.
But the most serious flaw in the chain described above is the use of the local time zone on the server. If there is no daylight saving time, then there will be no additional problems. But otherwise you can get a lot of unpleasant surprises.
The rules for daylight saving time are, strictly speaking, variable. Different countries may sometimes change their rules, and these changes must be incorporated in advance into system updates. In practice, situations of incorrect operation of this mechanism have repeatedly been encountered, which, as a result, were solved by installing hotfixes of either the operating system or third-party libraries used. The likelihood of repeating the same problems is not zero, so it’s best to have a way to avoid them guaranteed.
Taking into account the above considerations, we formulate the most reliable and simple approach to the transfer and storage of time:
on the server and in the database, all values ​​should be brought to UTC time zone .
Consider what this rule gives us:
- When sending data to the server, the client must transfer the time zone offset so that the server can correctly convert the time to UTC. The alternative is to require the client to do this conversion, but the first option is more flexible. When receiving data back from the server, the client will transfer the date and time to his local time zone knowing that he will come to UTC in any case.
- In UTC there are no transitions to summer and winter time, respectively, the associated problems will be irrelevant.
- When reading from the database on the server, it is not necessary to convert the time values, it is enough just to explicitly indicate that it corresponds to UTC. In .NET, for example, this can be achieved by setting the DateTimeKind for the time object to DateTimeKind.Utc.
- The difference in time zones between servers working with a common database, as well as the transfer of servers from one zone to another, does not affect the correctness of the received data.
To implement such a rule, it is enough to take care of three things:
- Make the serialization and deserialization mechanism such that the date / time values ​​are correctly transferred from UTC to the local time zone and back.
- Ensure that the server-side deserializer creates date / time objects in UTC.
- Make it so that when reading from the database, date / time objects are created in UTC. This item is sometimes provided without code changes — just the system time zone on all servers is set to UTC.
The above considerations and recommendations work well
with a combination of two conditions :
- In the system requirements there is no need to display the local time and / or time zone offset exactly in the form in which it was saved. For example, on air tickets, the time of departure and arrival must be printed in the time zone corresponding to the location of the airport. Or if the server sends to print invoices created in different countries, each should end up with local time, and not converted to the server time zone.
- All date and time values ​​in the system are “absolute” - i.e. describe a point in time in the future or the past, which corresponds to the only value in UTC. For example, “the launch of the launch vehicle took place at 11:00 pm Kyiv time”, or “the meeting will be held from 1:30 pm to 2:30 pm Minsk time”. In different time zones, the numbers for these events will be different, but they will describe the same point in time. But it may happen that software requirements imply “relative” local time for some cases. For example, "this TV show will go from 9:00 to 10:00 in the morning in each country where there is a branch of the TV channel." It turns out that the show of the program is not one event, but several, and all of them can potentially occur at different time intervals on an “absolute” scale.
For cases where the first condition is violated, the task can be solved using data types that contain the time zone, both on the server and in the database. Below is a small list of examples for different platforms and DBMS.
.NET | Datetimeoffset |
Java | org.joda.time.DateTime, java.time.ZonedDateTime |
MS SQL | datetimeoffset |
Oracle PostgreSQL | TIMESTAMP WITH TIME ZONE |
Mysql | - |
Violation of the second condition is a more complicated case. If this “relative” time needs to be stored just for display, and there is no task to determine the “absolute” moment of time when an event has come or will come for a given time zone - simply deny the time conversion. For example, the user entered the beginning of the transfer for all branches of a television company on March 25, 2016 at 9:00, and it will be transmitted, displayed and displayed in this form. But it may happen that some scheduler should automatically perform special actions one hour before the start of each program (send out notifications or check the presence of some data in the broadcaster's database). Reliable implementation of such a scheduler is not a trivial task. Suppose the scheduler is aware of what time zone each branch is located in. And one of the countries where there is a branch, after some time decides to change the time zone. The case is not as rare as it may seem - I have counted more than 10 similar events in this and two previous years (
http://www.timeanddate.com/news/time/ ). It turns out that either users must keep their time zone bindings up to date, or the scheduler must automatically take this information from global sources such as the Google Maps Time Zone API. I do not undertake to offer a universal solution for such cases, I just note that such situations require serious study.
As can be seen from the above,
there is no single approach covering 100% of cases . Therefore, you must first clearly understand from the requirements which of the above situations will be in your system. Most likely, everything will be limited to the first proposed approach with storage in UTC. Well, the described exceptional situations do not cancel it, but simply add other solutions for particular cases.
Date without time
Suppose that the date and time are displayed correctly, taking into account the client's time zone. Let us turn to the dates without time and the example specified for this case at the beginning - “the new contract comes into force on February 2, 2016”. What will happen if for such values ​​to use the same types and the same mechanism as for “ordinary” dates with time?
Not all platforms, languages ​​and DBMS have types that store only the date. For example, in .NET there is only the DateTime type, there is no separate “just Date”. Even if when creating such an object, only the date was indicated, the time is still present, and it is equal to 00:00:00. If we transfer the value “February 2, 2016 00:00:00” from the belt with offset +2 to +1, we get “February 1, 2016 23:00:00”. For the above example, this would be equivalent to the fact that in one time zone a new contract will begin on February 2, and in the other on February 1. From a legal point of view, this is absurd and so, of course, should not be.
The general rule for "pure" dates is extremely simple - such values ​​should not be converted at any one save and read step.There are several ways to avoid conversion for dates:
- If the platform supports a type that represents a date without time, then it should be used.
- Add a special attribute to object metadata that will tell the serializer that the time zone for this value should be ignored.
- Send the date from the client and back as a string, and store as a date. This approach is inconvenient if the client needs to not only display the date, but also perform some operations on it: comparison, subtraction, etc.
- Transfer and store as a string, and convert to date for formatting only, taking into account the client’s regional settings. It has even more drawbacks than the previous version - for example, if in the stored string of the date part are not in the order of "year, month, day", then it will be impossible to do an effective indexed search by date range.
You can, of course, try to give a counterexample and say that a contract makes sense only within the country in which it is concluded, the country is in the same time zone, and therefore it is possible to unambiguously determine the time of its entry into force. But even in this case, users from other time zones will not be interested at what point in their local time this event will occur. And even if there was a need to show this point in time, then it would be necessary to display not only the date, but also the time, which contradicts the original condition.
Time interval
With the storage and processing of time intervals, everything is simple: their value does not depend on the time zone, so there are no special recommendations here. They can be stored and transmitted as the number of units of time (integer or floating point, depending on the required accuracy). If second accuracy is important, then how is the number of seconds, if millisecond, then how is the number of milliseconds, etc.
But the calculation of the interval can have pitfalls. Suppose we have a typical C # code that counts the time interval between two events:
DateTime start = DateTime.Now; //... DateTime end = DateTime.Now; double hours = (end - start).TotalHours;
At first glance, there are no problems here, but this is not so. Firstly, there may be problems with unit testing of such code, but we'll talk about this a little later. Secondly, let's imagine that the initial moment of time fell on winter time, and the final one - on summer time (for example, the number of working hours is measured in this way, and the workers have a night shift).
Suppose the code works in a time zone in which daylight saving time in 2016 occurs on the night of March 27, and simulate the situation described above:
DateTime start = DateTime.Parse("2016-03-26T20:00:15+02"); DateTime end = DateTime.Parse("2016-03-27T05:00:15+03"); double hours = (end - start).TotalHours;
This code will result in 9 hours, although in fact between these moments 8 hours have passed. You can easily see this by changing the code like this:
DateTime start = DateTime.Parse("2016-03-26T20:00:15+02").ToUniversalTime(); DateTime end = DateTime.Parse("2016-03-27T05:00:15+03").ToUniversalTime(); double hours = (end - start).TotalHours;
Hence the conclusion -
any arithmetic operations with date and time must be done using either UTC values ​​or types that store time zone information . And then translate back to local if necessary. From this point of view, the original example is easy to fix by changing DateTime.Now to DateTime.UtcNow.
This nuance does not depend on a particular platform or language. Here is a similar Java code that has the same drawback:
LocalDateTime start = LocalDateTime.now();
It is also easily fixed - for example, using ZonedDateTime instead of LocalDateTime.
Scheduled events schedule
Schedule scheduled events - a more difficult situation. There is no universal type allowing storing timetables in standard libraries. But such a task does not arise so rarely, so ready-made solutions can be found without problems. A good example is the cron scheduler format, which is used in one form or another by other solutions, for example, Quartz:
http://quartz-scheduler.org/api/2.2.0/org/quartz/CronExpression.html . It covers almost all scheduling needs, including variants of the second Friday of the month.
In most cases, writing your scheduler does not make sense, since there are flexible time-tested solutions, but if for some reason you need to create your own mechanism, then at least the schedule format can be borrowed from cron.
General recommendations
In addition to the above recommendations on the storage and processing of different types of time, there are several others that I would also like to say.
First, about using static class members to get the current time - DateTime.UtcNow, ZonedDateTime.now (), etc. As it was said, using them directly in the code can seriously complicate unit testing, since without special framework frameworks it is impossible to replace the current time. Therefore, if you plan to write unit tests, you should make sure that the implementation of such methods can be replaced. To solve this problem, there are at least two ways:
- Highlight the IDateTimeProvider interface with a single method that returns the current time. Then add a dependency on this interface in all code points where you need to get the current time. With normal program execution, a “default” implementation will be injected into all these places, which returns the actual current time, and in unit tests any other necessary implementation will be injected. This method is the most flexible in terms of testing.
- Make your own static class with a method to get the current time and the ability to install any implementation of this method from the outside. For example, in the case of C # code, this class may expose the UtcNow property and the SetImplementation method (Func <DateTime> impl). Using a static property or method to get the current time eliminates the need to explicitly assign dependencies on an additional interface everywhere, but from the point of view of OOP principles, this is not an ideal solution. However, if for some reasons the previous version does not fit, then you can use this.
An additional problem that should be solved during the transition to its implementation of the current time provider is the control that no one “in the old manner” continues to use standard classes. This problem is easily solved in most code quality control systems. In essence, it boils down to searching for an “undesirable” substring in all files except where the default implementation is declared.
The second thing about getting the current time is that you
can't trust the customer . The current time on users' computers can be very different from the real one, and if there is logic tied to it, then this difference can spoil everything. All places where there is a need to get the current time should, if possible, be performed on the server side. And, as mentioned earlier, all arithmetic operations with time must be performed either in UTC values ​​or using types that store time zone offsets.
And one more thing I wanted to mention is the
ISO 8601 standard , which describes the date and time format for exchanging information. In particular, the string representation of the date and time used in serialization must conform to this standard to prevent potential compatibility problems. In practice, it is extremely rare to have to implement the formatting itself, so the standard itself can be useful mainly for informational purposes.