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 UTCTime 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 (

-> 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 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.