Time Package Design / Redesign
The Time package is going through a redesign these days, mainly to simplify what was too complex. There is a concern though that it is, or will become, too simple for certain use cases. This page is meant to be used to flesh out all details before further changes are committed. If necessary, a branch should be set up instead.
Concerns
- Date, Time and DateTime of the past weren't really that different.
- DateTime is tightly connected to the Calendars
- Is TimeSpan and DateTime really different?
- Is there, or should there even be, a way to represent a particular time of day (decoupled from a particular date)?
- Importing value-types (with implementation) into an interface is a big no-no for Tango. Interfaces are used in Tango for decoupling implementation, so importing implementation into an interface module is not the right approach. This is partly why Interval was leveraged as it was prior to recent changes. Consider, for example, that a toString() is added to DateTime? - now all the formatting code is imported into pure interface modules also, increasing implementation coupling quite dramatically (where before it had been cleanly decoupled). This could be solved by using classes instead of structs, so that interfaces could be applied. The new D2 struct-interface may help here also.
- ...
Goals
Solution / Design
- One alternative would be to revert to the prior Interval notion (a typedef) and implement all functionality as free functions. This would resolve the interface issue noted above, resolve concerns over toString and co, and may well resolve a slice of the others too?
Schveiguy's design
- DateTime and TimeSpan are represented as separate distinct structs
Yes, the functionality is very similar, but the alternative of having 96-bit or even 128-bit representation is much less attractive to me. The current implementation loses no performance over the original implementation of time as a long enumeration. With separate types, you don't have to check unreasonable calls at run time. For example, let's say TimeX is the type, and it has a member referenceDate, you would have to do runtime checks for simple operations:
TimeX opAdd(TimeX t) { if(referenceDate != unspecified && t.referenceDate != unspecified) throw Exception("cannot add two points in time together"); ... }
The current code performs this check at compile time because a DateTime cannot be added to another DateTime.
Using a typedef to a builtin type does not allow overloading of the operators, and so it is impossible to enforce the relationship.
Finally, the duplicate functionality is written only once, and has no effect on the user of the code. In fact, one type makes the code look more confusing if the coder does not name his variables appropriately, or re-uses variables to mean both types.
- DateTime should not be coupled to a particular calendar.
This means all calendar related functions for a DateTime will be deprecated. The following DateTime functions already exist in exact or portable forms in Calendar:
opCall(int year, int month, int day); // Calendar.getDateTime (int year, int month, int day, int hour, int minute, int second, int millisecond) int day(); int month(); int year(); int dayOfYear(); DayOfWeek dayOfWeek(); static int daysInMonth(int year, int month) static bool isLeapYear(int year); // Calendar.getDayOfMonth(DateTime) // Calendar.getMonth(DateTime) // Calendar.getYear(DateTime) // Calendar.getDayOfYear(DateTime) // Calendar.getDayOfWeek(DateTime) // Calendar.getDaysInMonth(int year, int month) // Calendar.isLeapYear(int year)
There are 2 functions which are in DateTime which are not in Calendar, but should go into Calendar:
DateTime addMonths(int value); DateTime addYears(int value);
I believe these kinds of functions belong in Calendar because they can be different depending on the calendar you want to use.
- Calendar should be more useable
If we just deprecate DateTime functions, and put them in calendar, then the following annoyance occurs:
DateTime dt = Clock.now; //int year = dt.year; int year = GregorianCalendar.getDefaultInstance.getYear(dt);
Most of the time, you want to use the same calendar throughout all code. Therefore, I propose you set a default calendar in a static member of Calendar itself. This will default to GregorianCalendar. In GregorianCalendar, this can be done by:
static this() { if(Calendar.current is null) Calendar.current = getDefaultInstance(); }
This way, if a user sets the default calendar in his own static this(), Gregorian doesn't override it. Now the call isn't as ugly:
int year = Calendar.current.getYear(dt);
And if static methods are added to calendar which simply call the default instance's method:
int year = Calendar.year(dt);
- DateTime only imports TimeSpan, TimeSpan imports nothing.
This addresses the issue of making DateTime and TimeSpan the minimal implementation so they can be used in interfaces. When D 2.0 is released, it is supposed to allow extending structs via the same syntax that arrays now have. i.e.:
char[] toString(DateTime dt)
can be called like:
dt.toString();
This will allow us to extend functionality without having interfaces import this functionality. If one wants the toString function, one will just import the module that does it.
- TimeSpan represents a time of day decoupled from a date
This really isn't a huge problem as I see it. Perhaps we could add accessors to TimeSpan to get the time of day fields. Currently the .seconds accessor gets the total seconds represented by the TimeSpan. Perhaps a .compSeconds accessor would get the number of seconds % 60.
These can also be done in a separate method, and then used as accessors when D 2.0 supports it.
- tango/text/convert/TimeStamp allows conversion of TimeSpan to a "[-]day.hh:mm:ss.subseconds" string
This allows people to print TimeStamp in a meaningful way (instead of a huge number of 100ns increments)
- tango/text/convert/Layout should print DateTime and TimeSpan natively
This further completes the notion that the types are builtin
I spoke with Kris on IRC, and he doesn't like this idea :) But it's just a nicety that I think would be good. The issue is that if you import TimeStamp in Layout, there is a lot of extra code there that may never be used.
- Clock and WallClock.now should be overloaded to return standard c time
Yes, the old seconds since 1970 :) Except we should return it as a long to avoid the 2038 bug. I find this is a very often requested feature, and doing the conversion with the current design is cumbersome:
int unixTime = (Clock.now - DateTime.epoch1970).seconds;
Schveiguy's enhancements
These are some ideas which might solve some of the mentioned concerns, but may be beyond the scope
TimeZone class
Currently, there is no TimeZone representation. The only notion of a time zone is the current time zone, and that does not describe when Daylight Savings time is in effect. Essentially, a TimeZone class should contain:
- Name
- Years of effectiveness. Time zones can change, this is not determined by science, but by governments, and so there is no logic behind when this happens. In order to account for when these occur, a TimeZone should know when it was in effect. Two timezones can have the same name, but they must have non-overlapping years of effectiveness.
- DST begin and end information. This allows one to determine when DST was in effect. I think this varies from year to year, so it should be a function of the year.
- Normal offset of UTC. This will be a TimeSpan.
- DST offset of UTC.
Time zones are loaded from the OS. On windows, it's in the registry, on Posix systems, I'm not sure.
The current time zone name should be accessible from WallClock.
Using a TimeZone class, we can now allow Date to be valid for all values of DateTime. Currently, the functions that translate a DateTime to a Date depend on OS support.
extended DateTime class
Some of the concerns have to do with ease of use with the time structures, and ability to extract components of the date and time from a point in time. All these depend on a lot of the features of Calendar, Clock, TimeZone, toString, and others. Putting all this functionality into the current DateTime would make it painful to include DateTime in interfaces. However, if all the functionality exists outside DateTime, then the usability of these features is less than ideal.
What we could do is have a class that encapsulates all these functions into one type. The class would replace Date, and contain components of the point in time like Date does. It would have a reference to a Calendar and a TimeZone. The constructor would take a DateTime instance, and optionally a Calendar and TimeZone instance. If not provided a Calendar or TimeZone, it would use the current default.
If this is a desired thing, then I think that we should change the names of everything. DateTime seems like a very good name for this. We could rename the struct representing a point in time to just Time.
The new DateTime would contain fields for all date/time components (i.e. year, month, day, etc down to nanosecond), and would provide methods to convert to a Time using the Calendar and TimeZone references. We could provide operators on 2 DateTime values (convert both to UTC Time, then perform operation).
The usage would be:
- Interfaces are only allowed to use TimeSpan or Time.
- If you want to just do simple timing functions, such as timeouts or date/time arithmetic, use Time and TimeSpan.
- If you want to use a single extended feature, such as Calendar functions, but do not want to increase the bloat of your code, use that feature directly with Time/TimeSpan as an argument.
- If you want an encapsulation of all things, including string conversion, Calendar, and TimeZone, use DateTime (the new version).
Dependencies of all the types
- TimeSpan: none
- Time: TimeSpan
- Calendar: Time, TimeSpan
- TimeZone: TimeSpan
- Clock: Time, TimeSpan
- WallClock: Clock, Time, TimeSpan, TimeZone
- text/convert/TimeStamp: Calendar, Time, TimeSpan, TimeZone
- DateTime: all of the above
References
Discussion thread