{-# OPTIONS_HADDOCK hide #-}
module Data.Time.LocalTime.TimeZone.Olson.Render
(
renderTimeZoneSeriesToOlsonFile,
timeZoneSeriesToOlson,
renderOlsonToFile,
verifyOlsonLimits,
putOlson,
extractOlsonV1
)
where
import Data.Time.LocalTime.TimeZone.Olson.Types
import Data.Time.LocalTime.TimeZone.Series (TimeZoneSeries(TimeZoneSeries))
import Data.Time (TimeZone(TimeZone, timeZoneSummerOnly, timeZoneName))
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
import Data.Binary.Put (Put, runPut, putByteString, putWord8, flush,
putWord32be, putWord64be)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import Data.List (partition, sortBy, sort, group)
import Data.Ord (comparing)
import Data.Word (Word8)
import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe, catMaybes)
import Data.Monoid (mempty)
import Control.Monad (guard, replicateM_, unless)
renderTimeZoneSeriesToOlsonFile :: FilePath -> TimeZoneSeries -> IO ()
renderTimeZoneSeriesToOlsonFile fp = renderOlsonToFile fp .
fromMaybe (error "Cannot render TimeZoneSeries: default is summer time") .
timeZoneSeriesToOlson
timeZoneSeriesToOlson :: TimeZoneSeries -> Maybe OlsonData
timeZoneSeriesToOlson (TimeZoneSeries dflt pairs)
| timeZoneSummerOnly dflt && not (all timeZoneSummerOnly $ map snd pairs)
= Nothing
| otherwise = Just $
OlsonData
[Transition secs ttinfo |
(t, tz) <- reverse pairs,
let secs = round $ utcTimeToPOSIXSeconds t,
ttinfo <- maybeToList $ lookup (mkTT tz) ttAssocs]
ttinfos
[]
Nothing
where
mkTT (TimeZone offset isdst abbr) =
TtInfo (offset*60) isdst Wall abbr
dfltTT = mkTT dflt
ttAssocs = (dfltTT, 0) :
zip (uniq . sort . filter (/= dfltTT) $ map (mkTT . snd) pairs) [1..]
ttinfos = map fst ttAssocs
verifyOlsonLimits :: SizeLimits -> OlsonData -> Bool
verifyOlsonLimits limits (OlsonData transs ttinfos leaps _) =
withinLimit maxTimes transs &&
withinLimit maxTypes ttinfos &&
withinLimit maxLeaps leaps &&
withinLimit maxAbbrChars abbrChars
where
withinLimit limit items = maybe True (null . flip drop items) $
limit limits
abbrChars = concat abbrStrs ++ map (const '\NUL') abbrStrs
abbrStrs = map tt_abbr ttinfos
renderOlsonToFile :: FilePath -> OlsonData -> IO ()
renderOlsonToFile fp olson = do
unless (verifyOlsonLimits defaultLimits olson) $
error "Olson timezone data exceeds size limits"
L.writeFile fp . runPut . putOlson $ olson
putOlson olson@(OlsonData _ _ _ posix)
| fitsInVersion1 = putOlsonPart 0 put32bitIntegral olson
| otherwise = do putOlsonPart 50 put32bitIntegral olson1
putOlsonPart 50 put64bitIntegral olson
putPosixTZ posix
where
olson1 = extractOlsonV1 olson
fitsInVersion1 = fmap null posix /= Just False &&
olson1 == olson {olsonPosixTZ = Nothing}
extractOlsonV1 :: OlsonData -> OlsonData
extractOlsonV1 (OlsonData transs ttinfos leaps _)
| allV1 = OlsonData transs ttinfos leaps Nothing
| otherwise = OlsonData transs1 ttinfos1 leaps1 Nothing
where
cutoff = 0x80000000
fitsIn32bits x = x < cutoff && x >= negate cutoff
leaps1 = filter (fitsIn32bits . leapTime) leaps
transs1' = filter (fitsIn32bits . transTime) transs
allV1 = length leaps1 == length leaps && length transs1' == length transs
assoc1 = zip
(sortBy (comparing $ fmap tt_ttype . listToMaybe . flip drop ttinfos) .
uniq . sort . (0 :) $ map transIndex transs1')
[0..]
transs1 = [t {transIndex = i} |
t <- transs1', i <- maybeToList $ lookup (transIndex t) assoc1]
ttinfos1 = map snd . dropWhile (isNothing . fst) .
sortBy (comparing fst) $ zip (map (flip lookup assoc1) [0..]) ttinfos
putOlsonPart :: Word8 -> (Integer -> Put) -> OlsonData -> Put
putOlsonPart version putTime (OlsonData transs ttinfos leaps _) = do
putASCII "magic number" "TZif"
putWord8 version
putByteString . B.pack $ replicate 15 0
replicateM_ 2 $ putCount ttinfosWithTtype
putCount leaps
putCount transs
putCount ttinfos
put32bitIntegral abbrChars
mapM_ (putTime . transTime ) transs
mapM_ (put8bitIntegral . transIndex) transs
mapM_ putTtInfo ttinfosIndexed
mapM_ putAbbr abbrStrings
mapM_ (putLeapInfo putTime) leaps
mapM_ (putBool . (== Std) . tt_ttype) ttinfosWithTtype
mapM_ (putBool . (== UTC) . tt_ttype) ttinfosWithTtype
where
putCount = put32bitIntegral . length
ttinfosWithTtype = takeWhile ((<= UTC) . tt_ttype) ttinfosIndexed
abbrStrings = uniq . sort $ map tt_abbr ttinfos
abbrChars = sum (map length abbrStrings) + length abbrStrings
putAbbr abbr = putASCII "time zone abbreviation" abbr >> putWord8 0
abbrAssocs = zip abbrStrings . scanl (+) 0 $
map ((+ 1) . length) abbrStrings
ttinfosIndexed = [TtInfo utoff isdst ttype i |
TtInfo utoff isdst ttype abbr <- ttinfos,
i <- maybeToList $ lookup abbr abbrAssocs]
putPosixTZ :: Maybe String -> Put
putPosixTZ posix = do
putWord8 10
putASCII "POSIX TZ string"$ fromMaybe "" posix
putWord8 10
putTtInfo :: TtInfo Int -> Put
putTtInfo tt = do
put32bitIntegral $ tt_utoff tt
putBool $ tt_isdst tt
put8bitIntegral $ tt_abbr tt
putLeapInfo :: Integral a => (a -> Put) -> LeapInfo -> Put
putLeapInfo putTime leap = do
putTime . fromIntegral $ leapTime leap
put32bitIntegral $ leapOffset leap
put8bitIntegral :: Integral a => a -> Put
put8bitIntegral = putWord8 . fromIntegral
put32bitIntegral :: Integral a => a -> Put
put32bitIntegral = putWord32be . fromIntegral
put64bitIntegral :: Integral a => a -> Put
put64bitIntegral = putWord64be . fromIntegral
putBool :: Bool -> Put
putBool False = putWord8 0
putBool True = putWord8 1
uniq :: Eq a => [a] -> [a]
uniq = map head . group
putASCII :: String -> String -> Put
putASCII what =
putByteString . B.pack . map (fromIntegral . verify . fromEnum)
where
verify c
| c >= 32 && c <= 126 = c
| otherwise = error $ "Cannot render TimeZoneSeries: " ++
what ++ " contains non-ASCII characters"