-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathTAI.hs
More file actions
358 lines (326 loc) · 13 KB
/
TAI.hs
File metadata and controls
358 lines (326 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
#include "thyme.h"
#if HLINT
#include "cabal_macros.h"
#endif
#define TAIUTCDAT @<http://maia.usno.navy.mil/ser7/tai-utc.dat tai-utc.dat>@
-- | <https://en.wikipedia.org/wiki/International_Atomic_Time International Atomic Time>
-- (TAI) and conversion to/from UTC, accounting for leap seconds.
module Data.Thyme.Clock.TAI
( AbsoluteTime
, taiEpoch
, TAIUTCMap (..)
, TAIUTCRow (..)
, absoluteTime
, absoluteTime'
, utcDayLength
, parseTAIUTCRow
, makeTAIUTCMap
, parseTAIUTCDAT
-- * Compatibility
, addAbsoluteTime
, diffAbsoluteTime
, utcToTAITime
, taiToUTCTime
) where
import Prelude
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative
import Data.Monoid (mempty)
#endif
import Control.DeepSeq
import Control.Lens
import Control.Monad
import Data.AffineSpace
import Data.Attoparsec.ByteString.Char8 ((<?>))
import qualified Data.Attoparsec.ByteString.Char8 as P
import qualified Data.ByteString as S
import Data.Data
import Data.Hashable
import Data.Ix
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Thyme.Calendar
import Data.Thyme.Clock.Internal
import Data.Thyme.Format.Internal (indexOf)
import Data.Thyme.Internal.Micro
import Data.Thyme.LocalTime
#if __GLASGOW_HASKELL__ == 704
import qualified Data.Vector.Generic
import qualified Data.Vector.Generic.Mutable
#endif
import Data.Vector.Unboxed.Deriving
import Data.VectorSpace
import GHC.Generics (Generic)
import System.Random (Random)
import Test.QuickCheck
-- | <https://en.wikipedia.org/wiki/International_Atomic_Time Temps Atomique International>
-- (TAI). Note that for most applications 'UTCTime' is perfectly sufficient,
-- and much more convenient to use.
--
-- Internally this is the number of seconds since 'taiEpoch'. TAI days are
-- exactly 86400 SI seconds long.
newtype AbsoluteTime = AbsoluteTime DiffTime deriving (INSTANCES_MICRO)
derivingUnbox "AbsoluteTime" [t| AbsoluteTime -> DiffTime |]
[| \ (AbsoluteTime a) -> a |] [| AbsoluteTime |]
instance Show AbsoluteTime where
{-# INLINEABLE showsPrec #-}
showsPrec p tai = showsPrec p lt . (++) " TAI" where
lt = tai ^. from (absoluteTime $ TAIUTCMap mempty mempty) . utcLocalTime utc
-- | The <http://en.wikipedia.org/wiki/Julian_day#Variants Modified Julian Day>
-- epoch, which is /1858-11-17 00:00:00 TAI/.
{-# INLINE taiEpoch #-}
taiEpoch :: AbsoluteTime
taiEpoch = AbsoluteTime zeroV
instance AffineSpace AbsoluteTime where
type Diff AbsoluteTime = DiffTime
{-# INLINE (.-.) #-}
(.-.) = \ (AbsoluteTime a) (AbsoluteTime b) -> a ^-^ b
{-# INLINE (.+^) #-}
(.+^) = \ (AbsoluteTime a) d -> AbsoluteTime (a ^+^ d)
-- | A table of 'TAIUTCRow's for converting between TAI and UTC.
--
-- The two 'Map's are keyed on the corresponding instants in UTC and TAI
-- from which the 'TAIUTCRow' becomes applicable. The 'UTCTime' key of the
-- first 'Map' is always at midnight.
--
-- No table is provided here because leap seconds are unpredictable, and any
-- program shipped with such a table could become out-of-date in as little
-- as 6 months. See 'parseTAIUTCDAT' for details.
data TAIUTCMap = TAIUTCMap (Map UTCTime TAIUTCRow) (Map AbsoluteTime TAIUTCRow)
deriving (INSTANCES_USUAL, Show)
-- | Each line of TAIUTCDAT (see 'parseTAIUTCDAT') specifies the difference
-- between TAI and UTC for a particular period. For example:
--
-- @
-- 1968 FEB 1 =JD 2439887.5 TAI-UTC= 4.2131700 S + (MJD - 39126.) X 0.002592 S
-- @
--
-- says that from 1968-02-01 00:00:00 (Julian Date 2439887.5; or Modified
-- Julian Date 39887.0), the difference between TAI and UTC is @4.2131700s@
-- (the /additive/ part) plus a scaled component that increases for each day
-- beyond MJD 39126 (the /base/) by 0.002592s (the /coefficient/). In
-- general, the latter half of each line is of the form:
--
-- @
-- TAI-UTC= /additive/ S + (MJD - /base/) X /coefficient/ S
-- @
--
-- @'TAIUTCRow' a b c@ is a normalised version of the above, with the /base/
-- multiplied by 86400s, and the /coefficient/ divided by the same. This
-- allows us to use the internal representation of 'UTCTime'—seconds since
-- the MJD epoch—as the @MJD@ term without further rescaling.
--
-- Note that between 1961-01-01 and 1972-01-01, each UTC second was actually
-- slightly longer than one TAI (or SI) second. For the first year this was
-- at the rate of exactly 1.000000015 TAI (or SI) seconds per UTC second,
-- but was subject to irregular updates. Since leap seconds came into effect
-- on 1972-01-01, the /additive/ part has always been an intergral number of
-- seconds, and the /coefficient/ has always been zero.
--
-- To convert between TAI and UTC, we refer to the definition:
--
-- @
-- TAI - UTC = a + (MJD - b) * c
-- @
--
-- Using UTC for MJD (with 'b' and 'c' scaled as described above):
--
-- @
-- TAI = UTC + a + (UTC - b) * c
-- TAI - a + b * c = UTC + UTC * c
-- (TAI - a + b * c) / (1 + c) = UTC
-- @
--
-- This is implemented by 'absoluteTime' and 'absoluteTime''.
--
-- Further reading:
--
-- * https://en.wikipedia.org/wiki/Coordinated_Universal_Time
-- * https://en.wikipedia.org/wiki/International_Atomic_Time
data TAIUTCRow = TAIUTCRow !DiffTime !UTCTime !Rational
-- ^ Each row comprises of an /additive/ component, the /base/ of the
-- scaled component, and the /coefficient/ of the scaled component.
deriving (INSTANCES_USUAL, Show)
{-# INLINE lookupLE #-}
lookupLE :: (Ord k) => k -> Map k TAIUTCRow -> TAIUTCRow
lookupLE k = maybe (TAIUTCRow zeroV (UTCRep zeroV) 0) snd . Map.lookupLE k
{-# INLINE unwrap #-}
unwrap :: TAIUTCRow -> (Micro, Micro, Rational)
unwrap (TAIUTCRow (DiffTime a) (UTCRep (NominalDiffTime b)) c) = (a, b, c)
-- | Convert between 'UTCTime' and 'AbsoluteTime' using a 'TAIUTCMap'.
--
-- Since 'UTCTime' cannot represent a time-of-day of 86400s or more, any
-- conversion from 'AbsoluteTime' that happens to be during a leap second
-- will overflow into the next day.
--
-- See 'parseTAIUTCDAT' for how to obtain the @tum :: 'TAIUTCMap'@ below.
--
-- @
-- > let jul1 = 'utcTime' 'Control.Lens.#' 'UTCView' ('gregorian' 'Control.Lens.#' 'YearMonthDay' 2015 7 1) 'zeroV'
-- > jul1 '&' 'absoluteTime' tum '%~' ('.-^' 'fromSeconds' 1.1)
-- 2015-06-30 23:59:59.9 UTC
-- @
{-# INLINE absoluteTime #-}
absoluteTime :: TAIUTCMap -> Iso' UTCTime AbsoluteTime
absoluteTime (TAIUTCMap utcMap taiMap) = iso toTAI toUTC where
{-# INLINEABLE toTAI #-}
toTAI :: UTCTime -> AbsoluteTime
toTAI utime@(UTCRep (NominalDiffTime uts)) = AbsoluteTime . DiffTime $
uts ^+^ a ^+^ (uts ^-^ b) ^* c where
(a, b, c) = unwrap $ lookupLE utime utcMap
{-# INLINEABLE toUTC #-}
toUTC :: AbsoluteTime -> UTCTime
toUTC atime@(AbsoluteTime (DiffTime ats)) = UTCRep . NominalDiffTime $
(ats ^-^ a ^+^ b ^* c) ^/ (1 + c) where
(a, b, c) = unwrap $ lookupLE atime taiMap
-- | Convert between 'UTCView' and TAI 'AbsoluteTime' using a 'TAIUTCMap'.
--
-- Unlike 'absoluteTime', 'UTCView' /can/ represent a time-of-day greater
-- than 86400s, and this gives the correct results during a leap second.
--
-- See 'parseTAIUTCDAT' for how to obtain the @tum :: 'TAIUTCMap'@ below.
--
-- @
-- > let jul1 = 'UTCView' ('gregorian' 'Control.Lens.#' 'YearMonthDay' 2015 7 1) 'zeroV'
-- > jul1 '&' 'absoluteTime'' tum '%~' ('.-^' 'fromSeconds' 0.1)
-- 'UTCView' {'utcvDay' = 2015-06-30, 'utcvDayTime' = 86400.9s}
-- @
--
-- However keep in mind that currently there is no standard way to get the
-- TAI on most platforms. Simply converting the result of
-- 'Data.Thyme.Clock.getCurrentTime' (which calls @gettimeofday(2)@) to
-- 'AbsoluteTime' during a leap second will still give non-monotonic times.
{-# INLINE absoluteTime' #-}
absoluteTime' :: TAIUTCMap -> Iso' UTCView AbsoluteTime
absoluteTime' (TAIUTCMap utcMap taiMap) = iso toTAI toUTC where
{-# INLINEABLE toTAI #-}
toTAI :: UTCView -> AbsoluteTime
toTAI uview@(UTCView day _) = AbsoluteTime . DiffTime $
uts ^+^ a ^+^ (uts ^-^ b) ^* c where
(a, b, c) = unwrap $ lookupLE (utcTime # UTCView day zeroV) utcMap
UTCRep (NominalDiffTime uts) = utcTime # uview
{-# INLINEABLE toUTC #-}
toUTC :: AbsoluteTime -> UTCView
toUTC atime@(AbsoluteTime (DiffTime ats)) = fixup (utime ^. utcTime) where
row@(unwrap -> (a, b, c)) = lookupLE atime taiMap
utime = UTCRep . NominalDiffTime $ (ats ^-^ a ^+^ b ^* c) ^/ (1 + c)
-- 'lookupLE' of the same instant in 'utcMap' and 'taiMap' should
-- give the same 'TAIUTCRow'. If it doesn't, then @utime@ must have
-- overflown into the next 'Day'.
fixup uview@(UTCView day dt) = if lookupLE utime utcMap == row
then uview else UTCView (day .-^ 1) (fromSeconds' 86400 ^+^ dt)
-- TODO: Linux >= 3.10 has @CLOCK_TAI@ for @clock_gettime(2)@.
-- | Using a 'TAIUTCMap', lookup the 'DiffTime' length of the UTC 'Day'.
--
-- See 'parseTAIUTCDAT' for how to obtain the @tum :: 'TAIUTCMap'@ below.
--
-- @
-- > 'utcDayLength' tum '.' 'view' '_utctDay' '<$>' 'getCurrentTime'
-- 86400s
-- > 'utcDayLength' tum '$' 'gregorian' 'Control.Lens.#' 'YearMonthDay' 2015 6 30
-- 86401s
-- @
utcDayLength :: TAIUTCMap -> Day -> DiffTime
utcDayLength tum day = diff (day .+^ 1) .-. diff day where
diff d = UTCView d zeroV ^. from utcTime . absoluteTime tum
-- | @attoparsec@ 'P.Parser' for a single line of TAIUTCDAT.
--
-- Returns the starting 'UTCTime' and the normalised 'TAIUTCRow'.
parseTAIUTCRow :: P.Parser (UTCTime, TAIUTCRow)
parseTAIUTCRow = do
y <- P.skipSpace *> P.decimal <?> "Year"
let months = [ "JAN", "FEB", "MAR", "APR", "MAY", "JUN"
, "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" ]
m <- (+) 1 <$ P.skipSpace <*> indexOf months <?> "Month"
d <- P.skipSpace *> P.decimal <?> "DayOfMonth"
tokens ["=", "JD"]
-- TAI-UTC changes always happen at midnight UTC, so just ignore ".5".
since <- subtract 2400000{-.5-} <$> P.decimal
<* P.string ".5" <?> "Julian Date .5"
let ymd = YearMonthDay y m d
unless (gregorian # ymd == ModifiedJulianDay since) $ do
fail $ show ymd ++ " ≠ MJD " ++ show since
++ " ≡ " ++ show (ModifiedJulianDay since)
tokens ["TAI", "-", "UTC", "="]
a <- P.rational <?> "Additive"
tokens ["S", "+", "(", "MJD", "-"]
b <- P.decimal <* P.char '.' <?> "Base" -- also always midnight UTC
tokens [")", "X"]
c <- (/ toSeconds' posixDayLength) <$> P.rational
<* P.skipSpace <* P.string "S" <?> "Coefficient"
return (mjdToUTC since, TAIUTCRow (fromSeconds' a) (mjdToUTC b) c)
where
tokens ts = foldr (\ tok a -> P.skipSpace >> P.string tok >> a)
P.skipSpace ts <?> ("tokens " ++ show ts)
mjdToUTC mjd = utcTime # UTCView (ModifiedJulianDay mjd) zeroV
-- | Build a 'TAIUTCMap' from the result of 'parseTAIUTCRow'.
makeTAIUTCMap :: [(UTCTime, TAIUTCRow)] -> TAIUTCMap
makeTAIUTCMap rows = TAIUTCMap (Map.fromList rows)
(Map.fromList $ invert <$> rows) where
invert (since, entry) = (since ^. absoluteTime single, entry) where
single = TAIUTCMap (Map.singleton since entry) mempty
-- | Parse the contents of TAIUTCDAT into a 'TAIUTCMap' for conversion
-- between TAI and UTC.
--
-- @
-- $ curl -O \"http:\/\/maia.usno.navy.mil\/ser7\/tai-utc.dat\"
-- $ ghci --package thyme
-- > import "Data.Thyme"
-- > import "Data.Thyme.Clock.TAI"
-- > import "Data.ByteString" ('S.readFile')
-- > Right tum \<- 'parseTAIUTCDAT' '<$>' 'S.readFile' \"tai-utc.dat\"
-- > 'utcDayLength' tum '$' 'gregorian' 'Control.Lens.#' 'YearMonthDay' 2015 6 30
-- 86401s
-- @
parseTAIUTCDAT :: S.ByteString -> Either String TAIUTCMap
parseTAIUTCDAT = P.parseOnly $ makeTAIUTCMap <$> P.manyTill
(parseTAIUTCRow <* P.endOfLine) P.endOfInput
------------------------------------------------------------------------
-- | Add a duration to an 'AbsoluteTime'.
--
-- @
-- 'addAbsoluteTime' = 'flip' ('.+^')
-- 'addAbsoluteTime' d t ≡ t '.+^' d
-- @
--
-- See also the 'AffineSpace' instance for 'AbsoluteTime'.
{-# INLINE addAbsoluteTime #-}
addAbsoluteTime :: DiffTime -> AbsoluteTime -> AbsoluteTime
addAbsoluteTime = flip (.+^)
-- | The duration difference between two 'AbsoluteTime's.
--
-- @
-- 'diffAbsoluteTime' = ('.-.')
-- 'diffAbsoluteTime' a b ≡ a '.-.' b
-- @
--
-- See also the 'AffineSpace' instance for 'AbsoluteTime'.
{-# INLINE diffAbsoluteTime #-}
diffAbsoluteTime :: AbsoluteTime -> AbsoluteTime -> DiffTime
diffAbsoluteTime = (.-.)
-- | Using a 'TAIUTCMap', convert a 'UTCTime' to 'AbsoluteTime'.
--
-- @
-- 'utcToTAITime' = 'view' '.' 'absoluteTime'
-- @
{-# INLINE utcToTAITime #-}
utcToTAITime :: TAIUTCMap -> UTCTime -> AbsoluteTime
utcToTAITime = view . absoluteTime
-- | Using a 'TAIUTCMap', convert a 'AbsoluteTime' to 'UTCTime'.
--
-- @
-- 'taiToUTCTime' = 'review' '.' 'absoluteTime'
-- @
{-# INLINE taiToUTCTime #-}
taiToUTCTime :: TAIUTCMap -> AbsoluteTime -> UTCTime
taiToUTCTime = review . absoluteTime