Entropic Thoughts

A Haskell Time Library Tutorial

A Haskell Time Library Tutorial

The time library is a common source of confusion for new Haskell users, I've noticed. With no disrespect to the authors intended, I feel like the existing tutorials don't do a very good job of conveying the essence of the library so I'm going to give it a shot myself.

Cheat Sheet

Here's a cheat sheet with the important functions I mention in this tutorial. There are others, but some of them I actively disrecommend you from using, and some of them are just convenient wrappers around the ones I mention.

This will probably make no sense until you've read the rest of the tutorial. When you have, you might want to bookmark this page for a quick reference.

  • Dates:

    • Day is the type representing a date
    • fromGregorianValid :: Integer -> Int -> Int -> Maybe Day creates a date value, by taking a year–month–day triple and returning Nothing if the date is invalid
    • addDays :: Integer -> Day -> Day can add and subtract a number of days to a date value; supply a negative Integer for subtraction
    • diffDays :: Day -> Day -> Integer gives you the number of days that lie between two dates
    • addGregorianMonthsClip :: Integer -> Day -> Day moves a date a number of months forward or backward, adjusting it when necessary to stay within short months
    • toGregorian :: Day -> (Integer, Int, Int) converts a date back to a year–month–day triple, but it shouldn't be something you do too often
  • Universal time:

    • UTCTime is the type representing a point in universal time; it is built from a date value and the number of seconds into that day
    • getCurrentTime :: IO UTCTime returns the current universal time
    • addUTCTime :: NominalDiffTime -> UTCTime -> UTCTime lets you compute new times based on an existing point in time, by adding the number of seconds specified in the first argument to the second argument
    • diffUTCTime :: UTCTime -> UTCTime -> NominalDiffTime computes the number of seconds that have passed between the two points in time specified
    • A NominalDiffTime is a regular number, but can not be combined with Integer, Double and other numbers without conversion
    • realToFrac converts any number to a NominalDiffTime
    • You can pattern match on a UTCTime to extract the date component and do maths on it separately
  • Local time:

    • ZonedTime is the type of a local time value
    • zonedTimeToUTC :: ZonedTime -> UTCTime converts a local time value to universal time
    • utcToZonedTime :: TimeZone -> UTCTime -> ZonedTime converts a universal time value to local time, given a timezone for the local time
    • A TimeZone is a value representing the users timezone; it can be pattern matched out from a local time value
    • getZonedTime :: IO ZonedTime gets the current local time for the user
  • Formatting:

    • formatTime :: FormatTime t => TimeLocale -> String -> t -> String formats a date or time value according to the format string and locale specified
    • A TimeLocale is a value that contains, among other things, translations of the weekday names to the users language
    • defaultTimeLocale contains the English names of weekdays among other things; can be modified if you don't want to create a time locale from scratch

Safety

It's important to know that the time library is designed with safety as a priority. Knowing this gives you understanding for why some things seem to need extra steps.

As an example, it is common to accidentally treat a local time value (10 o'clock on a Sunday morning in Sweden as I'm writing this) with UTC (strictly 8:00 everywhere in the world right now.) UTC stands for "universal coordinated time" and is the same everywhere on Earth, while local time can vary as you travel from place to place due to daylight savings time, time zones and other silly human irregularities.

If you accidentally mix local time and UTC, you can get bad, impredictable behaviour. Imagine agreeing that you should call someone at 10.00 UTC, and then you call them 10.00 your time! They'll either not take your call or be a bit upset with you, if it comes at a bad time for them.

The time library prevents this sort of confusion entirely. If you try to treat a local time value as UTC the compiler will refuse your program. If you want to convert a local time value to UTC you have to ask for it and provide a timezone so it knows how to do the conversion.

Overview

We can think of the time library as having three different kinds of data:

  • Date values, which indicate a specific date (like "25 april, 2009") and are timezone independent. These are represented in Haskell with the Day type.

  • Universal time values, represented with two types: the UTCTime type represents a date and time combination ("25 april, 2009, 13:52:31") and the NominalDiffTime type represents a difference between two UTCTimes.

  • Local time values, represented by the ZonedTime type which links a local time with the relevant timezone.

