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 datefromGregorianValid :: Integer -> Int -> Int -> Maybe Day
creates a date value, by taking a year–month–day triple and returningNothing
if the date is invalidaddDays :: Integer -> Day -> Day
can add and subtract a number of days to a date value; supply a negativeInteger
for subtractiondiffDays :: Day -> Day -> Integer
gives you the number of days that lie between two datesaddGregorianMonthsClip :: Integer -> Day -> Day
moves a date a number of months forward or backward, adjusting it when necessary to stay within short monthstoGregorian :: 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 daygetCurrentTime :: IO UTCTime
returns the current universal timeaddUTCTime :: 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 argumentdiffUTCTime :: 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 withInteger
,Double
and other numbers without conversion realToFrac
converts any number to aNominalDiffTime
- 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 valuezonedTimeToUTC :: ZonedTime -> UTCTime
converts a local time value to universal timeutcToZonedTime :: 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 theNominalDiffTime
type represents a difference between twoUTCTime
s.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 UTCTime
s 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
NominalDiffTime
s 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
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 ZonedTime
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.