A cheatsheet to the time library

September 16, 2019
« Previous post   Next post »

In a way, date and time handling is one place where Haskell's strict type system shines. Handling timezones correctly, along with the distinction between timestamps and calendar dates is an endless source of bugs. Being precise about which exact type of time data your program takes in and spits out can completely obliviate that problem.

The de-facto standard date and time library in Haskell, time, however, can be a little obtuse to get started with. I always feel like I have to reread the documentation every time I need to write date-related code. It's not even obvious how to do the most common operation, getting the current time. Producing things like the current month, day of the week, and so on, requires a surprising number of type conversions. Here's a cheatsheet for the most common use cases for the time library.

All examples were testing on time version 1.8.0.2.

Importing and using

Add to your package.yaml/cabal file:

dependencies:
  - time

In modules where you need to work with date/time data:

import Data.Time

The "primitive" type in the time library is UTCTime. If you need to do anything involving the current time, it will most likely involve some conversion from/to this type. As the name suggests, it's a timestamp in the UTC timezone. The exact accuracy will depend on your operating system, but it will be accurate to at least the second and is capable of being accurate up to the picosecond.

Getting the current time

getCurrentTime :: IO UTCTime

data UTCTime = UTCTime
  { utctDay     :: Day       -- calendar day
  , utctDayTime :: DiffTime  -- seconds from midnight
  }

λ> now <- getCurrentTime
λ> now
>>> 2019-08-31 05:14:37.537084021 UTC

Working with dates

If all you need is calendar day-level fidelity, then you want the Day type. Note that the UTCTime type has a utctDay accessor for getting the calendar day, if you need the current date.

toGregorian :: Day -> (Integer, Int, Int)  -- year, month, day of month
fromGregorian :: Integer -> Int -> Int -> Day  -- year, month, day of month

λ> today <- utctDay <$> getCurrentTime
λ> today
>>> 2019-08-31

λ> toGregorian today
>>> (2019, 8, 31)

Working with time of day

If you're instead looking for an intra-day timestamp, regardless of which particular calendar day it is, you'll want the fittingly-named TimeOfDay type.

timeToTimeOfDay :: DiffTime -> TimeOfDay
timeOfDayToTime :: TimeOfDay -> DiffTime

data TimeOfDay = TimeOfDay
  { todHour :: Int
  , todMin  :: Int
  , todSec  :: Pico
  }

midnight :: TimeOfDay
midday :: TimeOfDay

λ> liftA3 (,,) todHour todMin todSec $ midnight
>>> (0, 0, 0.000000000000)

λ> liftA3 (,,) todHour todMin todSec $ midday
>>> (12, 0, 0.000000000000)

There don't seem to be any functions for adding/diffing TimeOfDay values. You can either construct updated TimeOfDay values directly, or if you'd rather not deal with rollover manually, do your manipulations on UTCTime values and convert to a TimeOfDay once you need to output or display something.

Working with timezones and ZonedTime

The main types for working with local times are the ZonedTime and TimeZone types. As you might expect, the TimeZone type represents an offset from UTC; a ZonedTime is essentially a timestamp paired with a TimeZone.

Given a TimeZone and a UTCTime, you can convert into a ZonedTime. Since most of the time you'll just be working in the current (i.e. system) timezone, time provides some convenience functions that don't require you to provide a timezone.

getZonedTime :: IO ZonedTime
  -- like `getCurrentTime`, but in local timezone

utcToLocalZonedTime :: UTCTime -> IO ZonedTime

utcToZonedTime :: TimeZone -> UTCTime -> ZonedTime
zonedTimeToUTC :: ZonedTime -> UTCTime

λ> now <- getZonedTime
λ> now
>>> 2019-08-30 20:53:05.860397879 PDT

How do you actually get a TimeZone object? There are functions for getting the current timezone, but the ability to get a specific timezone is oddly anemic.

getCurrentTimeZone :: IO TimeZone
utc :: TimeZone

λ> utcToZonedTime <$> getCurrentTimeZone <*> getCurrentTime >>= print
>>> 2019-08-30 20:57:29.770819335 PDT

λ> utcToZonedTime utc <$> getCurrentTime >>= print
>>> 2019-08-31 03:57:29.770819335 UTC

TimeZone has a Read instance which can parse certain timezone abbreviations. It's rather unreliable, though, and will silently produce UTC if it can't figure out the timezone, so be careful with it.

λ> timeZoneMinutes $ read "PDT"
>>> -420

λ> timeZoneMinutes $ read "EST"
>>> -300

