@@ -0,0 +1,85 @@
1+module Data.Time.FastSeries
2+ ( TimeZoneSeries
3+ , toTimeZoneSeries
4+ , fromTimeZoneSeries
5+ , timeZoneFromSeries
6+ , timeZoneFromSeriesLocal
7+ , utcToLocalTime'
8+ , localTimeToUTC'
9+ ) where
10+
11+import Data.Map.Strict (Map)
12+import qualified Data.Map as Map
13+import Data.Time
14+import qualified Data.Time.LocalTime.TimeZone.Series as Series
15+
16+
17+-- | Contains the same data as 'Series.TimeZoneSeries' but precomputes some data
18+-- for faster conversion.
19+data TimeZoneSeries = TimeZoneSeries
20+ { fallback :: !TimeZone
21+ , transitionsUTC :: !(Map UTCTime TimeZone)
22+ -- ^ UTC -> Local wants to know the last transition that happened before a
23+ -- given 'UTCTime'
24+ , transitionsLocal :: !(Map LocalTime TimeZone)
25+ -- ^ Local -> UTC wants to know the largest transition @t@ into timezone
26+ -- @tz@ such that @t <= localTimeToUTC tz lt@ where @lt@ is a given
27+ -- 'LocalTime'. For a given TZ and valid time arguments, 'localTimeToUTC'
28+ -- and 'utcToLocalTime' are inverses. Hence this is equivalent to
29+ -- @utcToLocalTime t tz <= lt@.
30+ }
31+
32+toTimeZoneSeries :: Series.TimeZoneSeries -> TimeZoneSeries
33+toTimeZoneSeries
34+ Series.TimeZoneSeries { Series.tzsTimeZone, Series.tzsTransitions }
35+ = TimeZoneSeries
36+ { fallback = tzsTimeZone
37+ , transitionsUTC = Map.fromDescList tzsTransitions
38+ , transitionsLocal = Map.fromList
39+ [(utcToLocalTime tz u, tz) | (u, tz) <- tzsTransitions]
40+ }
41+
42+fromTimeZoneSeries :: TimeZoneSeries -> Series.TimeZoneSeries
43+fromTimeZoneSeries TimeZoneSeries { fallback, transitionsUTC }
44+ = Series.TimeZoneSeries
45+ { Series.tzsTimeZone = fallback
46+ , Series.tzsTransitions = Map.toList transitionsUTC
47+ }
48+
49+timeZoneFromSeries :: TimeZoneSeries -> UTCTime -> TimeZone
50+timeZoneFromSeries TimeZoneSeries { fallback, transitionsUTC } t
51+ = maybe fallback snd $ Map.lookupLE t transitionsUTC
52+
53+-- | See caveat on 'localTimeToUTC''.
54+timeZoneFromSeriesLocal :: TimeZoneSeries -> LocalTime -> TimeZone
55+timeZoneFromSeriesLocal TimeZoneSeries { fallback, transitionsLocal } lt
56+ = maybe fallback snd $ Map.lookupLE lt transitionsLocal
57+
58+utcToLocalTime' :: TimeZoneSeries -> UTCTime -> LocalTime
59+utcToLocalTime' tzsf t = utcToLocalTime tz t
60+ where !tz = timeZoneFromSeries tzsf t
61+
62+-- | This conversion is not actually always well defined.
63+--
64+-- At the moment of switching to summer time, there is a range of 'LocalTime's
65+-- that are skipped. For these, 'Series.isValidLocalTime' returns 'False'. This
66+-- function converts them to 'UTCTime' as if the switch didn't happen.
67+--
68+-- Around the moment of switching away from summer time, there is a range of
69+-- 'LocalTime's that happen twice: first before the switch, then once again
70+-- after the switch. For these, 'Series.isRedundantLocalTime' returns 'True'.
71+-- In such ambiguous cases, this function always picks the occurrence after the
72+-- switch.
73+--
74+-- The handling of these corner cases matches exactly to that of
75+-- 'Series.localTimeToUTC''
76+--
77+-- This means the equation
78+-- @lt == utcToLocalTime' tzsf (localTimeToUTC' tzsf lt)@ does not hold for @lt@
79+-- that were skipped. And the equation
80+-- @t == localTimeToUTC' tzsf (utcToLocalTime' tzsf t)@ does not hold for @t@
81+-- that come just before switching awya from summer time. For other arguments
82+-- these equations are assumed to hold.
83+localTimeToUTC' :: TimeZoneSeries -> LocalTime -> UTCTime
84+localTimeToUTC' tzsf lt = localTimeToUTC tz lt
85+ where !tz = timeZoneFromSeriesLocal tzsf lt