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
min, sec) =
mkUTCTime (year, mon, day) (hour, UTCTime (fromGregorian year mon day)
TimeOfDay hour min sec))
(timeOfDayToTime (
> 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 TimeLocale
s. You can define a handy alias that always uses the default locale:
formatTime_ :: FormatTime t => String -> t -> String
= formatTime defaultTimeLocale formatTime_
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
Before you close that tab...
Want to write practical, production-ready Haskell? Tired of broken libraries, barebones documentation, and endless type-theory papers only a postdoc could understand? I want to help. Subscribe below and you'll get useful techniques for writing real, useful programs straight in your inbox.
Absolutely no spam, ever. I respect your email privacy. Unsubscribe anytime.