λ> timeZoneMinutes $ read "JST"  -- Japan Standard Time
>>> 0  -- silently fails!

λ> timeZoneMinutes $ read "+0900"
>>> 540

EDIT: Neil Mayhew informed me that if you want to work in non-system timezones, you should look at the tz package. Thanks, Neil!

Getting subcomponents of local times

We've only seen how to get the year/month/day/hour/etc. starting from a UTCTime, but what if we want those values from a ZonedTime? We can't convert to a UTCTime first; that would defeat the purpose. Thankfully, if we pop open the fields of ZonedTime, we get a LocalTime object:

data ZonedTime = ZonedTime
  { zonedTimeToLocalTime :: LocalTime
  , zonedTimeZone        :: TimeZone
  }

data LocalTime = LocalTime
  { localDay       :: Day
  , localTimeOfDay :: TimeOfDay
  }

λ> getCurrentTime >>= print
>>> 2019-08-31 04:10:44.88163287 UTC

λ> getZonedTime >>= print
>>> 2019-08-30 21:10:44.88163287 PDT

λ> do now <- getZonedTime
      print $ toGregorian $ localDay $ zonedTimeToLocalTime now
>>> (2019,8,30)

Creating UTCTime values directly

Since this requires creating a bunch of intermediate values, here's a useful helper function:

import Data.Fixed

mkUTCTime :: (Integer, Int, Int)
          -> (Int, Int, Pico)
          -> UTCTime
mkUTCTime (year, mon, day) (hour, min, sec) =
  UTCTime (fromGregorian year mon day)
          (timeOfDayToTime (TimeOfDay hour min sec))

λ> mkUTCTime (2019, 9, 1) (15, 13, 0)
>>> 2019-09-01 15:13:00 UTC

Formatting dates and times

time provides a single function, formatTime, for formatting all time types into text. It takes a format string as its second argument for determining which components of the time type to show.

formatTime :: FormatTime t => TimeLocale -> String -> t -> String
defaultTimeLocale :: TimeLocale

-- All of the time types implement this class, so you could pass
-- `formatTime` e.g. a UTCTime, a ZonedTime, whatever.
class FormatTime t where
  {- ... -}

λ> now <- getCurrentTime
λ> formatTime defaultTimeLocale "%Y-%m-%d" now
>>> "2019-08-31"
λ> formatTime defaultTimeLocale "%H:%M:%S" now
>>> "04:10:44"

Here are the format directives you probably care about:

  • %y: year, 2-digit abbreviation
  • %Y: year, full
  • %m: month, 2-digit
  • %d: day of month, 2-digit
  • %H: hour, 2-digit, 24-hour clock
  • %I: hour, 2-digit, 12-hour clock
  • %M: minute, 2-digit
  • %S: second, 2-digit
  • %p: AM/PM
  • %z: timezone offset
  • %Z: timezone name

If you don't want the zero-padding of the specific component, you can add a dash between the percent sign and the directive, e.g. a format string of "%H:%M" would give "04:10" but a format string of "%-H:%M" would give "4:10".

The full list of directives is listed in the documentation for formatTime.

Formatting localization

If you need to output formatted times in languages other than English, formatTime provides some rudimentary support for that through its TimeLocale parameter. We passed in defaultTimeLocale above, but you can create your own as well. It's essentially a mapping for the non-numeric components, like month names, day-of-week names, and symbols for AM/PM.

For example, we could create a locale for formatting Japanese days-of-week like so:

data TimeLocale = TimeLocale
  { wDays  :: [(String, String)]
    -- weekdays
    -- full name (Sunday), then abbreviation (Sun)
    -- starts from Sunday
  , months :: [(String, String)]
    -- full name (Janurary), then abbreviation (Jan)
  , amPm   :: (String, String)
  }

λ> jpLocale = defaultTimeLocale
     { wDays =
       [ ("日曜日", "日")
       , ("月曜日", "月")
       , ("火曜日", "火")
       , ("水曜日", "水")
       , ("木曜日", "木")
       , ("金曜日", "金")
       , ("土曜日", "土")
       ]
     }

λ> putStrLn $ formatTime defaultTimeLocale "%Y-%m-%d (%a)" now
>>> 2019-08-31 (Sat)
λ> putStrLn $ formatTime jpLocale "%Y-%m-%d (%a)" now
>>> 2019-08-31 (土)

If you're just working with English dates, you probably don't need to mess with TimeLocales. You can define a handy alias that always uses the default locale:

formatTime_ :: FormatTime t => String -> t -> String
formatTime_ = formatTime defaultTimeLocale

