git.iko.soy/typeable/timezone-olson
on master help sign in
12 files changed+634−247
since splitsnapshots
unifiedsplit
.gitignore+16 .travis.yml+107 Data/Time/LocalTime/TimeZone/Olson.hs+31 Data/Time/LocalTime/TimeZone/Olson/Parse.hs+88−75 Data/Time/LocalTime/TimeZone/Olson/Render.hs+127−100 Data/Time/LocalTime/TimeZone/Olson/Types.hs+102−57 LICENSE+29 README.md+43 Setup.hs+2 catTZ.hs+14 hzdump.hs+30−15 timezone-olson.cabal+45
.gitignorenew file+16
@@ -0,0 +1,16 @@
1+dist
2+cabal-dev
3+*.o
4+*.hi
5+*.chi
6+*.chs.h
7+*.dyn_o
8+*.dyn_hi
9+.virtualenv
10+.hpc
11+.hsenv
12+.cabal-sandbox/
13+cabal.sandbox.config
14+*.prof
15+*.aux
16+*.hp
.travis.ymlnew file+107
@@ -0,0 +1,107 @@
1+# This Travis job script has been generated by a script via
2+#
3+# runghc make_travis_yml_2.hs 'timezone-olson.cabal'
4+#
5+# For more information, see https://github.com/hvr/multi-ghc-travis
6+#
7+language: c
8+sudo: false
9+
10+git:
11+ submodules: false # whether to recursively clone submodules
12+
13+cache:
14+ directories:
15+ - $HOME/.cabal/packages
16+ - $HOME/.cabal/store
17+
18+before_cache:
19+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/build-reports.log
20+ # remove files that are regenerated by 'cabal update'
21+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/00-index.*
22+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/*.json
23+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.cache
24+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar
25+ - rm -fv $HOME/.cabal/packages/hackage.haskell.org/01-index.tar.idx
26+
27+ - rm -rfv $HOME/.cabal/packages/head.hackage
28+
29+matrix:
30+ include:
31+ - compiler: "ghc-8.0.1"
32+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
33+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.0.1], sources: [hvr-ghc]}}
34+ - compiler: "ghc-8.0.2"
35+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
36+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.0.2], sources: [hvr-ghc]}}
37+ - compiler: "ghc-8.2.1"
38+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
39+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.2.1], sources: [hvr-ghc]}}
40+ - compiler: "ghc-8.2.2"
41+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
42+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.2.2], sources: [hvr-ghc]}}
43+ - compiler: "ghc-8.4.1"
44+ # env: TEST=--disable-tests BENCH=--disable-benchmarks
45+ addons: {apt: {packages: [ghc-ppa-tools,cabal-install-head,ghc-8.4.1], sources: [hvr-ghc]}}
46+
47+before_install:
48+ - HC=${CC}
49+ - HCPKG=${HC/ghc/ghc-pkg}
50+ - unset CC
51+ - ROOTDIR=$(pwd)
52+ - mkdir -p $HOME/.local/bin
53+ - "PATH=/opt/ghc/bin:/opt/ghc-ppa-tools/bin:$HOME/local/bin:$PATH"
54+ - HCNUMVER=$(( $(${HC} --numeric-version|sed -E 's/([0-9]+)\.([0-9]+)\.([0-9]+).*/\1 * 10000 + \2 * 100 + \3/') ))
55+ - echo $HCNUMVER
56+
57+install:
58+ - cabal --version
59+ - echo "$(${HC} --version) [$(${HC} --print-project-git-commit-id 2> /dev/null || echo '?')]"
60+ - BENCH=${BENCH---enable-benchmarks}
61+ - TEST=${TEST---enable-tests}
62+ - HADDOCK=${HADDOCK-true}
63+ - INSTALLED=${INSTALLED-true}
64+ - GHCHEAD=${GHCHEAD-false}
65+ - travis_retry cabal update -v
66+ - "sed -i.bak 's/^jobs:/-- jobs:/' ${HOME}/.cabal/config"
67+ - rm -fv cabal.project cabal.project.local
68+ - grep -Ev -- '^\s*--' ${HOME}/.cabal/config | grep -Ev '^\s*$'
69+ - "printf 'packages: \".\"\\n' > cabal.project"
70+ - cat cabal.project
71+ - if [ -f "./configure.ac" ]; then
72+ (cd "." && autoreconf -i);
73+ fi
74+ - rm -f cabal.project.freeze
75+ - cabal new-build -w ${HC} ${TEST} ${BENCH} --project-file="cabal.project" --dep -j2 all
76+ - cabal new-build -w ${HC} --disable-tests --disable-benchmarks --project-file="cabal.project" --dep -j2 all
77+ - rm -rf .ghc.environment.* "."/dist
78+ - DISTDIR=$(mktemp -d /tmp/dist-test.XXXX)
79+
80+# Here starts the actual work to be performed for the package under test;
81+# any command which exits with a non-zero exit code causes the build to fail.
82+script:
83+ # test that source-distributions can be generated
84+ - (cd "." && cabal sdist)
85+ - mv "."/dist/timezone-olson-*.tar.gz ${DISTDIR}/
86+ - cd ${DISTDIR} || false
87+ - find . -maxdepth 1 -name '*.tar.gz' -exec tar -xvf '{}' \;
88+ - "printf 'packages: timezone-olson-*/*.cabal\\n' > cabal.project"
89+ - cat cabal.project
90+ # this builds all libraries and executables (without tests/benchmarks)
91+ - cabal new-build -w ${HC} --disable-tests --disable-benchmarks all
92+
93+ # Build with installed constraints for packages in global-db
94+ - if $INSTALLED; then echo cabal new-build -w ${HC} --disable-tests --disable-benchmarks $(${HCPKG} list --global --simple-output --names-only | sed 's/\([a-zA-Z0-9-]\{1,\}\) */--constraint="\1 installed" /g') all | sh; else echo "Not building with installed constraints"; fi
95+
96+ # build & run tests, build benchmarks
97+ - cabal new-build -w ${HC} ${TEST} ${BENCH} all
98+
99+ # cabal check
100+ - (cd timezone-olson-* && cabal check)
101+
102+ # haddock
103+ - rm -rf ./dist-newstyle
104+ - if $HADDOCK; then cabal new-haddock -w ${HC} ${TEST} ${BENCH} all; else echo "Skipping haddock generation";fi
105+
106+# REGENDATA ["timezone-olson.cabal"]
107+# EOF
Data/Time/LocalTime/TimeZone/Olson.hsnew file+31
@@ -0,0 +1,31 @@
1+-----------------------------------------------------------------------------
2+-- |
3+-- Module : Data.Time.LocalTime.TimeZone.Olson
4+-- Copyright : Yitzchak Gale 2018
5+--
6+-- Maintainer : Yitzchak Gale <gale@sefer.org>
7+-- Portability : portable
8+--
9+-- A parser and renderer for binary Olson timezone files whose format
10+-- are specified by the tzfile(5) man page on Unix-like systems. For
11+-- more information about this format, see
12+-- <http://www.twinsun.com/tz/tz-link.htm>. Functions are provided for
13+-- converting the parsed data into @TimeZoneSeries@ and @TimeZone@
14+-- objects.
15+
16+{- Copyright (c) 2018 Yitzchak Gale. All rights reserved.
17+For licensing information, see the BSD3-style license in the file
18+LICENSE that was originally distributed by the author together with
19+this file. -}
20+
21+module Data.Time.LocalTime.TimeZone.Olson
22+(
23+ module Data.Time.LocalTime.TimeZone.Olson.Parse,
24+ module Data.Time.LocalTime.TimeZone.Olson.Render,
25+ module Data.Time.LocalTime.TimeZone.Olson.Types
26+)
27+where
28+
29+import Data.Time.LocalTime.TimeZone.Olson.Types
30+import Data.Time.LocalTime.TimeZone.Olson.Parse
31+import Data.Time.LocalTime.TimeZone.Olson.Render
Data/Time/LocalTime/TimeZone/Olson/Parse.hs+88−75
@@ -1,53 +1,27 @@
11{-# LANGUAGE DeriveDataTypeable #-}
2+{-# OPTIONS_HADDOCK hide #-}
23-----------------------------------------------------------------------------
34-- |
45-- Module : Data.Time.LocalTime.TimeZone.Olson.Parse
5−-- Copyright : Yitzchak Gale 2010
6+-- Copyright : Yitzchak Gale 2018
67--
78-- Maintainer : Yitzchak Gale <gale@sefer.org>
89-- Portability : portable
910--
10−-- A parser for binary Olson timezone files whose format are specified
11+-- A parser for binary Olson timezone files whose format is specified
1112-- by the tzfile(5) man page on Unix-like systems. For more
1213-- information about this format, see
13−-- http://www.twinsun.com/tz/tz-link.htm. Functions are provided for
14−-- converting the parsed data into @TimeZoneSeries@ and @TimeZone@
15−-- objects.
16−
17−{-
18−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
19−
20−Redistribution and use in source and binary forms, with or without
21−modification, are permitted provided that the following conditions are
22−met:
23−
24− * Redistributions of source code must retain the above copyright
25− notice, this list of conditions and the following disclaimer.
26−
27− * Redistributions in binary form must reproduce the above
28− copyright notice, this list of conditions and the following
29− disclaimer in the documentation and/or other materials provided
30− with the distribution.
31−
32− * Neither the name of Yitzchak Gale nor the names of other
33− contributors may be used to endorse or promote products derived
34− from this software without specific prior written permission.
35−
36−THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
37−"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
38−LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
39−A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
40−OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
41−SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
42−LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
43−DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
44−THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
45−(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
46−OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
47−----------------------------------------------------------------------------}
14+-- <http://www.twinsun.com/tz/tz-link.htm>. Functions are provided for
15+-- converting the parsed data into 'TimeZoneSeries' objects.
16+
17+{- Copyright (c) 2018 Yitzchak Gale. All rights reserved.
18+For licensing information, see the BSD3-style license in the file
19+LICENSE that was originally distributed by the author together with
20+this file. -}
4821
4922module Data.Time.LocalTime.TimeZone.Olson.Parse
5023(
24+ -- * Parsing Olson timezone files
5125 getTimeZoneSeriesFromOlsonFile,
5226 getOlsonFromFile,
5327 olsonToTimeZoneSeries,
@@ -65,11 +39,14 @@ import Data.Binary.Get (Get, runGet, getWord8, getWord32be, getWord64be,
6539import qualified Data.ByteString as B
6640import qualified Data.ByteString.Lazy as L
6741import Data.Monoid (mappend)
42+import Data.List (sortBy, groupBy)
6843import Data.Maybe (listToMaybe, fromMaybe)
6944import Data.Word (Word8)
7045import Data.Int (Int32, Int64)
46+import Data.Ord (comparing)
47+import Data.Function (on)
7148import Data.Typeable (Typeable)
72−import Control.Monad (guard, replicateM, replicateM_)
49+import Control.Monad (guard, replicateM, replicateM_, when)
7350import Control.Exception.Extensible (try, throw, Exception, ErrorCall)
7451
7552-- | An exception indicating that the binary data being parsed was not
@@ -83,8 +60,8 @@ instance Exception OlsonError
8360-- | Convert parsed Olson timezone data into a @TimeZoneSeries@.
8461olsonToTimeZoneSeries :: OlsonData -> Maybe TimeZoneSeries
8562olsonToTimeZoneSeries (OlsonData ttimes ttinfos@(dflt0:_) _ _) =
86− fmap (TimeZoneSeries $ mkTZ dflt) . mapM (lookupTZ ttinfos) $
87− reverse ttimes
63+ fmap (TimeZoneSeries $ mkTZ dflt) . mapM (lookupTZ ttinfos) .
64+ uniqTimes . sortBy futureToPast $ ttimes
8865 where
8966 dflt = fromMaybe dflt0 . listToMaybe $ filter isStd ttinfos
9067 isStd (TtInfo _ isdst _ _) = not isdst
@@ -93,53 +70,74 @@ olsonToTimeZoneSeries (OlsonData ttimes ttinfos@(dflt0:_) _ _) =
9370 lookupTZ ttinfos ttime = fmap (((,) $ toUTC ttime) . mkTZ) . listToMaybe $
9471 drop (transIndex ttime) ttinfos
9572 toUTC = posixSecondsToUTCTime . fromIntegral . transTime
73+ uniqTimes = map last . groupBy ((==) `on` transTime)
74+ futureToPast = comparing $ negate . transTime
9675olsonToTimeZoneSeries _ = Nothing
9776
9877-- | Read timezone data from a binary Olson timezone file and convert
99−-- it into a @TimeZoneSeries@.
78+-- it into a @TimeZoneSeries@ for use together with the types and
79+-- fucntions of "Data.Time". This is the function from this module
80+-- for which you are most likely to have use.
81+--
82+-- If the values in the Olson timezone file exceed the standard size
83+-- limits (see 'defaultLimits'), this function throws an
84+-- exception. For other behavior, use 'getOlson' and
85+-- 'Data.Binary.Get.runGet' directly.
10086getTimeZoneSeriesFromOlsonFile :: FilePath -> IO TimeZoneSeries
10187getTimeZoneSeriesFromOlsonFile fp = getOlsonFromFile fp >>=
10288 maybe (throwOlson fp "no timezone found in OlsonData") return .
10389 olsonToTimeZoneSeries
10490
10591-- | Parse a binary Olson timezone file.
92+--
93+-- If the values in the Olson timezone file exceed the standard size
94+-- limits (see 'defaultLimits'), this function throws an
95+-- exception. For other behavior, use 'getOlson' and
96+-- 'Data.Binary.Get.runGet' directly.
10697getOlsonFromFile :: FilePath -> IO OlsonData
10798getOlsonFromFile fp = do
108− e <- try . fmap (runGet getOlson) $ L.readFile fp
99+ e <- try . fmap (runGet $ getOlson defaultLimits) $ L.readFile fp
109100 either (formatError fp) return e
110101
111102formatError :: FilePath -> ErrorCall -> IO a
112103formatError fp e = throwOlson fp $ show e
113104
114105-- | A binary parser for binary Olson timezone files
115−getOlson :: Get OlsonData
116−getOlson = do
117− getByteString 4
118− >>= verify ((== "TZif") . toASCII . B.unpack) "missing magic number"
119− version <- getWord8
120− >>= verify (`elem` [0, 50]) "invalid version number"
121− replicateM_ 15 getWord8 -- padding nulls
122− getOlsonParts version
123−
124−-- There is one part for Version 1 format data, and two parts and a POSIX
125−-- TZ string for Version 2 format data
126−getOlsonParts :: Word8 -> Get OlsonData
127−getOlsonParts 0 = getOlsonPart get32bitInteger
128−getOlsonParts 50 = do part1 <- getOlsonPart get32bitInteger
129− part2 <- getOlsonPart get64bitInteger
130− posixTZ <- getPosixTZ
131− return $ part1 `mappend` part2 `mappend` posixTZ
132−getOlsonParts _ = error "Invalid version number" -- never reached
106+getOlson :: SizeLimits -> Get OlsonData
107+getOlson limits = do
108+ (version, part1) <- getOlsonPart True limits get32bitInteger
109+ -- There is one part for Version 1 format data, and two parts and a POSIX
110+ -- TZ string for Version 2 or 3 format data
111+ case () of
112+ _ | version == 0 -> return part1
113+ | version == 50 || version == 51 -> do
114+ (_, part2) <- getOlsonPart False limits get64bitInteger
115+ posixTZ <- getPosixTZ
116+ return $ part1 `mappend` part2 `mappend` posixTZ
117+ | otherwise -> do
118+ let msg = "getOlson: invalid tzfile version " ++ toASCII [version]
119+ verify (const False) msg undefined
133120
134121-- Parse the part of an Olson file that contains timezone data
135−getOlsonPart :: Integral a => Get a -> Get OlsonData
136−getOlsonPart getTime = do
122+getOlsonPart :: Integral a => Bool -> SizeLimits -> Get a ->
123+ Get (Word8, OlsonData)
124+getOlsonPart verifyMagic limits getTime = do
125+ magic <- fmap (toASCII . B.unpack) $ getByteString 4
126+ when verifyMagic $ verify_ (== "TZif") "missing magic number" magic
127+ version <- getWord8
128+ replicateM_ 15 getWord8 -- padding nulls
137129 tzh_ttisgmtcnt <- get32bitInt
138130 tzh_ttisstdcnt <- get32bitInt
139131 tzh_leapcnt <- get32bitInt
132+ >>= verify (withinLimit maxLeaps) "too many leap second specifications"
140133 tzh_timecnt <- get32bitInt
134+ >>= verify (withinLimit maxTimes) "too many timezone transitions"
141135 tzh_typecnt <- get32bitInt
136+ >>= verify (withinLimit maxTypes) "too many timezone type specifications"
137+ verify (withinLimit maxTypes) "too many isgmt specifiers" tzh_ttisgmtcnt
138+ verify (withinLimit maxTypes) "too many isstd specifiers" tzh_ttisstdcnt
142139 tzh_charcnt <- get32bitInt
140+ >>= verify (withinLimit maxAbbrChars) "too many tilezone specifiers"
143141 times <- fmap (map toInteger) $ replicateM tzh_timecnt getTime
144142 indexes <- replicateM tzh_timecnt get8bitInt
145143 ttinfos <- replicateM tzh_typecnt getTtInfo
@@ -147,22 +145,32 @@ getOlsonPart getTime = do
147145 leaps <- replicateM tzh_leapcnt $ getLeapInfo getTime
148146 isstds <- replicateM tzh_ttisstdcnt getBool
149147 isgmts <- replicateM tzh_ttisgmtcnt getBool
150− return $ OlsonData
151− (zipWith Transition times indexes)
152− (map (flip lookupAbbr abbr_chars) . zipWith setTtype ttinfos .
153− (++ repeat UnknownType) $
154− zipWith boolsToTType (map Just isstds ++ repeat Nothing) isgmts)
155− leaps
156− Nothing
148+ return
149+ (version,
150+ OlsonData
151+ (zipWith Transition times indexes)
152+ (map (flip lookupAbbr abbr_chars) . zipWith setTtype ttinfos $
153+ zipWithExtend boolsToTType False False isstds isgmts)
154+ leaps
155+ Nothing
156+ )
157157 where
158+ withinLimit limit value = maybe True (value <=) $ limit limits
158159 lookupAbbr (TtInfo gmtoff isdst ttype abbrind) =
159160 TtInfo gmtoff isdst ttype . takeWhile (/= '\NUL') . drop abbrind
160161 setTtype ttinfo ttype = ttinfo {tt_ttype = ttype}
161− boolsToTType _ isgmt | isgmt = UTC
162− boolsToTType isstd _ = maybe UnknownType stdOrWall isstd
163− stdOrWall isstd
164− | isstd = Std
165− | otherwise = Wall
162+ boolsToTType _ isgmt | isgmt = UTC
163+ boolsToTType isstd _
164+ | isstd = Std
165+ | otherwise = Wall
166+
167+-- A variant of zipWith whose result is the length of the longer
168+-- rather than the shorter list, by extending the shorter list with
169+-- a default value
170+zipWithExtend :: (a -> b -> c) -> a -> b -> [a] -> [b] -> [c]
171+zipWithExtend f x0 y0 (x:xs) (y:ys) = f x y : zipWithExtend f x0 y0 xs ys
172+zipWithExtend f x0 _ [] ys = map (f x0) ys
173+zipWithExtend f _ y0 xs _ = map (flip f y0) xs
166174
167175-- Parse a POSIX-style TZ string.
168176-- We don't try to understand the TZ string, we just pass it along whole.
@@ -183,7 +191,7 @@ getTtInfo = do
183191 gmtoff <- get32bitInt
184192 isdst <- getBool
185193 abbrind <- get8bitInt
186− return $ TtInfo gmtoff isdst UnknownType abbrind
194+ return $ TtInfo gmtoff isdst Wall abbrind
187195
188196-- Parse leap second info. (usually not used)
189197getLeapInfo :: Integral a => Get a -> Get LeapInfo
@@ -226,6 +234,11 @@ verify pred msg val
226234 | pred val = return val
227235 | otherwise = error msg
228236
237+verify_ :: Monad m => (a -> Bool) -> String -> a -> m ()
238+verify_ pred msg val
239+ | pred val = return ()
240+ | otherwise = error msg
241+
229242throwOlson :: FilePath -> String -> IO a
230243throwOlson fp msg = throw . OlsonError $
231244 fp ++ ": invalid timezone file: " ++ msg
Data/Time/LocalTime/TimeZone/Olson/Render.hs+127−100
@@ -1,63 +1,43 @@
1+{-# OPTIONS_HADDOCK hide #-}
12-----------------------------------------------------------------------------
23-- |
34-- Module : Data.Time.LocalTime.TimeZone.Olson.Render
4−-- Copyright : Yitzchak Gale 2010
5+-- Copyright : Yitzchak Gale 2018
56--
67-- Maintainer : Yitzchak Gale <gale@sefer.org>
78-- Portability : portable
89--
9−-- Render Olson timezone data in the standard binary format as
10−-- used in Olson timezone files, and as specified by the
11−-- tzfile(5) man page on Unix-like systems. For more information
12−-- about this format, see http://www.twinsun.com/tz/tz-link.htm.
13−
14−{-
15−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
16−
17−Redistribution and use in source and binary forms, with or without
18−modification, are permitted provided that the following conditions are
19−met:
20−
21− * Redistributions of source code must retain the above copyright
22− notice, this list of conditions and the following disclaimer.
23−
24− * Redistributions in binary form must reproduce the above
25− copyright notice, this list of conditions and the following
26− disclaimer in the documentation and/or other materials provided
27− with the distribution.
28−
29− * Neither the name of Yitzchak Gale nor the names of other
30− contributors may be used to endorse or promote products derived
31− from this software without specific prior written permission.
32−
33−THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
34−"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
35−LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
36−A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
37−OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
38−SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
39−LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
40−DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
41−THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
42−(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
43−OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
44−----------------------------------------------------------------------------}
10+-- Render Olson timezone data in the standard binary format as used in
11+-- Olson timezone files, and as specified by the tzfile(5) man page on
12+-- Unix-like systems. For more information about this format, see
13+-- <http://www.twinsun.com/tz/tz-link.htm>.
14+
15+{- Copyright (c) 2018ZZ Yitzchak Gale. All rights reserved.
16+For licensing information, see the BSD3-style license in the file
17+LICENSE that was originally distributed by the author together with
18+this file. -}
4519
4620module Data.Time.LocalTime.TimeZone.Olson.Render
4721(
48− -- * Rendering Olson timezone data
22+ -- * Rendering Olson timezone files
4923 -- | If any of the transition times or leap second times specified
5024 -- require more than a 32-bit integer to represent as a Unix
5125 -- timestamp, or if a POSIX-style TZ string is specified, timezone
5226 -- data is rendered using Version 2 format. Otherwise, the timezone data
5327 -- is rendered using Version 1 format.
28+ renderTimeZoneSeriesToOlsonFile,
29+ timeZoneSeriesToOlson,
5430 renderOlsonToFile,
31+ verifyOlsonLimits,
5532 putOlson,
56− splitOlson
33+ extractOlsonV1
5734)
5835where
5936
6037import Data.Time.LocalTime.TimeZone.Olson.Types
38+import Data.Time.LocalTime.TimeZone.Series (TimeZoneSeries(TimeZoneSeries))
39+import Data.Time (TimeZone(TimeZone, timeZoneSummerOnly, timeZoneName))
40+import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)
6141import Data.Binary.Put (Put, runPut, putByteString, putWord8, flush,
6242 putWord32be, putWord64be)
6343import qualified Data.ByteString as B
@@ -65,78 +45,117 @@ import qualified Data.ByteString.Lazy as L
6545import Data.List (partition, sortBy, sort, group)
6646import Data.Ord (comparing)
6747import Data.Word (Word8)
68−import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe)
48+import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe, catMaybes)
6949import Data.Monoid (mempty)
70−import Control.Monad (guard, replicateM_)
50+import Control.Monad (guard, replicateM_, unless)
7151
52+-- | Render a @TimeZoneSeries@ as a binary Olson timezone file.
53+--
54+-- If the values in the Olson timezone data exceed the standard size
55+-- limits (see 'defaultLimits'), this function throws an
56+-- exception. For other behavior, use 'timeZoneSeriesToOlson',
57+-- 'verifyOlsonLimits', 'putOlson' and 'Data.Binary.Put.runPut'
58+-- directly.
59+renderTimeZoneSeriesToOlsonFile :: FilePath -> TimeZoneSeries -> IO ()
60+renderTimeZoneSeriesToOlsonFile fp = renderOlsonToFile fp .
61+ fromMaybe (error "Cannot render TimeZoneSeries: default is summer time") .
62+ timeZoneSeriesToOlson
63+
64+-- | Convert a @TimeZoneSeries@ to @OlsonData@ for rendering.
65+timeZoneSeriesToOlson :: TimeZoneSeries -> Maybe OlsonData
66+timeZoneSeriesToOlson (TimeZoneSeries dflt pairs)
67+ | timeZoneSummerOnly dflt && not (all timeZoneSummerOnly $ map snd pairs)
68+ = Nothing
69+ | otherwise = Just $
70+ OlsonData
71+ [Transition secs ttinfo |
72+ (t, tz) <- reverse pairs,
73+ let secs = round $ utcTimeToPOSIXSeconds t,
74+ ttinfo <- maybeToList $ lookup (mkTT tz) ttAssocs]
75+ ttinfos
76+ []
77+ Nothing
78+ where
79+ mkTT (TimeZone offset isdst abbr) =
80+ TtInfo (offset*60) isdst Wall abbr
81+ dfltTT = mkTT dflt
82+ ttAssocs = (dfltTT, 0) :
83+ zip (uniq . sort . filter (/= dfltTT) $ map (mkTT . snd) pairs) [1..]
84+ ttinfos = map fst ttAssocs
85+
86+-- | Check whether @OlsonData@ is within size limits.
87+verifyOlsonLimits :: SizeLimits -> OlsonData -> Bool
88+verifyOlsonLimits limits (OlsonData transs ttinfos leaps _) =
89+ withinLimit maxTimes transs &&
90+ withinLimit maxTypes ttinfos &&
91+ withinLimit maxLeaps leaps &&
92+ withinLimit maxAbbrChars abbrChars
93+ where
94+ withinLimit limit items = maybe True (null . flip drop items) $
95+ limit limits
96+ abbrChars = concat abbrStrs ++ map (const '\NUL') abbrStrs
97+ abbrStrs = map tt_abbr ttinfos
7298
7399-- | Render Olson timezone data as a binary Olson timezone file
100+--
101+-- If the values in the Olson timezone data exceed the standard size
102+-- limits (see 'defaultLimits'), this function throws an
103+-- exception. For other behavior, use 'verifyOlsonLimits', 'putOlson'
104+-- and 'Data.Binary.Put.runPut' directly.
74105renderOlsonToFile :: FilePath -> OlsonData -> IO ()
75−renderOlsonToFile fp = L.writeFile fp . runPut . putOlson
106+renderOlsonToFile fp olson = do
107+ unless (verifyOlsonLimits defaultLimits olson) $
108+ error "Olson timezone data exceeds size limits"
109+ L.writeFile fp . runPut . putOlson $ olson
76110
77111-- | Render Olson timezone data in binary Olson timezone file format
78112-- as a lazy @ByteString@
79−putOlson :: OlsonData -> Put
80−putOlson olson = do
81− putASCII "TZif"
82− putWord8 version
83− putByteString . B.pack $ replicate 15 0 -- padding nulls
84− putOlsonParts version olson1 olson2 posix
85− flush
113+putOlson olson@(OlsonData _ _ _ posix)
114+ | fitsInVersion1 = putOlsonPart 0 put32bitIntegral olson
115+ | otherwise = do putOlsonPart 50 put32bitIntegral olson1
116+ putOlsonPart 50 put64bitIntegral olson
117+ putPosixTZ posix
86118 where
87− (olson1, olson2, posix) = splitOlson olson
88− version
89− | olson2 /= mempty = 50
90− | otherwise = maybe 0 (const 50) $ posix >>= guard . not . null
91−
92−-- | Split Olson timezone data into three parts: timezone data that can
93−-- be rendered using Version 1 format, timezone data that can only be
94−-- rendered using Version 2 format, and the POSIX-style TZ string
95−-- if specified
96−splitOlson :: OlsonData -> (OlsonData, OlsonData, Maybe String)
97−splitOlson (OlsonData transs ttinfos leaps posix) =
98− (OlsonData transs1 ttinfos1 leaps1 Nothing,
99− OlsonData transs2 ttinfos2 leaps2 Nothing,
100− posix)
119+ olson1 = extractOlsonV1 olson
120+ fitsInVersion1 = fmap null posix /= Just False &&
121+ olson1 == olson {olsonPosixTZ = Nothing}
122+
123+-- | Extract Olson timezone data that can be rendered using Version 1 format
124+extractOlsonV1 :: OlsonData -> OlsonData
125+extractOlsonV1 (OlsonData transs ttinfos leaps _)
126+ | allV1 = OlsonData transs ttinfos leaps Nothing
127+ | otherwise = OlsonData transs1 ttinfos1 leaps1 Nothing
101128 where
102129 cutoff = 0x80000000 -- 2^31
103− ( leaps1 , leaps2 ) = partition ((>= cutoff) . leapTime) leaps
104− (transs1', transs2') = partition ((>= cutoff) . transTime) transs
105− assoc1 = mkAssoc [0] transs1'
106− assoc2 = mkAssoc [] transs2'
107− transs1 = mkTranss transs1' assoc1
108− transs2 = mkTranss transs2' assoc2
109− ttinfos1 = mkTtinfos assoc1
110− ttinfos2 = mkTtinfos assoc2
111− mkAssoc prepend transs' = zip
130+ fitsIn32bits x = x < cutoff && x >= negate cutoff
131+ leaps1 = filter (fitsIn32bits . leapTime) leaps
132+ transs1' = filter (fitsIn32bits . transTime) transs
133+ allV1 = length leaps1 == length leaps && length transs1' == length transs
134+ assoc1 = zip
112135 (sortBy (comparing $ fmap tt_ttype . listToMaybe . flip drop ttinfos) .
113− uniq . sort . (prepend ++) $ map transIndex transs')
136+ uniq . sort . (0 :) $ map transIndex transs1')
114137 [0..]
115− mkTranss transs' assoc = [t {transIndex = i} |
116− t <- transs', i <- maybeToList $ lookup (transIndex t) assoc]
117− mkTtinfos assoc = map snd . dropWhile (isNothing . fst) .
118− sortBy (comparing fst) $ zip (map (flip lookup assoc) [0..]) ttinfos
119−
120−putOlsonParts :: Word8 -> OlsonData -> OlsonData -> Maybe String -> Put
121−putOlsonParts 0 olson1 _ _ = putOlsonPart put32bitIntegral olson1
122−putOlsonParts _ olson1 olson2 posix = do
123− putOlsonPart put32bitIntegral olson1
124− putOlsonPart put64bitIntegral olson2
125− putPosixTZ posix
126−
127−putOlsonPart :: (Integer -> Put) -> OlsonData -> Put
128−putOlsonPart putTime (OlsonData transs ttinfos leaps _) = do
138+ transs1 = [t {transIndex = i} |
139+ t <- transs1', i <- maybeToList $ lookup (transIndex t) assoc1]
140+ ttinfos1 = map snd . dropWhile (isNothing . fst) .
141+ sortBy (comparing fst) $ zip (map (flip lookup assoc1) [0..]) ttinfos
142+
143+putOlsonPart :: Word8 -> (Integer -> Put) -> OlsonData -> Put
144+putOlsonPart version putTime (OlsonData transs ttinfos leaps _) = do
145+ putASCII "magic number" "TZif"
146+ putWord8 version
147+ putByteString . B.pack $ replicate 15 0 -- padding nulls
129148 replicateM_ 2 $ putCount ttinfosWithTtype
130− -- tzh_ttisgmtcnt
131− -- tzh_ttisstdcnt
132− putCount leaps -- tzh_leapcnt
133− putCount transs -- tzh_timecnt
134− putCount ttinfos -- tzh_typecnt
135− putCount abbrChars -- tzh_charcnt
149+ -- tzh_ttisgmtcnt
150+ -- tzh_ttisstdcnt
151+ putCount leaps -- tzh_leapcnt
152+ putCount transs -- tzh_timecnt
153+ putCount ttinfos -- tzh_typecnt
154+ put32bitIntegral abbrChars -- tzh_charcnt
136155 mapM_ (putTime . transTime ) transs
137156 mapM_ (put8bitIntegral . transIndex) transs
138157 mapM_ putTtInfo ttinfosIndexed
139− putASCII abbrChars
158+ mapM_ putAbbr abbrStrings
140159 mapM_ (putLeapInfo putTime) leaps
141160 mapM_ (putBool . (== Std) . tt_ttype) ttinfosWithTtype -- isstd
142161 mapM_ (putBool . (== UTC) . tt_ttype) ttinfosWithTtype -- isgmt
@@ -144,16 +163,18 @@ putOlsonPart putTime (OlsonData transs ttinfos leaps _) = do
144163 putCount = put32bitIntegral . length
145164 ttinfosWithTtype = takeWhile ((<= UTC) . tt_ttype) ttinfosIndexed
146165 abbrStrings = uniq . sort $ map tt_abbr ttinfos
147− abbrChars = concatMap (++ "\NUL") abbrStrings
166+ abbrChars = sum (map length abbrStrings) + length abbrStrings
167+ putAbbr abbr = putASCII "time zone abbreviation" abbr >> putWord8 0
168+ abbrAssocs = zip abbrStrings . scanl (+) 0 $
169+ map ((+ 1) . length) abbrStrings
148170 ttinfosIndexed = [TtInfo gmtoff isdst ttype i |
149171 TtInfo gmtoff isdst ttype abbr <- ttinfos,
150− i <- maybeToList . lookup abbr . zip abbrStrings .
151− scanl (+) 0 $ map ((+ 1) . length) abbrStrings]
172+ i <- maybeToList $ lookup abbr abbrAssocs]
152173
153174putPosixTZ :: Maybe String -> Put
154175putPosixTZ posix = do
155176 putWord8 10
156− putASCII $ fromMaybe "" posix
177+ putASCII "POSIX TZ string"$ fromMaybe "" posix
157178 putWord8 10
158179
159180putTtInfo :: TtInfo Int -> Put
@@ -186,5 +207,11 @@ putBool True = putWord8 1
186207uniq :: Eq a => [a] -> [a]
187208uniq = map head . group
188209
189−putASCII :: String -> Put
190−putASCII = putByteString . B.pack . map (fromIntegral . fromEnum)
210+putASCII :: String -> String -> Put
211+putASCII what =
212+ putByteString . B.pack . map (fromIntegral . verify . fromEnum)
213+ where
214+ verify c
215+ | c >= 32 && c <= 126 = c
216+ | otherwise = error $ "Cannot render TimeZoneSeries: " ++
217+ what ++ " contains non-ASCII characters"
Data/Time/LocalTime/TimeZone/Olson/Types.hs+102−57
@@ -1,7 +1,9 @@
1+{-# OPTIONS_HADDOCK hide #-}
2+{-# LANGUAGE CPP #-}
13-----------------------------------------------------------------------------
24-- |
35-- Module : Data.Time.LocalTime.TimeZone.Olson.Types
4−-- Copyright : Yitzchak Gale 2010
6+-- Copyright : Yitzchak Gale 2018
57--
68-- Maintainer : Yitzchak Gale <gale@sefer.org>
79-- Portability : portable
@@ -9,80 +11,89 @@
911-- Data types to represent timezone data as used in Olson timezone
1012-- files, and as specified by the tzfile(5) man page on Unix-like
1113-- systems. For more information about this format, see
12−-- http://www.twinsun.com/tz/tz-link.htm.
14+-- <http://www.twinsun.com/tz/tz-link.htm>.
1315--
1416-- Both Version 1 and Version 2 timezone data can be represented.
1517
16−{-
17−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
18−
19−Redistribution and use in source and binary forms, with or without
20−modification, are permitted provided that the following conditions are
21−met:
22−
23− * Redistributions of source code must retain the above copyright
24− notice, this list of conditions and the following disclaimer.
25−
26− * Redistributions in binary form must reproduce the above
27− copyright notice, this list of conditions and the following
28− disclaimer in the documentation and/or other materials provided
29− with the distribution.
30−
31− * Neither the name of Yitzchak Gale nor the names of other
32− contributors may be used to endorse or promote products derived
33− from this software without specific prior written permission.
34−
35−THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
36−"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
37−LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
38−A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
39−OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
40−SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
41−LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
42−DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
43−THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44−(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
45−OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46−-}
18+{- Copyright (c) 2018 Yitzchak Gale. All rights reserved.
19+For licensing information, see the BSD3-style license in the file
20+LICENSE that was originally distributed by the author together with
21+this file. -}
4722
4823module Data.Time.LocalTime.TimeZone.Olson.Types
4924(
25+ -- * Olson timezone datatypes
5026 OlsonData(..),
5127 Transition(..),
5228 TransitionType(..),
5329 TtInfo(..),
54− LeapInfo(..)
30+ LeapInfo(..),
31+
32+ -- ** Size limits for Olson timezone data
33+ SizeLimits(..),
34+ defaultLimits,
35+ limitsNoSolar,
36+ noLimits
5537)
5638where
5739
40+#if MIN_VERSION_base(4,11,0)
41+import Data.Semigroup (Semigroup(..))
42+#endif
5843import Data.Monoid (Monoid(..))
5944import Control.Monad (mplus)
6045
6146-- | @OlsonData@ represents a full set of timezone data for a location.
6247--
63−-- @OlsonData@ can represent timezone data from files in Version 1 format
64−-- or Version 2 format. Version 1 format files can only contain timestamp
65−-- values that can be represented in less than 32 bits, and cannot contain
66−-- a POSIX TZ string.
48+-- @OlsonData@ can represent timezone data from files in Version 1, 2,
49+-- or 3 format.
50+--
51+-- Version 1 format files can only contain timestamp values that can
52+-- be represented in less than 32 bits, and cannot contain a POSIX TZ
53+-- string.
6754--
68−-- In a Version 2 format file, the timezone data is split into two parts.
69−-- The first part contains timezone data for which all timestamp values
70−-- can be represented in less 32 bits, and the second part contains
71−-- timezone data for which 32 bits or more are required to represent
72−-- timestamp values. The POSIX TZ string, if present, can only be rendered
73−-- in a Version 2 file, and appears after both parts of the timezone data.
55+-- In a Version 2 format file, the timezone data is split into two
56+-- parts. The first part contains timezone data for which all
57+-- timestamp values can be represented in less than 32 bits, and the
58+-- second part contains timezone data for which 32 bits or more are
59+-- required to represent timestamp values. The POSIX TZ string, if
60+-- present, can only be rendered in a Version 2 file, and appears
61+-- after both parts of the timezone data.
62+--
63+-- Version 3 format files relax certain syntax requirements for the
64+-- POSIX TZ string. Since we represent the POSIX TZ string as an
65+-- unparsed 'String', Version 3 is identical to Version 2 for our
66+-- purposes.
7467data OlsonData =
75− OlsonData
76− [Transition]
77− [TtInfo String]
78− [LeapInfo]
79− (Maybe String) -- ^ Optional POSIX TZ string for the last @Transition@
68+ OlsonData {
69+ olsonTransitions :: [Transition],
70+ olsonTypes :: [TtInfo String],
71+ olsonLeaps :: [LeapInfo],
72+ olsonPosixTZ :: (Maybe String)
73+ -- ^ Optional POSIX TZ string for
74+ -- times after the last @Transition@
75+ }
8076 deriving (Eq, Show)
8177
78+#if MIN_VERSION_base(4,11,0)
79+instance Semigroup OlsonData where
80+ OlsonData a b c d <> OlsonData a' b' c' d' =
81+ OlsonData (a ++ map (shiftBy $ length b) a')
82+ (b ++ b') (c ++ c') (d `mplus` d')
83+ where
84+ shiftBy n trans = trans {transIndex = n + transIndex trans}
85+
8286instance Monoid OlsonData where
8387 mempty = OlsonData [] [] [] Nothing
84− mappend (OlsonData a b c d ) (OlsonData a' b' c' d')
85− = OlsonData (a++a') (b++b') (c++c') (d `mplus` d')
88+#else
89+instance Monoid OlsonData where
90+ mempty = OlsonData [] [] [] Nothing
91+ mappend (OlsonData a b c d ) (OlsonData a' b' c' d') =
92+ OlsonData (a ++ map (shiftBy $ length b) a')
93+ (b ++ b') (c ++ c') (d `mplus` d')
94+ where
95+ shiftBy n trans = trans {transIndex = n + transIndex trans}
96+#endif
8697
8798-- | A @Transition@ represents a moment when the clocks change in a
8899-- timezone. It consists of a Unix timestamp value that indicates the
@@ -93,8 +104,9 @@ data Transition =
93104 Transition
94105 {transTime :: Integer, -- ^ Unix timestamp indicating the time
95106 -- when the clocks change
96− transIndex :: Int} -- ^ 0-based index in the list of @TtInfo@
107+ transIndex :: Int -- ^ 0-based index in the list of @TtInfo@
97108 -- that describes the new time
109+ }
98110 deriving (Eq, Show)
99111
100112-- | A @TransitionType@ is historical information about whether the
@@ -104,7 +116,7 @@ data Transition =
104116-- zone. This historical trivia may seem rather boring, but
105117-- unfortunately it is needed to interpret a POSIX-style TZ string
106118-- timezone specification correctly.
107−data TransitionType = Std | Wall | UTC | UnknownType
119+data TransitionType = Std | Wall | UTC
108120 deriving (Eq, Ord, Show)
109121
110122-- | A @TtInfo@ is a specification of local time in a timezone for
@@ -119,15 +131,48 @@ data TtInfo abbr =
119131 -- in seconds
120132 tt_isdst :: Bool, -- ^ True if local clocks are summer time
121133 tt_ttype :: TransitionType,
122− tt_abbr :: abbr} -- ^ The timezone abbreviation string.
123− deriving (Eq, Show)
134+ tt_abbr :: abbr -- ^ The timezone abbreviation string.
135+ }
136+ deriving (Eq, Ord, Show)
124137
125138-- | Olson timezone files can contain leap second specifications, though
126−-- the official files have not done so since the late 1980s.
139+-- most do not.
127140data LeapInfo =
128141 LeapInfo
129142 {leapTime :: Integer, -- ^ A Unix timestamp indicating the time
130143 -- that the leap second occurred
131− leapOffset :: Int} -- ^ The new total offset of UTC from UT1
144+ leapOffset :: Int -- ^ The new total offset of UTC from UT1
132145 -- after this leap second
146+ }
133147 deriving (Eq, Show)
148+
149+-- | The reference C implentation imposes size limits on the data
150+-- structures in a timezone file.
151+data SizeLimits = SizeLimits
152+ {maxTimes :: Maybe Int, -- ^ The maximum number of transition times
153+ maxTypes :: Maybe Int, -- ^ The maximum number of TtInfo
154+ -- clock settings
155+ maxAbbrChars :: Maybe Int, -- ^ The maximum total number of bytes in
156+ -- all timezone abbreviations.
157+ maxLeaps :: Maybe Int -- ^ The maximum number of leap second
158+ -- specifications.
159+ }
160+
161+-- | The size limits in @defaultLimits@ are taken from the file
162+-- tzfile.h from tzcode version 2010f. These are the limits for the C
163+-- implementation on many platforms.
164+defaultLimits :: SizeLimits
165+defaultLimits = SizeLimits { maxTimes = Just 1200, maxTypes = Just 256,
166+ maxAbbrChars = Just 50, maxLeaps = Just 50 }
167+
168+-- | @limitsNoSolar@ contains the tighter size limits imposed on some
169+-- platforms that do not allow timezones that are based on solar time.
170+limitsNoSolar :: SizeLimits
171+limitsNoSolar = defaultLimits { maxTypes = Just 20 }
172+
173+-- | @noLimits@ imposes no size limits. If you use @noLimits@ when
174+-- parsing, you may exhaust all available memory when reading a faulty
175+-- or malicious timezone file. If you use @noLimits@ when rendering,
176+-- the rendered timezone file might not be readable on some systems.
177+noLimits :: SizeLimits
178+noLimits = SizeLimits Nothing Nothing Nothing Nothing
LICENSEnew file+29
@@ -0,0 +1,29 @@
1+Copyright (c) 2010-2018 Yitzchak Gale. All rights reserved.
2+
3+Redistribution and use in source and binary forms, with or without
4+modification, are permitted provided that the following conditions are
5+met:
6+
7+ * Redistributions of source code must retain the above copyright
8+ notice, this list of conditions and the following disclaimer.
9+
10+ * Redistributions in binary form must reproduce the above
11+ copyright notice, this list of conditions and the following
12+ disclaimer in the documentation and/or other materials provided
13+ with the distribution.
14+
15+ * Neither the name of Yitzchak Gale nor the names of other
16+ contributors may be used to endorse or promote products derived
17+ from this software without specific prior written permission.
18+
19+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
20+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
21+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
22+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
23+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
24+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
25+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
26+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
27+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
28+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
README.mdnew file+43
@@ -0,0 +1,43 @@
1+# timezone-olson
2+
3+On Hackage: [timezone-olson](http://hackage.haskell.org/package/timezone-olson)
4+
5+This package provides a parser and renderer for binary Olson timezone
6+files whose format is specified by the tzfile(5) man page on Unix-like
7+systems. For more information about this format, see
8+[the IANA timezone database site](https://www.iana.org/time-zones).
9+
10+Functions are provided for
11+converting the parsed data into `TimeZoneSeries` objects from the
12+[timezone-series](http://hackage.haskell.org/package/timezone-series)
13+package. On many platforms, binary Olson timezone
14+files suitable for use with this package are available in the
15+directory `/usr/share/zoneinfo` and its subdirectories on your computer.
16+
17+See also the
18+[timezone-olson-th](http://hackage.haskell.org/package/timezone-olson-th)
19+package for a way to include timezone informaton from a binary Olson
20+timezone file at compile time.
21+
22+Copyright (c) 2010-2018 Yitzchak Gale. All rights reserved.
23+
24+For licensing information, see the BSD3-style license in the file
25+LICENSE that was originally distributed by the author together with
26+this file.
27+
28+This package is part of the [time-ng project](http://projects.haskell.org/time-ng/).
29+
30+## Testing utilities
31+
32+This package also provides two Haskell files, each of which can be
33+compiled into a command-line utility that might be helpful for testing
34+purposes.
35+
36+zhdump.hs: A clone of zdump(8), including most of its bugs, that
37+ is usually present on systems that have an Olson timezone
38+ database, except hzdump takes paths to timezone files
39+ instead of timezone identifiers on the command line.
40+
41+catTZ: Read and parse a timezone file, then render it. With the
42+ -i flag, interprets the timezone data as a TimeZoneSeries
43+ object before rendering.
Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
catTZ.hsnew file+14
@@ -0,0 +1,14 @@
1+-- | Parse an Olson timezone file, then render it.
2+-- If -i is specified, interpret it as a TimeZoneSeries before rendering.
3+module Main where
4+
5+import Data.Time.LocalTime.TimeZone.Olson
6+import Data.Time.LocalTime.TimeZone.Series
7+import System.Environment
8+
9+main = do
10+ args <- getArgs
11+ if head args == "-i"
12+ then getTimeZoneSeriesFromOlsonFile (args !! 1) >>=
13+ renderTimeZoneSeriesToOlsonFile (args !! 2)
14+ else getOlsonFromFile (args !! 0) >>= renderOlsonToFile (args !! 1)
hzdump.hs+30−15
@@ -1,6 +1,7 @@
11-- | A clone in Haskell of the zdump(8) command, including most of
2−-- the bugs. This is useful for testing the Haskell implementation
3−-- of an Olson timezone parser and renderer against the reference
2+-- the bugs, that takes paths to Olson files instead of timezone
3+-- names. This is useful for testing the Haskell implementation
4+-- of the Olson timezone parser and renderer against the reference
45-- implementation in C. But less useful than it might seem at first,
56-- because Haskell rounds historical solar mean timezones to the
67-- nearest minute, whereas the C implementation rounds to the nearest
@@ -8,22 +9,24 @@
89module Main where
910
1011import Data.Time.LocalTime.TimeZone.Series
11−import Data.Time.LocalTime.TimeZone.Olson.Types
12−import Data.Time.LocalTime.TimeZone.Olson.Parse
12+import Data.Time.LocalTime.TimeZone.Olson
1313import Data.Time
1414import Data.Maybe (listToMaybe)
1515import System.Environment (getArgs)
1616import System.Exit (exitWith, exitSuccess, ExitCode(ExitFailure))
1717import System.Locale (defaultTimeLocale)
18+import Control.Monad (guard)
1819
1920version :: IO a
2021version = do
21− putStrLn "hzdump version 0.1, clone of zdump 1.7 with -c option fixed"
22+ putStrLn "hzdump version 0.1, clone of zdump 1.7 -c option fixed,"
23+ putStrLn "takes zone file paths instead of zone specifications"
2224 exitSuccess
2325
2426usage :: IO a
2527usage = do
26− putStrLn "usage: hzdump [--version] [-v] [-c cutoff] zonename ..."
28+ putStrLn "usage: hzdump [--version] [-v] [-c cutoff] zone-file-path ..."
29+ putStrLn " where cutoff is lo-year,hi-year or hi-year"
2730 exitWith $ ExitFailure 1
2831
2932illegalOpt :: Char -> IO a -- sic
@@ -31,7 +34,8 @@ illegalOpt opt = do
3134 putStrLn $ "hzdump: illegal option -- " ++ [opt]
3235 usage
3336
34−data Option = Version | Illegal Char | Verbose (Maybe Integer) | Now
37+data Option = Version | Illegal Char |
38+ Verbose (Maybe Integer, Maybe Integer) | Now
3539 deriving (Eq, Ord, Show)
3640
3741main = do
@@ -39,7 +43,7 @@ main = do
3943 (getTimes, displayTime) <- case opts of
4044 Version -> version
4145 Illegal opt -> illegalOpt opt
42− Verbose yr -> return (transitionTimesUntil yr, displayVerbose)
46+ Verbose rng -> return (transitionTimes rng, displayVerbose)
4347 _ -> do now <- getCurrentTime
4448 return (const [now], displayConcise)
4549 tzss <- mapM getZone zones
@@ -54,17 +58,26 @@ main = do
5458
5559parseArgs :: [String] -> (Option, [FilePath])
5660parseArgs args | "--version" `elem` args = (Version, [])
57−parseArgs args = getOpts False Nothing args
61+parseArgs args = getOpts False (Nothing, Nothing) args
5862 where
5963 getOpts _ cutoff ("-v":args) = getOpts True cutoff args
60− getOpts v Nothing ("-c":c:args) = maybe (Illegal 'c', [])
61− (\y -> getOpts v (Just y) args) $
62− maybeRead c
64+ getOpts v _ ("-c":c:args) = maybe (Illegal 'c', [])
65+ (\y -> getOpts v y args) $
66+ parseCutoff c
6367 getOpts v cutoff ("--":zones ) = (opts v cutoff, zones)
6468 getOpts _ _ (('-':x:_):_) = (Illegal x, [])
6569 getOpts v cutoff zones = (opts v cutoff, zones)
70+
6671 opts v cutoff = if v then Verbose cutoff else Now
6772
73+ parseCutoff c = let p0 = listToMaybe $ reads c
74+ los = fmap fst p0
75+ his = do str <- fmap snd p0
76+ let (comma, str1) = splitAt 1 str
77+ guard $ comma == ","
78+ maybeRead str1
79+ in los >> Just (maybe (Nothing, los) ((,) los . Just) his)
80+
6881displayConcise :: TimeZoneSeries -> UTCTime -> String
6982displayConcise tzs t = formatTime defaultTimeLocale format $
7083 ZoneSeriesTime t tzs
@@ -77,9 +90,11 @@ displayVerbose tzs t = concat [displayConcise utcTZ t, " = ",
7790 where
7891 isdst = timeZoneSummerOnly $ timeZoneFromSeries tzs t
7992
80−transitionTimesUntil :: Maybe Integer -> TimeZoneSeries -> [UTCTime]
81−transitionTimesUntil yr =
82− maybe id (dropWhile . (>=)) (fmap happyNewYear yr) .
93+transitionTimes :: (Maybe Integer, Maybe Integer) -> TimeZoneSeries ->
94+ [UTCTime]
95+transitionTimes (lo, hi) =
96+ maybe id (takeWhile . (>)) (fmap happyNewYear hi) .
97+ maybe id (dropWhile . (>)) (fmap happyNewYear lo) .
8398 addBugs . addPrevSecond . actualTransitions
8499 where
85100 happyNewYear y = UTCTime (fromGregorian y 1 1) 0
timezone-olson.cabalnew file+45
@@ -0,0 +1,45 @@
1+Name: timezone-olson
2+Version: 0.1.9
3+Synopsis: A pure Haskell parser and renderer for binary Olson timezone files
4+Description: A parser and renderer for binary Olson timezone
5+ files whose format is specified by the tzfile(5)
6+ man page on Unix-like systems. For more
7+ information about this format, see
8+ <http://www.iana.org/time-zones/repository/tz-link.html>.
9+ Functions are provided for converting the parsed data into
10+ 'TimeZoneSeries' objects from the timezone-series
11+ package. On many platforms, binary Olson timezone
12+ files suitable for use with this package are
13+ available in the directory /usr/share/zoneinfo
14+ and its subdirectories on your computer.
15+ For a way to read binary Olson timezone files at compile
16+ time, see the timezone-olson-th package
17+ (<http://hackage.haskell.org/package/timezone-olson-th>).
18+Homepage: http://projects.haskell.org/time-ng/
19+License: BSD3
20+License-file: LICENSE
21+Author: Yitzchak Gale
22+Maintainer: yitz@sefer.org
23+Copyright: Copyright (c) 2010-2018 Yitzchak Gale. All rights reserved.
24+Category: Data
25+Build-type: Simple
26+Extra-source-files: README.md, catTZ.hs, hzdump.hs
27+Cabal-version: >=1.10
28+Tested-with: GHC > 8.0 && < 8.5
29+
30+Source-repository HEAD
31+ type: git
32+ location: https://github.com/ygale/timezone-olson.git
33+
34+Library
35+ Default-language: Haskell2010
36+ Exposed-modules: Data.Time.LocalTime.TimeZone.Olson
37+ , Data.Time.LocalTime.TimeZone.Olson.Parse
38+ , Data.Time.LocalTime.TimeZone.Olson.Render
39+ , Data.Time.LocalTime.TimeZone.Olson.Types
40+ Build-depends: base >= 3.0 && < 5.0,
41+ timezone-series >= 0.1.0 && < 0.2,
42+ time >= 1.6 && < 1.10,
43+ binary >= 0.4.1 && < 0.9,
44+ bytestring >= 0.9 && < 1.0,
45+ extensible-exceptions >= 0.1.0 && < 0.2