You are not meant to use local time values too much. Local time is a complicated and difficult thing to handle. The local time stuff is useful when you want to interface with users, but internally your application should use universal time for calculations and coordination.

Generally, you'll use universal time for most of the things you do, but since UTCTime uses the Day type, I thought we might look at date values first.

These three things should cover the most common use cases, but will not cover all of them. Time management is really complicated, and to include every possible use case in this tutorial is not possible without turning it into a book. This is a beginners' tutorial to the time library, and does not intend to be a comprehensive guide to time management in computer programs.

Oh, and by the way: unless I say otherwise, all the functions and constructors I talk about are available by importing just Data.Time.

Date Values

Sometimes you don't care about the exact point in time something happened. This blog, for example, only has a date attached to articles – not a time. I don't care about micro-managing which hour and minute an article is published at, so I just set a date on it and then publish it at UTC midnight that date.

Date values are also useful when something happens for an entire day, or a span of days. Like, Christmas Day in western cultures is on December 25th. There is no time specification that it starts at 03:00 and ends at 14:30 – it spans the entire day.

Construction

Constructing a date value from a year–month–day triple is easy. The function fromGregorianValid is your friend. It returns a Maybe Day value which is Nothing if you enter an invalid date.

In other words,

Prelude Data.Time> fromGregorianValid 2008 10 22
Just 2008-10-22 :: Maybe Day

but,

Prelude Data.Time> fromGregorianValid 2014 2 31
Nothing :: Maybe Day

because the February 31, 2014 is not a valid date. So be sure to check the return value of fromGregorianValid, especially if the date is entered by a user.

What's all this Gregorian business and who was Gregory? Gregory was a pope in the 14th century who accepted a proposal by Lilius (astronomer and polymath) to modernise the calendar. The one they used at the time slowly drifted out of sync with the actual seasons, and the calendar Lilius proposed takes longer to drift out of sync with the actual seasons. The calendar we use in the west today is the one Lilius proposed. It's called Gregorian after the pope who decided that it was time to switch to it.

(The calendar we used before the Gregorian reform was called the Julian calendar. You can guess who decided on that one …)

Destructuring

If you for some reason want to convert a Day value back to a year–month–day triple, you do that with the toGregorian function. However, I'd argue that wanting to do that often is a sign of bad code design somewhere.

If you want to plain convert a date to a string, you can do that with the regular show function:

Prelude Data.Time> show today
"2015-08-30" :: String

If you want to print a date in a custom format, you can do that too without having to convert it to numbers again. We'll get to that later.

Calculation

There are a few ways you may want to calculate with dates. The most obvious one is saying something like "what date is it 7 days from now?"

Prelude Data.Time> today
2015-08-30 :: Day
Prelude Data.Time> addDays 7 today
2015-09-06 :: Day

This tells us that 7 days from today (i.e. in a week) it'll be the 6th of September. If I want to know how many days it is until my birthday, we can figure that out too.

Prelude Data.Time> birthday
2015-09-30 :: Day
Prelude Data.Time> diffDays birthday today
31 :: Integer

These are probably the most common ways you'll want to calculate with days, but there are a couple more "business-y" ways of doing it. For example, imagine you want to bill a customer a month from now. Do you add 31 days to the current date? Or 30? What if it's January and the next month only has 28 days? The customer will expect to be billed in February, but if you add 30 days to January 31, you'll end up in March!

For that particular situation, we have the addGregorianMonthsClip function. If you use that to add one month to May 31, it will attempt to find June 31, but since that date is invalid it back down to June 30, the closest valid date in June:

Prelude Data.Time> clientRegistered
2015-05-31 :: Day
Prelude Data.Time> addGregorianMonthsClip 1 clientRegistered
2015-06-30 :: Day

You could easily use this to build a list of the dates you want to bill the customer for the next year:

Prelude Data.Time> map (\n -> addGregorianMonthsClip n clientRegistered) [1..12]
[ 2015-06-30, 2015-07-31, 2015-08-31
, 2015-09-30, 2015-10-31, 2015-11-30
, 2015-12-31, 2016-01-31, 2016-02-29
, 2016-03-31, 2016-04-30, 2016-05-31
]

