-- | TAI and leap-second tables for converting to UTC: most people won't need this module. module Data.Time.Clock.TAI ( -- TAI arithmetic AbsoluteTime,taiEpoch,addAbsoluteTime,diffAbsoluteTime, -- leap-second table type LeapSecondTable, -- conversion between UTC and TAI with table utcDayLength,utcToTAITime,taiToUTCTime, parseTAIUTCDATFile ) where import Data.Time.LocalTime import Data.Time.Calendar.Days import Data.Time.Clock import Data.Fixed -- | AbsoluteTime is TAI, time as measured by a clock. newtype AbsoluteTime = MkAbsoluteTime {unAbsoluteTime :: DiffTime} deriving (Eq,Ord) instance Show AbsoluteTime where show t = show (utcToLocalTime utc (taiToUTCTime (const 0) t)) ++ " TAI" -- ugly, but standard apparently -- | The epoch of TAI, which is 1858-11-17 00:00:00 TAI. taiEpoch :: AbsoluteTime taiEpoch = MkAbsoluteTime 0 -- | addAbsoluteTime a b = a + b addAbsoluteTime :: DiffTime -> AbsoluteTime -> AbsoluteTime addAbsoluteTime t (MkAbsoluteTime a) = MkAbsoluteTime (a + t) -- | diffAbsoluteTime a b = a - b diffAbsoluteTime :: AbsoluteTime -> AbsoluteTime -> DiffTime diffAbsoluteTime (MkAbsoluteTime a) (MkAbsoluteTime b) = a - b -- | TAI - UTC during this day. -- No table is provided, as any program compiled with it would become -- out of date in six months. type LeapSecondTable = Day -> Integer utcDayLength :: LeapSecondTable -> Day -> DiffTime utcDayLength table day = realToFrac (86400 + (table (addDays 1 day)) - (table day)) dayStart :: LeapSecondTable -> Day -> AbsoluteTime dayStart table day = MkAbsoluteTime (realToFrac ((toModifiedJulianDay day) * 86400 + (table day))) utcToTAITime :: LeapSecondTable -> UTCTime -> AbsoluteTime utcToTAITime table (UTCTime day dtime) = MkAbsoluteTime (t + dtime) where MkAbsoluteTime t = dayStart table day taiToUTCTime :: LeapSecondTable -> AbsoluteTime -> UTCTime taiToUTCTime table abstime = stable (ModifiedJulianDay (div' (unAbsoluteTime abstime) 86400)) where stable day = if (day == day') then UTCTime day dtime else stable day' where dayt = dayStart table day dtime = diffAbsoluteTime abstime dayt day' = addDays (div' dtime (utcDayLength table day)) day -- | Parse the contents of a tai-utc.dat file. -- This does not do any kind of validation and will return a bad table for input -- not in the correct format. parseTAIUTCDATFile :: String -> LeapSecondTable parseTAIUTCDATFile ss = offsetlist 0 (parse (lines ss)) where offsetlist :: Integer -> [(Day,Integer)] -> LeapSecondTable offsetlist i [] _ = i offsetlist i ((d0,_):_) d | d < d0 = i offsetlist _ ((_,i0):xx) d = offsetlist i0 xx d parse :: [String] -> [(Day,Integer)] parse [] = [] parse (a:as) = let ps = parse as in case matchLine a of Just di -> di:ps Nothing -> ps matchLine :: String -> Maybe (Day,Integer) matchLine s = do check0S s (d,s') <- findJD s i <- findOffset s' return (d,i) -- a bit fragile check0S :: String -> Maybe () check0S "X 0.0 S" = Just () check0S [] = Nothing check0S (_:cs) = check0S cs findJD :: String -> Maybe (Day,String) findJD ('=':'J':'D':s) = do d <- getInteger '5' s return (ModifiedJulianDay (d - 2400000),s) findJD [] = Nothing findJD (_:cs) = findJD cs findOffset :: String -> Maybe Integer findOffset ('T':'A':'I':'-':'U':'T':'C':'=':s) = getInteger '0' s findOffset [] = Nothing findOffset (_:cs) = findOffset cs getInteger :: Char -> String -> Maybe Integer getInteger p s = do digits <- getDigits p s fromDigits 0 digits getDigits :: Char -> String -> Maybe String getDigits p (' ':s) = getDigits p s getDigits p (c:cs) | c >= '0' && c <= '9' = do s <- getDigits p cs return (c:s) getDigits p ('.':p1:_) = if p == p1 then Just [] else Nothing getDigits _ _ = Nothing fromDigits :: Integer -> String -> Maybe Integer fromDigits i [] = Just i fromDigits i (c:cs) | c >= '0' && c <= '9' = fromDigits ((i * 10) + (fromIntegral ((fromEnum c) - (fromEnum '0')))) cs fromDigits _ _ = Nothing -- typical line format: -- 1972 JAN 1 =JD 2441317.5 TAI-UTC= 10.0 S + (MJD - 41317.) X 0.0 S -- 1972 JUL 1 =JD 2441499.5 TAI-UTC= 11.0 S + (MJD - 41317.) X 0.0 S