Parsing dates and times

For taking date strings as input, time provides the parseTimeM function. As the name suggests, it gives the output time value wrapped in a monad of your choice, although in practice you'll probably only use Maybe. It uses the same format string format as formatTime.

parseTimeM :: (Monad m, ParseTime t)
           => Bool        -- ^ surrounding whitespace okay?
           -> TimeLocale
           -> String      -- ^ format string
           -> String
           -> m t

-- Again, all of the time types implement this class, so you
-- can parse into any type you need.
class ParseTime t where
  {- ... -}

λ> parseTimeM True defaultTimeLocale "%Y-%m-%d" "2019-08-31" :: Maybe Day
>>> Just 2019-08-31
λ> parseTimeM True defaultTimeLocale "%Y-%m-%d" "asdf" :: Maybe Day
>>> Nothing

Note that before time version 1.9, the constraint is Monad m, not MonadFail m; this can bite you if you try to use Either! Check which version of the time library you're using if you intend to rely on this function.

Again, it would likely make sense to make an alias for parseTimeM to suit your own situation; say, using the default locale and restricting the monad to Maybe.

Performing arithmetic on time data

If you need to add and subtract times, you have two options based on whether you need to do arithmetic at day-level fidelity or timestamp-level fidelity.

If you want to do arithmetic on days, months, and years, work within the Day type and use the addX functions defined in Data.Time.Calendar.

addDays :: Integer -> Day -> Day
addGregorianMonthsClip :: Integer -> Day -> Day
addGregorianYearsClip :: Integer -> Day -> Day

λ> today <- utctDay <$> getCurrentTime
λ> today
>>> 2019-08-31

λ> addDays 3 today
>>> 2019-09-03

λ> addGregorianMonthsClip 6 today
>>> 2020-02-29

λ> addGregorianYearsClip 1 today
>>> 2020-08-31

There's also addGregorianMonthsRollOver and addGregorianYearsRollOver functions in addition to the addXXXClip functions. They increment (or decrement) the year/month, then if the day of month would be out of bounds for that month, they 'roll over' the extra days into the next month.

addGregorianMonthsRollOver :: Integer -> Day -> Day
addGregorianYearsRollOver :: Integer -> Day -> Day

λ> today
>>> 2019-08-31

λ> addGregorianMonthsRollOver 1 today
>>> 2019-10-01  -- since September has 30 days,
                -- the 31st day gets rolled over

You almost certainly want to use the clipping functions.

If you want to do arithmetic on hours, minutes, and seconds, work within the UTCTime type and use the functions in Data.Time.Clock.

data NominalDiffTime
  -- unit is 1 second
  -- implements Num, Fractional

addUTCTime :: NominalDiffTime -> UTCTime -> UTCTime
nominalDay :: NominalDiffTime  -- 24 hours

λ> now <- getCurrentTime
λ> now
>>> 2019-08-31 05:14:37.537084021 UTC

λ> addUTCTime 3600 now
>>> 2019-08-31 06:14:37.537084021 UTC

λ> addUTCTime (-nominalDay) now
>>> 2019-08-30 05:14:37.537084021 UTC

Remember that you can do day/month/year arithmetic on UTCTimes by mapping over their inner Day values.

Working with POSIX timestamps

If you want to work with POSIX timestamps (i.e. seconds since the Unix epoch), you're looking for the functions in Data.Time.Clock.POSIX.

type POSIXTime = NominalDiffTime

posixSecondsToUTCTIme :: POSIXTime -> UTCTime
utcTimeToPOSIXSeconds :: UTCTime -> POSIXTime
getPOSIXTime :: IO POSIXTime

λ> nowPOSIX <- getPOSIXTime
λ> nowPOSIX
>>> 1591123167.308290018s

λ> posixSecondsToUTCTime 0
>>> 1970-01-01 00:00:00 UTC

Generally, you should be doing all your calculations in UTCTime, only converting to "derivative" types like Day, TimeOfDay, or ZonedTime if you (a) need the subcomponents of them like the current day of the week, or (b) for outputting to some human-visible result.

Since Haskell is pure, you'll have to be careful within date-related code about how you actually get timestamps. Getting the current time clearly isn't a pure operation, so some thought is required to avoid 'infecting' all of your code with IO when working with time. But that's a topic for another time. With just this, you should be able to write perfectly useful datetime code in Haskell.

Have fun working with time!

Found this useful? Got a comment to make? Talk to me!


You might also like

« Previous post   Next post »

Before you close that tab...