Recently I had to write end to end tests for my personal web project. I decided to write tests in Haskell so I don’t need to debug runtime errors in tests and I can also utilize popular property testing libraries like QuickCheck and Hedgehog to test certain properties about each page and endpoint.
Many of the requests to the web application involved generating and sending timestamps in the universal ISO 8601 format. I’m documenting here the functions I’ve discovered for handling date and time in Haskell so I can refer to this later when required and also might help others.
time
package
time package is the de-facto standard for date/time representation and manipulation in Haskell. The types in the package are designed in such a way, that it will not allow meaning less operations. It is not uncommon for developers coming from mainstream programming languages like C#, JavaScript or Python to get frustated with time
package.
The reason date and time is easy to deal with in those languages is because they allow invalid representations of date/time. For example, C# has only DateTime
and TimeSpan
types for all kinds of representation. Let’s say you want to build an alarm app and would like to represent daily wake up time (say 7am), you still have to use DateTime
or TimeSpan
. Neither of that is accurate representation. Why? Because you’ve no clue if DateTime
is your birthday in local timezone or an alarm time or end of the world in UTC. It depends on the context of the value’s usage.
In fact that is why third party libraries like NodaTime or Joda-Time exist for C# and Java respectively to replace the primitive and error prone types.
No wonder, time
package follows Haskell’s philosophy of correctness through types.
Types
Here are the most commonly used date and time types provided by the time
package under Data.Time
module.
Type | Description |
---|---|
UTCTime |
An instant in UTC representing both date and time. This is the most common type you would want to use. |
NominalDiffTime |
Length of time or interval between two UTCTime , as measured by UTC. |
Day |
Represents only date without time and TimeZone info. |
TimeOfDay |
Represents only time. |
LocalTime |
Date with Time (Combination of Day and TimeOfDay ) without TimeZone info. |
TimeZone |
Whole number of minutes offset from UTC, together with a name. |
ZonedTime |
Date with Time (Combination of Day and TimeOfDay ) with TimeZone info. |
These are only basic types. There are even richer types that you can find here from the official quick start guide.
Get current Local Time
Most of the date and time functions in Haskell are pure functions. But getting current time in a function makes it non-deterministic and not referentially transparent. So getZonedTime
produces an IO monad with the type IO ZonedTime
.
getZonedTime :: IO ZonedTime
is what you need to get the current local time along with local timezone information.
import Data.Time
main :: IO ()
= do
main <- getZonedTime
now print now -- 2021-07-06 23:57:48.187103629 IST
Get current Epoch (Unix) time
Epoch is an Unix time standard where time is represented as seconds elapsed since 1970-01-01 00:00 UTC. This format is easy to parse and transmit over wire since it is basically an integer. getPOSIXTime :: IO POSIXTime
from Data.Time.Clock.POSIX
can be used to get current epoch time.
import Data.Time.Clock.POSIX
main :: IO ()
= do
main <- getPOSIXTime
epochNow print $ round epochNow -- 1625594110
Get current UTC Time
getCurrentTime :: IO UTCTime
is what you need to get the current UTC time.
import Data.Time
main :: IO ()
= do
main <- getCurrentTime
utcNow print utcNow -- 2021-07-06 18:28:51.693103262 UTC
Add to UTCTime
Arithmetic operations on UTCTime involves additional type called NominalDiffTime
which is basically an integer type that reperesents length of time (for example, 3000 seconds). Let’s say you want to add one hour to the UTCTime, first you’ve to construct NominalDiffTime
. Fortunately, NominalDiffTime
supports Num
instance which means you can construct an instance using fromInteger
. fromInteger
treats given argument as seconds and produces a NominalDiffTime
instance which can then be used to add to UTCTime.
addUTCTime
function takes a UTCTime
and NominalDiffTime
and evaluates to the UTCTime
with the time added.
oneHour :: NominalDiffTime
= fromInteger 3600 -- secs
oneHour
addOneHour :: UTCTime -> UTCTime
= addUTCTime oneHour x addOneHour x
Subtract from UTCTime
Just negate
the NominalDiffTime
to subtract.
oneHour :: NominalDiffTime
= fromInteger 3600 -- secs
oneHour
subtractOneHour :: UTCTime -> UTCTime
= addUTCTime (negate oneHour) x subtractOneHour x
Formatting
Displaying or sending date/time over wire involves converting the type into a string format. So time
package provides these facilities through FormatTime
typeclass. Most of the date/time types support FormatTime
instance which give us a function called formatTime
to produce a string in the desired format.
formatTime:: FormatTime t
=> TimeLocale -- Type that provides functionalities to produce name of weekday, month, AM/PM symbols, timezones etc. in a particular locale. We have also predefined `TimeLocale` such as `defaultTimeLocale` for English.
-> String -- Format string like `"%H:%M"` or `"%Y-%m-%d"`. Complete specification is [documented here](https://hackage.haskell.org/package/time-1.12/docs/Data-Time-Format.html).
-> t -- One of the date/time types such as `UTCTime` or `LocalTime`.
-> String -- Return the formatted string
Example usage
import Data.Time
printTime :: UTCTime -> String
= formatTime defaultTimeLocale "%Y-%m-%d %H:%M" x
printTime x -- produces "2021-07-05 14:40"
Formatting to ISO8601
Most applications need to serialize date/time values to the standard ISO8601 format. Though you can use the regular formatTime
function, it involves more ceremony. Luckily, this facility is already available through Format
typeclass from the Data.Time.Format.ISO8601
module.
You can use either
formatShow :: Format t -> t -> String
or
formatShowM :: Format t -> t -> Maybe String
function depending on the level of type safety you need.
Example usage
formatUTCToISO8601 :: UTCTime -> String
= formatShow iso8601Format x -- produces "2021-07-05T14:40:25.436865727Z" formatUTCToISO8601 x
Note: ISO8601 string can be generated from not just UTCTime
but also from Day
, LocalTime
, ZonedTime
or any other date/time types. Because all these types comes with Format t
instance.
Parsing
Just like formatting, date/time types can be constructed from string representations. It’s not uncommon for applications to construct Date type from a string like “2021-07-15”. This ability comes from a typeclass called ParseTime
which all common date/time types implement. Use the parseTimeM
function to construct a type from string.
parseTimeM:: (MonadFail m, ParseTime t)
=> Bool -- Accept leading and trailing whitespace?
-> TimeLocale -- Time locale like defaultTimeLocale
-> String -- Format string like "%Y-%-m-%-d"
-> String -- Input string like "2010-3-04"
-> m t -- Return the time value, or fail if the input could not be parsed using the given format.
Example usage
stringToDay :: String -> Maybe Day
= parseTimeM True defaultTimeLocale "%Y-%m-%d %H:%M" "2020-09-04 12:30" :: Maybe LocalTime
stringToDay s -- evaluates to Just 2020-09-04 12:30:00
Parsing from ISO8601
If you have a ISO8601 timestamp as a string, it can be easily converted to UTCTime
through iso8601ParseM
function from Data.Time.Format.ISO8601
module.
iso8601ParseM:: (MonadFail m, ISO8601 t)
=> String -- ISO8601 string. Example: "2021-07-05T14:40:25.436865727Z"
-> m t -- Return a monad that supports MonadFail. Example: Maybe
Example usage
> import Data.Time.Format.ISO8601
> iso8601ParseM "2021-07-05T14:40:25.436865727Z" :: Maybe UTCTime -- produces Just 2021-07-05 14:40:25.436865727 UTC
Hope this helped you. If you need guide to even more advanced functionalities, please check out these excellent blog posts.
https://two-wrongs.com/haskell-time-library-tutorial
https://www.williamyaoh.com/posts/2019-09-16-time-cheatsheet.html
References