Download Reference Manual
The Developer's Library for D
About Wiki Forums Source Search Contact

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

Comments
Author Message

Posted: 11/27/07 22:34:48

If I understand this right, we're discussing this in the interest of reducing the complexity of several different types that were used in varying and sometimes overlapping ways.

The TimeSpan/DateTime? solution offered is a step in the right direction, the only comment that I have to make on that is that both TimeSpan? and DateTime? are very similar, being spans of time. The only thing that differentiates DateTime? from TimeSpan? is that it is relative to a specific point in time. Because of this, I wonder if there is really cause for splitting time representation into two.

If there were a single TimeX (for lack of a better name) type which contained a span and was relative to a mutable point in time, I believe this single TimeX could accomplish the same goals as the TimeSpam/DateTime? solution, while also allowing for representation of time decoupled from a particular date. It also simplifies time representation in Tango even moreso than TimeSpan/DateTime?, from 3 representations to 1 (instead of 2).

The particular implementation could be a struct with both a mutable 'relative' date as well as a span, though this would increase the size of the struct. Perhaps better would be for the various 'DateTime?' like functions to reference a particular relative point. In fact, I think this is exactly as it functions now, and that it is indeed mutable, although the syntax is a bit peculiar.

Having a single type would also simplify decisions such as, 'What's the basic time type in Tango', as they pertain to Core discussions.

In any event, seeing as the TimeSpan? and DateTime? types are so very similar, I think that there is much complexity reduced in using a common type for both ideas of a particular point in time and a span of time, and moving those separate conceptual ideas into a related group of functions.

Posted: 11/28/07 04:01:38 -- Modified: 11/29/07 18:42:18 by
schveiguy -- Modified 3 Times

Here is what I think everything should look like:

... Moved to main page

Posted: 11/28/07 05:12:18

One thing I forgot to address. These changes I'm proposing should have very little effect on the Locale package.

Locale mainly uses the Calendar package to do it's work. In any case, I tried at least deprecating the DateTime? calendar accessors in a local copy of tango, and the only noticeable change in Locale is that the DayOfWeek? enumeration moved out of DateTime? into Calendar.

Posted: 11/29/07 10:59:31

Some thoughts, as writer of ISO8601:

  • The ISO 8601 standard allows for decoupled dates and times, all the way from only a century, "20", to only an hour, "20". (Yes, the same string can result in two different things depending on whether you want a date or a time.) Basically these can be thrown in a DateTime?, but I'd like the possibility to decouple them. This isn't strictly necessary, but it seems odd to me that something documented as returning only a date or time returns a "DateTime?". Using TimeSpan? for time isn't a solution, because these aren't time spans, but points in time. And as darrylb says, depending on how you think about it, DateTime? is also a time span, so the naming is perhaps somewhat misleading.
  • Parsing "04+6" as a time results in "22" (or it will, after fixing Ticket 778) due to time zone conversion. However, the day has changed by -1. In this case, the day is a span from the .init value, which can only be accessed cumbersomely through something like Calendar.days(returnVal - DateTime?.init). However, the time value is a point in time. Here we have, within one value, a point-in-time part and a span part. In this kind of case the TimeSpan/DateTime? dichotomy isn't so clear. Having separate timezone fields in a DateTime? might solve this, but that doubles the size of the struct, and since most of the time people will be using the default it's probably not worth it.
  • An "uninitialized" value for each field would be handy for unittesting and contracts: currently you can't know whether a function set a DateTime?'s year to 1 or whether it hasn't been changed. The problem now is also that given only a century, "20", it currently parses to the year 2000. That's not what it means - it means the 21st century, all dates from 2000 to 2099. Even worse, the resulting DateTime? is actually "2000-01-01 00:00:00" - but that's very distinct from actually parsing "2000-01-01T00:00:00Z". The century versus year thing is a limitation of the types we use which probably can't be helped, but uninitialized values would at least allow differentiating between a fully specified date/time pair and just "2000".

Regarding what you guys have been talking about:

  • Agree with darrylb that the types are a bit too similar for comfort. A typedef may suffice if the distinction is needed, but see also my second point above about how it can get confusing to differentiate between the two.
  • Agree with schveiguy that calendars should be decoupled, but especially the default, Gregorian, should be easy to access.
  • I like the free-functions idea for toString and co.
  • I'm not sure that UNIX time is very important to access easily. If people want it, though, I guess something like WallClock?.unix would be the way of doing it.

Posted: 12/19/07 08:25:38

The result of this discussion is unfortunately not ideal when it comes to DateTime, and I fear that it will be necessary to break the time package yet another time for the next release. There is no time to fix it prior to 0.99.4.

A type wrapping Calendar is needed, and normally this is the type called DateTime, but that has now been taken by tango.time.Time.DateTime, a simple and not all that useful struct (I know that we can have different types with same name in different modules, but that must be avoided at all costs). Since this type by definition will be fat, it probably should also be used to hold a time of arbitrary precision, i.e. 64 or more bits. Generally, I think Schveiguy's description of this type further up on this page was fairly spot on.