Note how 2016 must be a leap year, because it expects to be able to bill the customer on February 29. Had 2016 not been a leap year, it would have said February 28 instead.

If we add time to these date values, we get universal time.

Universal Time

Universal time, or UTC, is an attempt to standardise and coordinate time across the whole world. Writing code to deal with time is never fun, but whenever you have to do it, I strongly recommend using universal time for all your calculations, and then converting to and from local time only as a last step.

As you will see shortly, universal time is an extension of the Day type you have already learned how to manipulate. This means that what you learned about the Day type can be used when manipulating universal time as well.

Construction

In the time library, universal time is represented through the UTCTime type. It is simply built up from a Day and how many seconds into that day the clock shows.

So we can signify midnight today as

Prelude Data.Time> today
2015-08-30 :: Day
Prelude Data.Time> UTCTime today 0
2015-08-30 00:00:00 UTC :: UTCTime

If we wanted 12:34:56 today then we could do

Prelude Data.Time> UTCTime today (12*60*60 + 34*60 + 56)
2015-08-30 12:34:56 UTC :: UTCTime

Finally, the easiest way to get a UTCTime value is to just get the current universal time!

Prelude Data.Time> getCurrentTime
2015-08-30 10:07:24.923811 UTC :: IO UTCTime

Note that for obvious reasons, this is an I/O action so it needs to be run like any other IO computation.

Calculation

The two most basic forms of calculation with universal time are similar to the ones we saw with dates: adding time, and figuring out the difference between two times.

With UTCTime, we express the time to add as a number of seconds, and the difference between two UTCTimes is also returned as a number of seconds.

When I started writing this tutorial, I ran

Prelude Data.Time> start <- getCurrentTime

and if we look at start now, we see that I started writing at around 8 o'clock universal time:

Prelude Data.Time> start
2015-08-30 08:14:47 UTC :: UTCTime

How many seconds ago was that? Let's find out!

Prelude Data.Time> now <- getCurrentTime
Prelude Data.Time> diffUTCTime now start
12187.929197s :: NominalDiffTime

Apparently that was around 12,200 seconds ago, or 3.4 hours. Don't get too caught up with the NominalDiffTime name. It is just a number representing the seconds that have passed between two points in universal time. For the most part, it can be treated like any other number.

Difftime Calculation

However, you might notice one pecularity. You can't add a regular Integer or Double to a NominalDiffTime, because the compiler will complain that they are of different types. So you might end up here:

Prelude Data.Time> let timePassed = diffUTCTime now start
Prelude Data.Time> timePassed
12187.929197s :: NominalDiffTime
Prelude Data.Time> moreSeconds <- readLn :: IO Integer
572
Prelude Data.Time> moreSeconds
572 :: Integer
Prelude Data.Time> timePassed + moreSeconds

<interactive>:82:14:
    Couldn't match expected type ‘NominalDiffTime’
                with actual type ‘Integer’
    In the second argument of ‘(+)’, namely ‘moreSeconds’
    In the expression: timePassed + moreSeconds

The solution is to convert our regular Integer to a NominalDiffTime, which we do (slightly unintuitively) with realToFrac.

Prelude Data.Time> timePassed + realToFrac moreSeconds
12759.929197s :: NominalDiffTime

The same thing applies if we want to add an Integer (or Double) to a UTCTime:

Prelude Data.Time> addUTCTime moreSeconds now

<interactive>:84:12:
    Couldn't match expected type ‘NominalDiffTime’
                with actual type ‘Integer’
    In the first argument of ‘addUTCTime’, namely ‘moreSeconds’
    In the expression: addUTCTime moreSeconds now

an error which is easily solved by converting with realToFrac.

Prelude Data.Time> addUTCTime (realToFrac moreSeconds) now
2015-08-30 11:47:26.929197 UTC :: UTCTime

Destructuring

