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.
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.
Dayis the type representing a date
fromGregorianValid :: Integer -> Int -> Int -> Maybe Daycreates a date value, by taking a year–month–day triple and returning
Nothingif the date is invalid
addDays :: Integer -> Day -> Daycan add and subtract a number of days to a date value; supply a negative
diffDays :: Day -> Day -> Integergives you the number of days that lie between two dates
addGregorianMonthsClip :: Integer -> Day -> Daymoves 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
UTCTimeis 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 UTCTimereturns the current universal time
addUTCTime :: NominalDiffTime -> UTCTime -> UTCTimelets 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 -> NominalDiffTimecomputes the number of seconds that have passed between the two points in time specified
NominalDiffTimeis a regular number, but can not be combined with
Doubleand other numbers without conversion
realToFracconverts any number to a
- You can pattern match on a
UTCTimeto extract the date component and do maths on it separately
ZonedTimeis the type of a local time value
zonedTimeToUTC :: ZonedTime -> UTCTimeconverts a local time value to universal time
utcToZonedTime :: TimeZone -> UTCTime -> ZonedTimeconverts a universal time value to local time, given a timezone for the local time
TimeZoneis a value representing the users timezone; it can be pattern matched out from a local time value
getZonedTime :: IO ZonedTimegets the current local time for the user
formatTime :: FormatTime t => TimeLocale -> String -> t -> Stringformats a date or time value according to the format string and locale specified
TimeLocaleis a value that contains, among other things, translations of the weekday names to the users language
defaultTimeLocalecontains the English names of weekdays among other things; can be modified if you don't want to create a time locale from scratch
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.
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
Universal time values, represented with two types: the
UTCTimetype represents a date and time combination ("25 april, 2009, 13:52:31") and the
NominalDiffTimetype represents a difference between two
Local time values, represented by the
ZonedTimetype 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
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.
Constructing a date value from a year–month–day triple is easy. The function
fromGregorianValid is your friend. It returns a
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
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 …)
If you for some reason want to convert a
Day value back to
a year–month–day triple, you do that with the
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
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.
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, 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.
In the time library, universal time is represented through the
UTCTime type. It is simply built up from a
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
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.
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.
However, you might notice one pecularity. You can't add a regular
Double to a
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
NominalDiffTime, which we do (slightly unintuitively) with
Prelude Data.Time> timePassed + realToFrac moreSeconds 12759.929197s :: NominalDiffTime
The same thing applies if we want to add an
Double) to a
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
Prelude Data.Time> addUTCTime (realToFrac moreSeconds) now 2015-08-30 11:47:26.929197 UTC :: UTCTime
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
It bears repeating: do not just put in an unvalidated
NominalDiffTime into a
UTCTime constructor. Use
This is going to be a short segment! Local time is represented in the
time library as the
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
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
corresponding to the users timezone, you can convert a
UTCTime back to a
ZonedTime value with the
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
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
As arguments, it takes a
TimeLocale, which we will mostly ignore;
String with directives for how to format the time; and a
t value which is something like a
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
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
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.