This post is meant to be the first in a series highlighting various
interesting features of Core (although I should acknowledge that
most of the continuing series I've started so far have not, when it
comes down to it, continued.) This time I wanted to focus on how
Core handles time.
Time is a surprisingly complex topic, as anyone who has worked through
the gory details of calendrical calculations can tell you. One of
the initial complexities is that there are lots of
different-but-related concepts that come into play, and in order to
design a good library, you have to figure out how to reflect those
concepts in your datatypes. Here are the primary types
that Core uses for dealing with time:
Time.t, an absolute time, i.e., a fully specified point in time,
independent of time zone or any other information.Time.Span.t, a length of time, as in "5 minutes" or "3
hours".Time.Ofday.t, a time of day, as in, "3:53:12 PM".Date.t, a date, e.g. "2008-12-13".Weekday.t, a day of the week, e.g., "Monday"TZ.Zone.t, a timezone, e.g., "EST5EDT". The combination of a
date, a time of day, and a timezone is sufficient to compute a
Time.tInterestingly, Time.t, Time.Ofday.t and Time.Span.t share the
same underlying implementation: they are all floating point numbers
representing a number of seconds. A Time.t is the same kind of
float returned by the gettimeofday call in the standard of library,
basically a traditional UNIX time. A Time.Ofday.t is implemented
with a float representing the number of seconds since the beginning of
the day, and a Time.Span.t is represented by a float representing
the number of seconds in question.
By seperating into three types rather than one, we get types that are
more informative and less error prone. For example, the function
Time.diff and Time.add have the following signatures:
val diff: Time.t -> Time.t -> Time.Span.t val add: Time.t -> Time.Span.t -> Time.t
This stops you from making basic mistakes, like taking two absolute times and adding them together and expecting to get another absolute time.
Core's handling of time has a lot going for it. There are many
useful functions, and it's been reasonably well battle tested
(although there are surely bugs yet to be found.) There is also the
very useful TZ module, which is a reimplementation of UNIX's
timezone handling, which uses the standard UNIX timezone database.
("Why reimplement?" you may ask. It turns out that the libc calls for
doing timezone conversion require you to specify the timezone by
stuffing it into the TZ environment variable before making the call.
That makes these calls painful and error-prone in the presence of
threads.)
The biggest remaining problem we have is that time zone handling is
not integrated into the Time module itself -- Time can only
convert strings in localtime and UTC. Integration of TZ into Time is something we hope to get done in the next release or two.
Comments
Time.Span.t might also need more work
One of the things that I'm not sure of we have the right abstraction yet is Time.Span.t. As it is just a number of seconds it cannot really be used to express concepts such as
one month, one year, ten year as those are all different depending on the starting point.
Interestingly even one day is not independent of the starting point as there is such a horrible concept as a leap second. (And even 1 hour is a strange concept if you hit Daylight savings time).
Maybe a type
could be a good idea. But I have only given it a cursory thought so far.
Time and Calendars
This is tricky stuff. It's worth noting that the
Timemodule does have more calendrical calculations than it used to -- for instance, there are functions for computing on dates, i.e., adding n days to the present day. And there are even some calculations that deal with business days and holidays and such. These calculations are indeed subtly different from doing the obvious transformations using spans.I'm not sure that having a single type called
Logical_time_spanreally works, though. For instance, adding years and adding days are quite different operations, and so you would kind of need to invent two different span types (and maybe more. What about adding months?) I think a better way to approach this is to have functions that can add years, months or days to a given date, when the former are provided as simple integers. I think this is a case where adding more types doesn't really help matters.Comparison with the Calendar library
The calendar library has the very same purpose than your Time library included in Core. It properly handles time spans (called periods in Calendar) and supports year/month/week/day/hour/minute/second spans in a single datatype (Calendar.Period.t) : there is really no need to have several span types for supporting different kinds of span.
More generally, it seems to me that the main features of Time and Calendar are very similar. However Calendar has more extra features but is bigger/heavier than Time. Is it the main reason why you don't use this library at Jane Street?
Time vs Calendar
I'm surprised by the idea that one would want to use the same type for measuring a span of, say, days and a span of seconds. They are really quite different animals. A
Time.Span.tis a span of time in the ordinary stopwatch sense. A span of days is more of a calendrical concept, and something we don't model explicitly. After all, going forward by 24 hours is not always the same thing as going forward by one day. I would think that one would not want to mix the calendrical and stopwatch concepts in the same type. To be clear, we do supportTime.Span.t's of arbitrary duration. It's just these calendrical spans that would require special handling, I believe.As for why we use
Timeinstead ofCalendar, there are a few reasons. First of all, theTimelibrary goes back many years, and is in fact about as old asCalendar. Beyond that, time is an extremely fundamental concept, and we wanted a library that hewed closely to the conventions and standards of the remainder of our libraries. The fact that the types in Time satisfy all of our standard interfaces (Sexpable,Binable,Comparable,Floatable, etc.) is very useful.I don't think either library is strictly more capable than the other. For example, I think our
TZlibrary is considerably more sophisticated thanCalendar'sTime_Zonelibrary. AndTimehas a distinct (and I think useful) type for times of day (Time.Ofday.t), whichCalendarI think lacks. AndCalendarhas some features (like int and float based implementations, a generic time printer, and some extra calendrical operations) thatTimelacks.I admit to being somewhat chagrined at introducing extra library duplication in the world, but it never quite made sense for us to toss our library out the window and switch to
Calendar. And for someone usingCore, there is a lot of value to having a library that works smoothly with and follows the conventions ofCore.I agree (mostly)
I agree that going forward 24 hours is not always the same thing as going forward by one day. As the Time library does, the calendar library does not internally mix incompatible time spans even if they are represented by the same datatypes. But having only one datatype allows to go forward 1 month, 3 days and 5 hours in only one operation: that may be useful.
I fully understand all the reasons why you do not use the Calendar library both in a general-purpose library like Core and in your internal development. By the way, in my company, we also avoid to use external out-of-control libraries.
I agree that your TZ library is considerably more sophisticated than Calendar's Time_Zone. But, after a quick look, it seems to me that Time.Ofday.t of your library corresponds to Calendar's Time.t.
See Smalltalk Chronos time library: time points as intervals
I would recommend having a look at the Smalltalk Chronos date/time library, starting with the following URL:
They have considered and addressed a number of date, time, and time zone issues, including the ones you've mentioned. I found their work very helpful as a guide in implementing a date/time library for Scheme -- one that supports TAI, leap seconds, and bi-directional inter-conversion of universal and nominal time values.