Since a UTCTime value is just a regular data type with two fields (one for the date and one for the time of day) it is quite easy to pick it apart and put it together again. Imagine, for example, that we wanted to create a UTCTime value for the same time, but tomorrow.

Prelude Data.Time> UTCTime day time <- getCurrentTime
Prelude Data.Time> UTCTime (addDays 1 day) time
2015-08-31 12:03:54.886849 UTC :: UTCTime

We get the current time and use pattern matching to extract the day and time separately. We then create a new UTCTime value from the "day + 1" and the same time.

I strongly recommend to not manipulate the time part of a UTCTime other than to set it to a value you know is safe. Don't add or subtract hours or minutes from it, because if you accidentally create an invalid value you will get unexpected results:

Prelude Data.Time> UTCTime day 1234567890
2015-08-30 23:59:1234481550 UTC :: UTCTime
Prelude Data.Time> UTCTime day (-1234567890)
1976-07-16 00:28:30 UTC :: UTCTime

If you need to add or subtract hours and minutes, use the addUTCTime function with positive/negative NominalDiffTimes instead, and you'll get the expected behaviour.

It bears repeating: do not just put in an unvalidated NominalDiffTime into a UTCTime constructor. Use addUTCTime instead.

Local Time

This is going to be a short segment! Local time is represented in the time library as the ZonedTime type. A ZonedTime value consists of two parts: a time and a time zone. These can be pattern matched out, but you shouldn't ever touch the time value. If you need to poke at the time, convert to a UTCTime first, and then poke at the UTCTime the way you've already learned.

Extracting the timezone from a ZonedTime value can be useful, if you need the TimeZone value for conversions and the like.

You convert a ZonedTime value into a UTCTime value with the zonedTimeToUTC function. If you have a TimeZone value corresponding to the users timezone, you can convert a UTCTime back to a ZonedTime value with the utcToZonedTime function.

Here's an example of how you can (avoid to) manipulate local time values.

Prelude Data.Time> -- Get my local time
Prelude Data.Time> myTime <- getZonedTime
Prelude Data.Time> myTime
2015-08-30 14:37:49.570802 CEST

Prelude Data.Time> -- Convert my time to universal time
Prelude Data.Time> let utcTime = zonedTimeToUTC myTime
Prelude Data.Time> utcTime
2015-08-30 12:37:49.570802 UTC

Prelude Data.Time> -- Add 45 minutes to the universal time
Prelude Data.Time> let newUtcTime = addUTCTime (45*60) utcTime
Prelude Data.Time> newUtcTime
2015-08-30 13:22:49.570802 UTC

Prelude Data.Time> -- Extract out my timezone from my local time
Prelude Data.Time> let ZonedTime _ myTimezone = myTime
Prelude Data.Time> myTimezone
CEST

Prelude Data.Time> -- Convert the universal time back to my time
Prelude Data.Time> let myNewTime = utcToZonedTime myTimezone newUtcTime
Prelude Data.Time> myNewTime
2015-08-30 15:22:49.570802 CEST

Formatting

Since it's probably a good idea, I'll quickly touch on converting time values to strings.

The basic tool for formatting time is the formatTime function. As arguments, it takes a TimeLocale, which we will mostly ignore; a String with directives for how to format the time; and a t value which is something like a Day, UTCTime or ZonedTime.

If you specify a completely numeric format, the locale doesn't matter at all, so we will just use the defaultTimeLocale provided. Keep in mind though that if you want to represent the time with non-English words, like "Lundi 8 Sept 2014", then you'll have to modify the default locale with the correct translations.

The specification for the time format directives is on the Haddock documentation for the formatTime function. Here's an example on how to use it:

Prelude Data.Time> formatTime defaultTimeLocale "%T, %F (%Z)" myTime
"14:37:49, 2015-08-30 (CEST)" :: String

There!

So that's it, I think. The time library can be a bit overwhelming if you are used to time libraries in other languages. The reason is the safety. The time library is really solidly built, and when used correctly, you can reap the benefits of that.

The key to good usage is to stick to universal time for as much as you can, and only do local time when you are interfacing with users. As soon as your program gets its hands on a local time value, convert it to universal time.