Simple time with Hourglass

Each time, I've used the time API in Haskell, I'm left with the distinct feeling that the API is not what I want it to be. After one time too many searching the API to do some basic thing, I've decided to look at the design space and just try implementing what I want to use.

Before going into this re-design, this is my list of issues with the current API:

Ironically, old-time seems much closer to what I have in mind with some part of the time API. The name seems to imply that this was the time api before it got changed to what is currently available.

Re-design

So I've got 4 items on this design list:

  1. Some better types
  2. Use the system API to go faster
  3. Unified and open system
  4. Better capability for printing and parsing

Better types

I wanted the main time type to be computer friendly, and linked to how existing API return the time:

It's probably fair to expect other systems to have similar accounting method, and anyway just those two flavors covers probably 99% of usage. I originally planned to keep the system referential in the type, but instead it's simpler to choose one.

Inventing a new one would be fairly pointless, as it would force both system to do operations. Converting between windows and unix epoch, is really simple and very cheap (one int64 addition, one int64 multiplication), so Unix has been chosen.

Along with the computer types, proper human types are useful for interacting with the users. This mean a Date type, a TimeOfDay, and a combined DateTime describe in pseudo haskell as:

    data Date = Date Year Month Day
    data TimeOfDay = TimeOfDay Hour Minute Seconds
    data DateTime = DateTime Date TimeOfDay

Use the System, Luke !

Heavy conversion between seconds and date is done by the system. Most systems got a very efficient way to do that:

One side effect is that we have the same working code as the system. There's much less need to worry about exactness or bugs in this critical piece.

For futureproofing, a haskell implementation could be used as fall back for other systems or different compiler target (e.g. haste), if anyone is interested.

Unified API

I don't want to have to remember many different functions to interact with many types. Also time representation should be all equivalent as to which time value they represent. So that mean it's easy to convert between them with a unified system.

So 2 type classes have been devised:

With this, hourglass support conversion between time types:

> timeConvert (Elasped 0) :: Date
Date { dateYear = 1970, dateMonth = January, dateDay = 1 }
> timeConvert (Date 1970 January 1) :: Elapsed
Elapsed 0
> timeConvert (DateTime (Date 1970 January 1) (TimeOfDay 0 0 0 0)) :: Date
Date { dateYear = 1970, dateMonth = January, dateDay = 1 }

Anyone can add new calendar types or other low level types, and still interact with them with the built-in functions, provided it implement conversion with the Elapsed. It allow anyone to define new calendar for example, without complicating anything.

Better formatting API

Formatter have a known enumeration types:

> timePrint [Format_Day,Format_Text '-',Format_Month2] (Date 2011 January 12)
"12-01"

But can be overloaded either by string, or some known formats:

> timePrint "DD-MM-YYYY" (Date 2011 January 12)
"12-01-2011"
> timePrint ISO8601_Date (Date 2011 January 12)
"2011-01-12"

Someone could also re-add C time format string too with this design, without changing the API.

Implementation

The API and values returned has been tested under 32 and 64 bits linux, freeBSD, and Windows 7. It's got the same limitations that the system has:

I find the tradeoff acceptable considering that in counterpart we have descent performance, and all-in-all a working range that is enough.

For a look on performance, as measured by criterion:

The library is small too:

And its documentation is available on hackage, and the code on github.

Example of use

> t <- timeCurrent
> timeGetDate t
Date {dateYear = 2014, dateMonth = May, dateDay = 4}
> t
1399183466s
> timeGetElapsed t
1399183466s
> timeGetDateTimeOfDay t
DateTime { dtDate = Date {dateYear = 2014, dateMonth = May, dateDay = 4}
         , dtTime = TimeOfDay {todHour = 6, todMin = 4, todSec = 26, todNSec = 0ns}}
> timePrint "YYYY-MM-DD" t
"2014-05-04"
> timePrint "DD Mon YYYY EPOCH TZHM" t
"04 May 2014 1399183466 +0000"

Q&A