time-diff

2018-04-12

I live in Bothell, Washington. I want to call my mother in Haworth, West Yorkshire. But what time is it there?

That’s not a terribly difficult question to get an answer for with Google. However, there is a much more interesting and nuanced question:

What was the time difference between Bothell, Washington and Haworth, West Yorkshire over time?

Overview

To answer this, we’ll use Haskell, of course. Specifically, we’re going to use the following:

I want to generate a list of every day in a given year along with the effective time zones in both Bothell and Haworth and the corresponding difference in hours and minutes between the times (at midnight) in these two locations.

Step 1: A sequence of UTC days

We can make a single UTC day as follows:

let startDay = fromGregorian 2018 1 1
view raw Snippet0.hs hosted with ❤ by GitHub

That’s 1 January 2018, just in case you were wondering.

I scratched my head thinking about how to generate a sequence for a little while. At first I was planning to map the fromGregorian function over various sequences of numbers. However, this requires knowledge of the number of days in each month in each year and lots of similar nastiness. How else to do this? Well, fortunately for me, time also exposes an addDays function. With this, we should be able to generate a list of calendar days by mapping some function of this function and our startDay over a list of numbers:

map (flip addDays startDay) [0..]
view raw Snippet1.hs hosted with ❤ by GitHub

or

map (\i -> addDays i startDay) [0..]
view raw Snippet2.hs hosted with ❤ by GitHub

These two formulations are equivalent, the first being the point-free style version of the latter. pointfree.io is your friend for exploring automatic conversions of Haskell expressions into point-free form.

Update: Let’s use Enum instead.

It turns out that Day has an instance for the Enum type class which makes generating a sequence of days trivial:

[startDay..]
view raw Snippet3.hs hosted with ❤ by GitHub

Thanks to lgastako for this tip.

Step 2: Time zone in effect at a given UTC time

The tz package exposes functions like timeZoneForPOSIX and diffForPOSIX which sound like they may do what we need. Unfortunately, both functions deal in terms of Int64 values and the documentation for the package does not explain what this Int64 is directly. So, I had to take a look at the code. After doing that, I figured out that I need the Int64 that is the result type of the utcTimeToInt64 function in the Data.Time.Zones.Internal module.

This leads to the following function:

tzOffsetInfo :: TZ -> UTCTime -> (Minutes, TimeZone)
tzOffsetInfo tz utcTime =
let posixTime = utcTimeToInt64 utcTime
tzInEffect = timeZoneForPOSIX tz posixTime
offset = Minutes $ (diffForPOSIX tz posixTime) `div` 60
in (offset, tzInEffect)
view raw tzOffsetInfo.hs hosted with ❤ by GitHub

Minutes is a newtype wrapper around Int.

This function takes a TZ (which is a time zone database entry for a given geographical location), a UTCTime (which we can derive from our Gregorian Day from above) and returns the time difference from UTC in minutes as well as the name of the time zone in effect (encoded using the TimeZone) type.

Since the hidden utcTimeToInt64 function is critical to interoperability between tz and time (I think), I’ll probably file an issue against the package and submit a pull request to make this function part of the public API. If I get time, of course. That was a terrible pun.

The full program

Now, all we need to do is apply our tzOffsetInfo function to a stream of UTCTime instances deriving from our stream of Day instances and print the results out. I present the full program here:

{-# LANGUAGE TemplateHaskell #-}
module Main (main) where
import Data.Foldable (for_)
import Data.Time (TimeZone, UTCTime(..), fromGregorian)
import Data.Time.Zones (diffForPOSIX, timeZoneForPOSIX)
import Data.Time.Zones.Internal (utcTimeToInt64)
import Data.Time.Zones.TH (includeTZFromDB)
import Data.Time.Zones.Types (TZ)
import Text.Printf (printf)
newtype Minutes = Minutes Int
instance Show Minutes where
show (Minutes n) = printf "%d:%02d" (n `div` 60) (n `mod` 60)
tzHaworth :: TZ
tzHaworth = $(includeTZFromDB "Europe/London")
tzBothell :: TZ
tzBothell = $(includeTZFromDB "America/Los_Angeles")
tzOffsetInfo :: TZ -> UTCTime -> (Minutes, TimeZone)
tzOffsetInfo tz utcTime =
let posixTime = utcTimeToInt64 utcTime
tzInEffect = timeZoneForPOSIX tz posixTime
offset = Minutes $ (diffForPOSIX tz posixTime) `div` 60
in (offset, tzInEffect)
minutesDiff :: Minutes -> Minutes -> Minutes
minutesDiff (Minutes a) (Minutes b) = Minutes (a - b)
main :: IO ()
main = do
let startDay = fromGregorian 2018 1 1
for_ (take 365 [startDay..]) $ \day -> do
let time = UTCTime day 0
(offsetBothell, tzInEffectBothell) = tzOffsetInfo tzBothell time
(offsetHaworth, tzInEffectHaworth) = tzOffsetInfo tzHaworth time
diff = offsetBothell `minutesDiff` offsetHaworth
putStrLn $
printf
"%s: %s vs %s %s"
(show day)
(show tzInEffectBothell)
(show tzInEffectHaworth)
(show diff)
view raw Main.hs hosted with ❤ by GitHub

There, that was a quick introduction to time and time zones in Haskell.

Related posts

Update

Tags

Haskell
Time zone
Bothell
Haworth

Content © 2025 Richard Cook. All rights reserved.