git.iko.soy/typeable/timezone-olson
on master help sign in
12 files changed+577−181
since splitsnapshots
unifiedsplit
.gitignore+16 .travis.yml+107 Data/Time/LocalTime/TimeZone/Olson.hs+8−14 Data/Time/LocalTime/TimeZone/Olson/Parse.hs+86−48 Data/Time/LocalTime/TimeZone/Olson/Render.hs+125−73 Data/Time/LocalTime/TimeZone/Olson/Types.hs+101−31 LICENSE+1−1 README.md+43 Setup.hs+2 catTZ.hs+14 hzdump.hs+29−14 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.hs+8−14
@@ -1,7 +1,7 @@
11-----------------------------------------------------------------------------
22-- |
33-- Module : Data.Time.LocalTime.TimeZone.Olson
4−-- Copyright : Yitzchak Gale 2010
4+-- Copyright : Yitzchak Gale 2018
55--
66-- Maintainer : Yitzchak Gale <gale@sefer.org>
77-- Portability : portable
@@ -9,26 +9,20 @@
99-- A parser and renderer for binary Olson timezone files whose format
1010-- are specified by the tzfile(5) man page on Unix-like systems. For
1111-- more information about this format, see
12−-- http://www.twinsun.com/tz/tz-link.htm. Functions are provided for
12+-- <http://www.twinsun.com/tz/tz-link.htm>. Functions are provided for
1313-- converting the parsed data into @TimeZoneSeries@ and @TimeZone@
1414-- objects.
1515
16−{-
17−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
18−For licensing information, see the file LICENSE that was
19−originally distributed by the author together with this file.
20−-}
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. -}
2120
2221module Data.Time.LocalTime.TimeZone.Olson
2322(
24− -- * Olson timezone datatypes
25− module Data.Time.LocalTime.TimeZone.Olson.Types,
26−
27− -- * Parsing Olson timezone files
2823 module Data.Time.LocalTime.TimeZone.Olson.Parse,
29−
30− -- * Rendering Olson timezone files
31− module Data.Time.LocalTime.TimeZone.Olson.Render
24+ module Data.Time.LocalTime.TimeZone.Olson.Render,
25+ module Data.Time.LocalTime.TimeZone.Olson.Types
3226)
3327where
3428
Data/Time/LocalTime/TimeZone/Olson/Parse.hs+86−48
@@ -3,26 +3,25 @@
33-----------------------------------------------------------------------------
44-- |
55-- Module : Data.Time.LocalTime.TimeZone.Olson.Parse
6−-- Copyright : Yitzchak Gale 2010
6+-- Copyright : Yitzchak Gale 2018
77--
88-- Maintainer : Yitzchak Gale <gale@sefer.org>
99-- Portability : portable
1010--
11−-- A parser for binary Olson timezone files whose format are specified
11+-- A parser for binary Olson timezone files whose format is specified
1212-- by the tzfile(5) man page on Unix-like systems. For more
1313-- information about this format, see
14−-- http://www.twinsun.com/tz/tz-link.htm. Functions are provided for
15−-- converting the parsed data into @TimeZoneSeries@ and @TimeZone@
16−-- objects.
14+-- <http://www.twinsun.com/tz/tz-link.htm>. Functions are provided for
15+-- converting the parsed data into 'TimeZoneSeries' objects.
1716
18−{-
19−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
20−For licensing information, see the file LICENSE that was
21−originally distributed by the author together with this file.
22−-}
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. -}
2321
2422module Data.Time.LocalTime.TimeZone.Olson.Parse
2523(
24+ -- * Parsing Olson timezone files
2625 getTimeZoneSeriesFromOlsonFile,
2726 getOlsonFromFile,
2827 olsonToTimeZoneSeries,
@@ -40,11 +39,14 @@ import Data.Binary.Get (Get, runGet, getWord8, getWord32be, getWord64be,
4039import qualified Data.ByteString as B
4140import qualified Data.ByteString.Lazy as L
4241import Data.Monoid (mappend)
42+import Data.List (sortBy, groupBy)
4343import Data.Maybe (listToMaybe, fromMaybe)
4444import Data.Word (Word8)
4545import Data.Int (Int32, Int64)
46+import Data.Ord (comparing)
47+import Data.Function (on)
4648import Data.Typeable (Typeable)
47−import Control.Monad (guard, replicateM, replicateM_)
49+import Control.Monad (guard, replicateM, replicateM_, when)
4850import Control.Exception.Extensible (try, throw, Exception, ErrorCall)
4951
5052-- | An exception indicating that the binary data being parsed was not
@@ -58,8 +60,8 @@ instance Exception OlsonError
5860-- | Convert parsed Olson timezone data into a @TimeZoneSeries@.
5961olsonToTimeZoneSeries :: OlsonData -> Maybe TimeZoneSeries
6062olsonToTimeZoneSeries (OlsonData ttimes ttinfos@(dflt0:_) _ _) =
61− fmap (TimeZoneSeries $ mkTZ dflt) . mapM (lookupTZ ttinfos) $
62− reverse ttimes
63+ fmap (TimeZoneSeries $ mkTZ dflt) . mapM (lookupTZ ttinfos) .
64+ uniqTimes . sortBy futureToPast $ ttimes
6365 where
6466 dflt = fromMaybe dflt0 . listToMaybe $ filter isStd ttinfos
6567 isStd (TtInfo _ isdst _ _) = not isdst
@@ -68,53 +70,74 @@ olsonToTimeZoneSeries (OlsonData ttimes ttinfos@(dflt0:_) _ _) =
6870 lookupTZ ttinfos ttime = fmap (((,) $ toUTC ttime) . mkTZ) . listToMaybe $
6971 drop (transIndex ttime) ttinfos
7072 toUTC = posixSecondsToUTCTime . fromIntegral . transTime
73+ uniqTimes = map last . groupBy ((==) `on` transTime)
74+ futureToPast = comparing $ negate . transTime
7175olsonToTimeZoneSeries _ = Nothing
7276
7377-- | Read timezone data from a binary Olson timezone file and convert
74−-- 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.
7586getTimeZoneSeriesFromOlsonFile :: FilePath -> IO TimeZoneSeries
7687getTimeZoneSeriesFromOlsonFile fp = getOlsonFromFile fp >>=
7788 maybe (throwOlson fp "no timezone found in OlsonData") return .
7889 olsonToTimeZoneSeries
7990
8091-- | 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.
8197getOlsonFromFile :: FilePath -> IO OlsonData
8298getOlsonFromFile fp = do
83− e <- try . fmap (runGet getOlson) $ L.readFile fp
99+ e <- try . fmap (runGet $ getOlson defaultLimits) $ L.readFile fp
84100 either (formatError fp) return e
85101
86102formatError :: FilePath -> ErrorCall -> IO a
87103formatError fp e = throwOlson fp $ show e
88104
89105-- | A binary parser for binary Olson timezone files
90−getOlson :: Get OlsonData
91−getOlson = do
92− getByteString 4
93− >>= verify ((== "TZif") . toASCII . B.unpack) "missing magic number"
94− version <- getWord8
95− >>= verify (`elem` [0, 50]) "invalid version number"
96− replicateM_ 15 getWord8 -- padding nulls
97− getOlsonParts version
98−
99−-- There is one part for Version 1 format data, and two parts and a POSIX
100−-- TZ string for Version 2 format data
101−getOlsonParts :: Word8 -> Get OlsonData
102−getOlsonParts 0 = getOlsonPart get32bitInteger
103−getOlsonParts 50 = do part1 <- getOlsonPart get32bitInteger
104− part2 <- getOlsonPart get64bitInteger
105− posixTZ <- getPosixTZ
106− return $ part1 `mappend` part2 `mappend` posixTZ
107−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
108120
109121-- Parse the part of an Olson file that contains timezone data
110−getOlsonPart :: Integral a => Get a -> Get OlsonData
111−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
112129 tzh_ttisgmtcnt <- get32bitInt
113130 tzh_ttisstdcnt <- get32bitInt
114131 tzh_leapcnt <- get32bitInt
132+ >>= verify (withinLimit maxLeaps) "too many leap second specifications"
115133 tzh_timecnt <- get32bitInt
134+ >>= verify (withinLimit maxTimes) "too many timezone transitions"
116135 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
117139 tzh_charcnt <- get32bitInt
140+ >>= verify (withinLimit maxAbbrChars) "too many tilezone specifiers"
118141 times <- fmap (map toInteger) $ replicateM tzh_timecnt getTime
119142 indexes <- replicateM tzh_timecnt get8bitInt
120143 ttinfos <- replicateM tzh_typecnt getTtInfo
@@ -122,22 +145,32 @@ getOlsonPart getTime = do
122145 leaps <- replicateM tzh_leapcnt $ getLeapInfo getTime
123146 isstds <- replicateM tzh_ttisstdcnt getBool
124147 isgmts <- replicateM tzh_ttisgmtcnt getBool
125− return $ OlsonData
126− (zipWith Transition times indexes)
127− (map (flip lookupAbbr abbr_chars) . zipWith setTtype ttinfos .
128− (++ repeat UnknownType) $
129− zipWith boolsToTType (map Just isstds ++ repeat Nothing) isgmts)
130− leaps
131− 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+ )
132157 where
158+ withinLimit limit value = maybe True (value <=) $ limit limits
133159 lookupAbbr (TtInfo gmtoff isdst ttype abbrind) =
134160 TtInfo gmtoff isdst ttype . takeWhile (/= '\NUL') . drop abbrind
135161 setTtype ttinfo ttype = ttinfo {tt_ttype = ttype}
136− boolsToTType _ isgmt | isgmt = UTC
137− boolsToTType isstd _ = maybe UnknownType stdOrWall isstd
138− stdOrWall isstd
139− | isstd = Std
140− | 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
141174
142175-- Parse a POSIX-style TZ string.
143176-- We don't try to understand the TZ string, we just pass it along whole.
@@ -158,7 +191,7 @@ getTtInfo = do
158191 gmtoff <- get32bitInt
159192 isdst <- getBool
160193 abbrind <- get8bitInt
161− return $ TtInfo gmtoff isdst UnknownType abbrind
194+ return $ TtInfo gmtoff isdst Wall abbrind
162195
163196-- Parse leap second info. (usually not used)
164197getLeapInfo :: Integral a => Get a -> Get LeapInfo
@@ -201,6 +234,11 @@ verify pred msg val
201234 | pred val = return val
202235 | otherwise = error msg
203236
237+verify_ :: Monad m => (a -> Bool) -> String -> a -> m ()
238+verify_ pred msg val
239+ | pred val = return ()
240+ | otherwise = error msg
241+
204242throwOlson :: FilePath -> String -> IO a
205243throwOlson fp msg = throw . OlsonError $
206244 fp ++ ": invalid timezone file: " ++ msg
Data/Time/LocalTime/TimeZone/Olson/Render.hs+125−73
@@ -2,37 +2,42 @@
22-----------------------------------------------------------------------------
33-- |
44-- Module : Data.Time.LocalTime.TimeZone.Olson.Render
5−-- Copyright : Yitzchak Gale 2010
5+-- Copyright : Yitzchak Gale 2018
66--
77-- Maintainer : Yitzchak Gale <gale@sefer.org>
88-- Portability : portable
99--
10−-- Render Olson timezone data in the standard binary format as
11−-- used in Olson timezone files, and as specified by the
12−-- tzfile(5) man page on Unix-like systems. For more information
13−-- about this format, see http://www.twinsun.com/tz/tz-link.htm.
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>.
1414
15−{-
16−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
17−For licensing information, see the file LICENSE that was
18−originally distributed by the author together with this file.
19−-}
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. -}
2019
2120module Data.Time.LocalTime.TimeZone.Olson.Render
2221(
23− -- * Rendering Olson timezone data
22+ -- * Rendering Olson timezone files
2423 -- | If any of the transition times or leap second times specified
2524 -- require more than a 32-bit integer to represent as a Unix
2625 -- timestamp, or if a POSIX-style TZ string is specified, timezone
2726 -- data is rendered using Version 2 format. Otherwise, the timezone data
2827 -- is rendered using Version 1 format.
28+ renderTimeZoneSeriesToOlsonFile,
29+ timeZoneSeriesToOlson,
2930 renderOlsonToFile,
31+ verifyOlsonLimits,
3032 putOlson,
31− splitOlson
33+ extractOlsonV1
3234)
3335where
3436
3537import 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)
3641import Data.Binary.Put (Put, runPut, putByteString, putWord8, flush,
3742 putWord32be, putWord64be)
3843import qualified Data.ByteString as B
@@ -40,78 +45,117 @@ import qualified Data.ByteString.Lazy as L
4045import Data.List (partition, sortBy, sort, group)
4146import Data.Ord (comparing)
4247import Data.Word (Word8)
43−import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe)
48+import Data.Maybe (listToMaybe, maybeToList, isNothing, fromMaybe, catMaybes)
4449import Data.Monoid (mempty)
45−import Control.Monad (guard, replicateM_)
50+import Control.Monad (guard, replicateM_, unless)
4651
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
4798
4899-- | 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.
49105renderOlsonToFile :: FilePath -> OlsonData -> IO ()
50−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
51110
52111-- | Render Olson timezone data in binary Olson timezone file format
53112-- as a lazy @ByteString@
54−putOlson :: OlsonData -> Put
55−putOlson olson = do
56− putASCII "TZif"
57− putWord8 version
58− putByteString . B.pack $ replicate 15 0 -- padding nulls
59− putOlsonParts version olson1 olson2 posix
60− 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
61118 where
62− (olson1, olson2, posix) = splitOlson olson
63− version
64− | olson2 /= mempty = 50
65− | otherwise = maybe 0 (const 50) $ posix >>= guard . not . null
66−
67−-- | Split Olson timezone data into three parts: timezone data that can
68−-- be rendered using Version 1 format, timezone data that can only be
69−-- rendered using Version 2 format, and the POSIX-style TZ string
70−-- if specified
71−splitOlson :: OlsonData -> (OlsonData, OlsonData, Maybe String)
72−splitOlson (OlsonData transs ttinfos leaps posix) =
73− (OlsonData transs1 ttinfos1 leaps1 Nothing,
74− OlsonData transs2 ttinfos2 leaps2 Nothing,
75− 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
76128 where
77129 cutoff = 0x80000000 -- 2^31
78− ( leaps1 , leaps2 ) = partition ((>= cutoff) . leapTime) leaps
79− (transs1', transs2') = partition ((>= cutoff) . transTime) transs
80− assoc1 = mkAssoc [0] transs1'
81− assoc2 = mkAssoc [] transs2'
82− transs1 = mkTranss transs1' assoc1
83− transs2 = mkTranss transs2' assoc2
84− ttinfos1 = mkTtinfos assoc1
85− ttinfos2 = mkTtinfos assoc2
86− 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
87135 (sortBy (comparing $ fmap tt_ttype . listToMaybe . flip drop ttinfos) .
88− uniq . sort . (prepend ++) $ map transIndex transs')
136+ uniq . sort . (0 :) $ map transIndex transs1')
89137 [0..]
90− mkTranss transs' assoc = [t {transIndex = i} |
91− t <- transs', i <- maybeToList $ lookup (transIndex t) assoc]
92− mkTtinfos assoc = map snd . dropWhile (isNothing . fst) .
93− sortBy (comparing fst) $ zip (map (flip lookup assoc) [0..]) ttinfos
94−
95−putOlsonParts :: Word8 -> OlsonData -> OlsonData -> Maybe String -> Put
96−putOlsonParts 0 olson1 _ _ = putOlsonPart put32bitIntegral olson1
97−putOlsonParts _ olson1 olson2 posix = do
98− putOlsonPart put32bitIntegral olson1
99− putOlsonPart put64bitIntegral olson2
100− putPosixTZ posix
101−
102−putOlsonPart :: (Integer -> Put) -> OlsonData -> Put
103−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
104148 replicateM_ 2 $ putCount ttinfosWithTtype
105− -- tzh_ttisgmtcnt
106− -- tzh_ttisstdcnt
107− putCount leaps -- tzh_leapcnt
108− putCount transs -- tzh_timecnt
109− putCount ttinfos -- tzh_typecnt
110− 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
111155 mapM_ (putTime . transTime ) transs
112156 mapM_ (put8bitIntegral . transIndex) transs
113157 mapM_ putTtInfo ttinfosIndexed
114− putASCII abbrChars
158+ mapM_ putAbbr abbrStrings
115159 mapM_ (putLeapInfo putTime) leaps
116160 mapM_ (putBool . (== Std) . tt_ttype) ttinfosWithTtype -- isstd
117161 mapM_ (putBool . (== UTC) . tt_ttype) ttinfosWithTtype -- isgmt
@@ -119,16 +163,18 @@ putOlsonPart putTime (OlsonData transs ttinfos leaps _) = do
119163 putCount = put32bitIntegral . length
120164 ttinfosWithTtype = takeWhile ((<= UTC) . tt_ttype) ttinfosIndexed
121165 abbrStrings = uniq . sort $ map tt_abbr ttinfos
122− 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
123170 ttinfosIndexed = [TtInfo gmtoff isdst ttype i |
124171 TtInfo gmtoff isdst ttype abbr <- ttinfos,
125− i <- maybeToList . lookup abbr . zip abbrStrings .
126− scanl (+) 0 $ map ((+ 1) . length) abbrStrings]
172+ i <- maybeToList $ lookup abbr abbrAssocs]
127173
128174putPosixTZ :: Maybe String -> Put
129175putPosixTZ posix = do
130176 putWord8 10
131− putASCII $ fromMaybe "" posix
177+ putASCII "POSIX TZ string"$ fromMaybe "" posix
132178 putWord8 10
133179
134180putTtInfo :: TtInfo Int -> Put
@@ -161,5 +207,11 @@ putBool True = putWord8 1
161207uniq :: Eq a => [a] -> [a]
162208uniq = map head . group
163209
164−putASCII :: String -> Put
165−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+101−31
@@ -1,8 +1,9 @@
11{-# OPTIONS_HADDOCK hide #-}
2+{-# LANGUAGE CPP #-}
23-----------------------------------------------------------------------------
34-- |
45-- Module : Data.Time.LocalTime.TimeZone.Olson.Types
5−-- Copyright : Yitzchak Gale 2010
6+-- Copyright : Yitzchak Gale 2018
67--
78-- Maintainer : Yitzchak Gale <gale@sefer.org>
89-- Portability : portable
@@ -10,54 +11,89 @@
1011-- Data types to represent timezone data as used in Olson timezone
1112-- files, and as specified by the tzfile(5) man page on Unix-like
1213-- systems. For more information about this format, see
13−-- http://www.twinsun.com/tz/tz-link.htm.
14+-- <http://www.twinsun.com/tz/tz-link.htm>.
1415--
1516-- Both Version 1 and Version 2 timezone data can be represented.
1617
17−{-
18−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
19−For licensing information, see the file LICENSE that was
20−originally distributed by the author together with this file.
21−-}
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. -}
2222
2323module Data.Time.LocalTime.TimeZone.Olson.Types
2424(
25+ -- * Olson timezone datatypes
2526 OlsonData(..),
2627 Transition(..),
2728 TransitionType(..),
2829 TtInfo(..),
29− LeapInfo(..)
30+ LeapInfo(..),
31+
32+ -- ** Size limits for Olson timezone data
33+ SizeLimits(..),
34+ defaultLimits,
35+ limitsNoSolar,
36+ noLimits
3037)
3138where
3239
40+#if MIN_VERSION_base(4,11,0)
41+import Data.Semigroup (Semigroup(..))
42+#endif
3343import Data.Monoid (Monoid(..))
3444import Control.Monad (mplus)
3545
3646-- | @OlsonData@ represents a full set of timezone data for a location.
3747--
38−-- @OlsonData@ can represent timezone data from files in Version 1 format
39−-- or Version 2 format. Version 1 format files can only contain timestamp
40−-- values that can be represented in less than 32 bits, and cannot contain
41−-- 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.
54+--
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.
4262--
43−-- In a Version 2 format file, the timezone data is split into two parts.
44−-- The first part contains timezone data for which all timestamp values
45−-- can be represented in less 32 bits, and the second part contains
46−-- timezone data for which 32 bits or more are required to represent
47−-- timestamp values. The POSIX TZ string, if present, can only be rendered
48−-- in a Version 2 file, and appears after both parts of the timezone data.
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.
4967data OlsonData =
50− OlsonData
51− [Transition]
52− [TtInfo String]
53− [LeapInfo]
54− (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+ }
5576 deriving (Eq, Show)
5677
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+
86+instance Monoid OlsonData where
87+ mempty = OlsonData [] [] [] Nothing
88+#else
5789instance Monoid OlsonData where
5890 mempty = OlsonData [] [] [] Nothing
59− mappend (OlsonData a b c d ) (OlsonData a' b' c' d')
60− = OlsonData (a++a') (b++b') (c++c') (d `mplus` d')
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
6197
6298-- | A @Transition@ represents a moment when the clocks change in a
6399-- timezone. It consists of a Unix timestamp value that indicates the
@@ -68,8 +104,9 @@ data Transition =
68104 Transition
69105 {transTime :: Integer, -- ^ Unix timestamp indicating the time
70106 -- when the clocks change
71− transIndex :: Int} -- ^ 0-based index in the list of @TtInfo@
107+ transIndex :: Int -- ^ 0-based index in the list of @TtInfo@
72108 -- that describes the new time
109+ }
73110 deriving (Eq, Show)
74111
75112-- | A @TransitionType@ is historical information about whether the
@@ -79,7 +116,7 @@ data Transition =
79116-- zone. This historical trivia may seem rather boring, but
80117-- unfortunately it is needed to interpret a POSIX-style TZ string
81118-- timezone specification correctly.
82−data TransitionType = Std | Wall | UTC | UnknownType
119+data TransitionType = Std | Wall | UTC
83120 deriving (Eq, Ord, Show)
84121
85122-- | A @TtInfo@ is a specification of local time in a timezone for
@@ -94,15 +131,48 @@ data TtInfo abbr =
94131 -- in seconds
95132 tt_isdst :: Bool, -- ^ True if local clocks are summer time
96133 tt_ttype :: TransitionType,
97− tt_abbr :: abbr} -- ^ The timezone abbreviation string.
98− deriving (Eq, Show)
134+ tt_abbr :: abbr -- ^ The timezone abbreviation string.
135+ }
136+ deriving (Eq, Ord, Show)
99137
100138-- | Olson timezone files can contain leap second specifications, though
101−-- the official files have not done so since the late 1980s.
139+-- most do not.
102140data LeapInfo =
103141 LeapInfo
104142 {leapTime :: Integer, -- ^ A Unix timestamp indicating the time
105143 -- that the leap second occurred
106− leapOffset :: Int} -- ^ The new total offset of UTC from UT1
144+ leapOffset :: Int -- ^ The new total offset of UTC from UT1
107145 -- after this leap second
146+ }
108147 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
LICENSE+1−1
@@ -1,4 +1,4 @@
1−Copyright (c) 2010 Yitzchak Gale. All rights reserved.
1+Copyright (c) 2010-2018 Yitzchak Gale. All rights reserved.
22
33Redistribution and use in source and binary forms, with or without
44modification, are permitted provided that the following conditions are
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+29−14
@@ -1,5 +1,6 @@
11-- | A clone in Haskell of the zdump(8) command, including most of
2−-- the bugs. This is useful for testing the Haskell implementation
2+-- the bugs, that takes paths to Olson files instead of timezone
3+-- names. This is useful for testing the Haskell implementation
34-- 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
@@ -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