on master help sign in
Create iso8601-attoparsec package
commit85046d203fbaa9404f1d63d0b57d59d41d179536 authorAdam Bergmark <adam@bergmark.nl> dateSun 26 Mar 2017, 23:40 (10 years ago) parent3e5b028
12 files changed+338−149
Data/Aeson/Internal/Time.hs+1−42 Data/Aeson/Parser/Time.hs+11−103 aeson.cabal+3−1 iso8601-attoparsec/Data/Attoparsec/Time.hs+149 iso8601-attoparsec/Data/Attoparsec/Time/Internal.hs+61 iso8601-attoparsec/LICENSE+30 iso8601-attoparsec/README.md+19 iso8601-attoparsec/iso8601-attoparsec.cabal+51 stack-lts6.yaml+3 stack-lts7.yaml+3 stack-lts8.yaml+3 stack-nightly.yaml+4−3
Data/Aeson/Internal/Time.hs+1−42
@@ -17,45 +17,4 @@ module Data.Aeson.Internal.Time
1717 , toTimeOfDay64
1818 ) where
1919
20−import Prelude ()
21−import Prelude.Compat
22−
23−import Data.Int (Int64)
24−import Data.Time
25−import Unsafe.Coerce (unsafeCoerce)
26−
27−#if MIN_VERSION_base(4,7,0)
28−
29−import Data.Fixed (Pico, Fixed(MkFixed))
30−
31−toPico :: Integer -> Pico
32−toPico = MkFixed
33−
34−fromPico :: Pico -> Integer
35−fromPico (MkFixed i) = i
36−
37−#else
38−
39−import Data.Fixed (Pico)
40−
41−toPico :: Integer -> Pico
42−toPico = unsafeCoerce
43−
44−fromPico :: Pico -> Integer
45−fromPico = unsafeCoerce
46−
47−#endif
48−
49−-- | Like TimeOfDay, but using a fixed-width integer for seconds.
50−data TimeOfDay64 = TOD {-# UNPACK #-} !Int
51− {-# UNPACK #-} !Int
52− {-# UNPACK #-} !Int64
53−
54−diffTimeOfDay64 :: DiffTime -> TimeOfDay64
55−diffTimeOfDay64 t = TOD (fromIntegral h) (fromIntegral m) s
56− where (h,mp) = fromIntegral pico `quotRem` 3600000000000000
57− (m,s) = mp `quotRem` 60000000000000
58− pico = unsafeCoerce t :: Integer
59−
60−toTimeOfDay64 :: TimeOfDay -> TimeOfDay64
61−toTimeOfDay64 (TimeOfDay h m s) = TOD h m (fromIntegral (fromPico s))
20+import Data.Attoparsec.Time.Internal
Data/Aeson/Parser/Time.hs+11−103
@@ -1,16 +1,3 @@
1−{-# LANGUAGE BangPatterns #-}
2−{-# LANGUAGE ScopedTypeVariables #-}
3−
4−-- |
5−-- Module: Data.Aeson.Parser.Time
6−-- Copyright: (c) 2015-2016 Bryan O'Sullivan
7−-- License: BSD3
8−-- Maintainer: Bryan O'Sullivan <bos@serpentine.com>
9−-- Stability: experimental
10−-- Portability: portable
11−--
12−-- Parsers for parsing dates and times.
13−
141module Data.Aeson.Parser.Time
152 (
163 run
@@ -25,120 +12,44 @@ module Data.Aeson.Parser.Time
2512import Prelude ()
2613import Prelude.Compat
2714
28−import Control.Applicative ((<|>))
29−import Control.Monad (void, when)
30−import Data.Aeson.Internal.Time (toPico)
31−import Data.Attoparsec.Text as A
32−import Data.Bits ((.&.))
33−import Data.Char (isDigit, ord)
34−import Data.Fixed (Pico)
35−import Data.Int (Int64)
36−import Data.Maybe (fromMaybe)
15+import Data.Attoparsec.Text (Parser)
3716import Data.Text (Text)
38−import Data.Time.Calendar (Day, fromGregorianValid)
17+import Data.Time.Calendar (Day)
3918import Data.Time.Clock (UTCTime(..))
4019import qualified Data.Aeson.Types.Internal as Aeson
41−import qualified Data.Text as T
20+import qualified Data.Attoparsec.Text as A
21+import qualified Data.Attoparsec.Time as T
4222import qualified Data.Time.LocalTime as Local
4323
4424-- | Run an attoparsec parser as an aeson parser.
4525run :: Parser a -> Text -> Aeson.Parser a
46−run p t = case A.parseOnly (p <* endOfInput) t of
26+run p t = case A.parseOnly (p <* A.endOfInput) t of
4727 Left err -> fail $ "could not parse date: " ++ err
4828 Right r -> return r
4929
5030-- | Parse a date of the form @[+,-]YYYY-MM-DD@.
5131day :: Parser Day
52−day = do
53− absOrNeg <- negate <$ char '-' <|> id <$ char '+' <|> pure id
54− y <- decimal <* char '-'
55− m <- twoDigits <* char '-'
56− d <- twoDigits
57− maybe (fail "invalid date") return (fromGregorianValid (absOrNeg y) m d)
58−
59−-- | Parse a two-digit integer (e.g. day of month, hour).
60−twoDigits :: Parser Int
61−twoDigits = do
62− a <- digit
63− b <- digit
64− let c2d c = ord c .&. 15
65− return $! c2d a * 10 + c2d b
32+day = T.day
6633
6734-- | Parse a time of the form @HH:MM[:SS[.SSS]]@.
6835timeOfDay :: Parser Local.TimeOfDay
69−timeOfDay = do
70− h <- twoDigits
71− m <- char ':' *> twoDigits
72− s <- option 0 (char ':' *> seconds)
73− if h < 24 && m < 60 && s < 61
74− then return (Local.TimeOfDay h m s)
75− else fail "invalid time"
76−
77−data T = T {-# UNPACK #-} !Int {-# UNPACK #-} !Int64
78−
79−-- | Parse a count of seconds, with the integer part being two digits
80−-- long.
81−seconds :: Parser Pico
82−seconds = do
83− real <- twoDigits
84− mc <- peekChar
85− case mc of
86− Just '.' -> do
87− t <- anyChar *> takeWhile1 isDigit
88− return $! parsePicos real t
89− _ -> return $! fromIntegral real
90− where
91− parsePicos a0 t = toPico (fromIntegral (t' * 10^n))
92− where T n t' = T.foldl' step (T 12 (fromIntegral a0)) t
93− step ma@(T m a) c
94− | m <= 0 = ma
95− | otherwise = T (m-1) (10 * a + fromIntegral (ord c) .&. 15)
36+timeOfDay = T.timeOfDay
9637
9738-- | Parse a time zone, and return 'Nothing' if the offset from UTC is
9839-- zero. (This makes some speedups possible.)
9940timeZone :: Parser (Maybe Local.TimeZone)
100−timeZone = do
101− let maybeSkip c = do ch <- peekChar'; when (ch == c) (void anyChar)
102− maybeSkip ' '
103− ch <- satisfy $ \c -> c == 'Z' || c == '+' || c == '-'
104− if ch == 'Z'
105− then return Nothing
106− else do
107− h <- twoDigits
108− mm <- peekChar
109− m <- case mm of
110− Just ':' -> anyChar *> twoDigits
111− Just d | isDigit d -> twoDigits
112− _ -> return 0
113− let off | ch == '-' = negate off0
114− | otherwise = off0
115− off0 = h * 60 + m
116− case undefined of
117− _ | off == 0 ->
118− return Nothing
119− | off < -720 || off > 840 || m > 59 ->
120− fail "invalid time zone offset"
121− | otherwise ->
122− let !tz = Local.minutesToTimeZone off
123− in return (Just tz)
41+timeZone = T.timeZone
12442
12543-- | Parse a date and time, of the form @YYYY-MM-DD HH:MM[:SS[.SSS]]@.
12644-- The space may be replaced with a @T@. The number of seconds is optional
12745-- and may be followed by a fractional component.
12846localTime :: Parser Local.LocalTime
129−localTime = Local.LocalTime <$> day <* daySep <*> timeOfDay
130− where daySep = satisfy (\c -> c == 'T' || c == ' ')
47+localTime = T.localTime
13148
13249-- | Behaves as 'zonedTime', but converts any time zone offset into a
13350-- UTC time.
13451utcTime :: Parser UTCTime
135−utcTime = do
136− lt@(Local.LocalTime d t) <- localTime
137− mtz <- timeZone
138− case mtz of
139− Nothing -> let !tt = Local.timeOfDayToTime t
140− in return (UTCTime d tt)
141− Just tz -> return $! Local.localTimeToUTC tz lt
52+utcTime = T.utcTime
14253
14354-- | Parse a date with time zone info. Acceptable formats:
14455--
@@ -152,7 +63,4 @@ utcTime = do
15263-- two digits are hours, the @:@ is optional and the second two digits
15364-- (also optional) are minutes.
15465zonedTime :: Parser Local.ZonedTime
155−zonedTime = Local.ZonedTime <$> localTime <*> (fromMaybe utc <$> timeZone)
156−
157−utc :: Local.TimeZone
158−utc = Local.TimeZone 0 False ""
66+zonedTime = T.zonedTime
aeson.cabal+3−1
@@ -80,7 +80,7 @@ flag cffi
8080
8181library
8282 default-language: Haskell2010
83− hs-source-dirs: .
83+ hs-source-dirs: . iso8601-attoparsec/
8484
8585 exposed-modules:
8686 Data.Aeson
@@ -109,6 +109,8 @@ library
109109 Data.Aeson.Types.ToJSON
110110 Data.Aeson.Types.Class
111111 Data.Aeson.Types.Internal
112+ Data.Attoparsec.Time
113+ Data.Attoparsec.Time.Internal
112114
113115 build-depends:
114116 attoparsec >= 0.13.0.1,
iso8601-attoparsec/Data/Attoparsec/Time.hsnew file+149
@@ -0,0 +1,149 @@
1+{-# LANGUAGE BangPatterns #-}
2+{-# LANGUAGE ScopedTypeVariables #-}
3+
4+-- |
5+-- Module: Data.Aeson.Parser.Time
6+-- Copyright: (c) 2015-2016 Bryan O'Sullivan
7+-- License: BSD3
8+-- Maintainer: Bryan O'Sullivan <bos@serpentine.com>
9+-- Stability: experimental
10+-- Portability: portable
11+--
12+-- Parsers for parsing dates and times.
13+
14+module Data.Attoparsec.Time
15+ (
16+ day
17+ , localTime
18+ , timeOfDay
19+ , timeZone
20+ , utcTime
21+ , zonedTime
22+ ) where
23+
24+import Prelude ()
25+import Prelude.Compat
26+
27+import Control.Applicative ((<|>))
28+import Control.Monad (void, when)
29+import Data.Attoparsec.Text as A
30+import Data.Attoparsec.Time.Internal (toPico)
31+import Data.Bits ((.&.))
32+import Data.Char (isDigit, ord)
33+import Data.Fixed (Pico)
34+import Data.Int (Int64)
35+import Data.Maybe (fromMaybe)
36+import Data.Time.Calendar (Day, fromGregorianValid)
37+import Data.Time.Clock (UTCTime(..))
38+import qualified Data.Text as T
39+import qualified Data.Time.LocalTime as Local
40+
41+-- | Parse a date of the form @[+,-]YYYY-MM-DD@.
42+day :: Parser Day
43+day = do
44+ absOrNeg <- negate <$ char '-' <|> id <$ char '+' <|> pure id
45+ y <- decimal <* char '-'
46+ m <- twoDigits <* char '-'
47+ d <- twoDigits
48+ maybe (fail "invalid date") return (fromGregorianValid (absOrNeg y) m d)
49+
50+-- | Parse a two-digit integer (e.g. day of month, hour).
51+twoDigits :: Parser Int
52+twoDigits = do
53+ a <- digit
54+ b <- digit
55+ let c2d c = ord c .&. 15
56+ return $! c2d a * 10 + c2d b
57+
58+-- | Parse a time of the form @HH:MM[:SS[.SSS]]@.
59+timeOfDay :: Parser Local.TimeOfDay
60+timeOfDay = do
61+ h <- twoDigits
62+ m <- char ':' *> twoDigits
63+ s <- option 0 (char ':' *> seconds)
64+ if h < 24 && m < 60 && s < 61
65+ then return (Local.TimeOfDay h m s)
66+ else fail "invalid time"
67+
68+data T = T {-# UNPACK #-} !Int {-# UNPACK #-} !Int64
69+
70+-- | Parse a count of seconds, with the integer part being two digits
71+-- long.
72+seconds :: Parser Pico
73+seconds = do
74+ real <- twoDigits
75+ mc <- peekChar
76+ case mc of
77+ Just '.' -> do
78+ t <- anyChar *> takeWhile1 isDigit
79+ return $! parsePicos real t
80+ _ -> return $! fromIntegral real
81+ where
82+ parsePicos a0 t = toPico (fromIntegral (t' * 10^n))
83+ where T n t' = T.foldl' step (T 12 (fromIntegral a0)) t
84+ step ma@(T m a) c
85+ | m <= 0 = ma
86+ | otherwise = T (m-1) (10 * a + fromIntegral (ord c) .&. 15)
87+
88+-- | Parse a time zone, and return 'Nothing' if the offset from UTC is
89+-- zero. (This makes some speedups possible.)
90+timeZone :: Parser (Maybe Local.TimeZone)
91+timeZone = do
92+ let maybeSkip c = do ch <- peekChar'; when (ch == c) (void anyChar)
93+ maybeSkip ' '
94+ ch <- satisfy $ \c -> c == 'Z' || c == '+' || c == '-'
95+ if ch == 'Z'
96+ then return Nothing
97+ else do
98+ h <- twoDigits
99+ mm <- peekChar
100+ m <- case mm of
101+ Just ':' -> anyChar *> twoDigits
102+ Just d | isDigit d -> twoDigits
103+ _ -> return 0
104+ let off | ch == '-' = negate off0
105+ | otherwise = off0
106+ off0 = h * 60 + m
107+ case undefined of
108+ _ | off == 0 ->
109+ return Nothing
110+ | off < -720 || off > 840 || m > 59 ->
111+ fail "invalid time zone offset"
112+ | otherwise ->
113+ let !tz = Local.minutesToTimeZone off
114+ in return (Just tz)
115+
116+-- | Parse a date and time, of the form @YYYY-MM-DD HH:MM[:SS[.SSS]]@.
117+-- The space may be replaced with a @T@. The number of seconds is optional
118+-- and may be followed by a fractional component.
119+localTime :: Parser Local.LocalTime
120+localTime = Local.LocalTime <$> day <* daySep <*> timeOfDay
121+ where daySep = satisfy (\c -> c == 'T' || c == ' ')
122+
123+-- | Behaves as 'zonedTime', but converts any time zone offset into a
124+-- UTC time.
125+utcTime :: Parser UTCTime
126+utcTime = do
127+ lt@(Local.LocalTime d t) <- localTime
128+ mtz <- timeZone
129+ case mtz of
130+ Nothing -> let !tt = Local.timeOfDayToTime t
131+ in return (UTCTime d tt)
132+ Just tz -> return $! Local.localTimeToUTC tz lt
133+
134+-- | Parse a date with time zone info. Acceptable formats:
135+--
136+-- @YYYY-MM-DD HH:MM Z@
137+-- @YYYY-MM-DD HH:MM:SS Z@
138+-- @YYYY-MM-DD HH:MM:SS.SSS Z@
139+--
140+-- The first space may instead be a @T@, and the second space is
141+-- optional. The @Z@ represents UTC. The @Z@ may be replaced with a
142+-- time zone offset of the form @+0000@ or @-08:00@, where the first
143+-- two digits are hours, the @:@ is optional and the second two digits
144+-- (also optional) are minutes.
145+zonedTime :: Parser Local.ZonedTime
146+zonedTime = Local.ZonedTime <$> localTime <*> (fromMaybe utc <$> timeZone)
147+
148+utc :: Local.TimeZone
149+utc = Local.TimeZone 0 False ""
iso8601-attoparsec/Data/Attoparsec/Time/Internal.hsnew file+61
@@ -0,0 +1,61 @@
1+{-# LANGUAGE CPP #-}
2+
3+-- |
4+-- Module: Data.Aeson.Internal.Time
5+-- Copyright: (c) 2015-2016 Bryan O'Sullivan
6+-- License: BSD3
7+-- Maintainer: Bryan O'Sullivan <bos@serpentine.com>
8+-- Stability: experimental
9+-- Portability: portable
10+
11+module Data.Attoparsec.Time.Internal
12+ (
13+ TimeOfDay64(..)
14+ , fromPico
15+ , toPico
16+ , diffTimeOfDay64
17+ , toTimeOfDay64
18+ ) where
19+
20+import Prelude ()
21+import Prelude.Compat
22+
23+import Data.Int (Int64)
24+import Data.Time
25+import Unsafe.Coerce (unsafeCoerce)
26+
27+#if MIN_VERSION_base(4,7,0)
28+
29+import Data.Fixed (Pico, Fixed(MkFixed))
30+
31+toPico :: Integer -> Pico
32+toPico = MkFixed
33+
34+fromPico :: Pico -> Integer
35+fromPico (MkFixed i) = i
36+
37+#else
38+
39+import Data.Fixed (Pico)
40+
41+toPico :: Integer -> Pico
42+toPico = unsafeCoerce
43+
44+fromPico :: Pico -> Integer
45+fromPico = unsafeCoerce
46+
47+#endif
48+
49+-- | Like TimeOfDay, but using a fixed-width integer for seconds.
50+data TimeOfDay64 = TOD {-# UNPACK #-} !Int
51+ {-# UNPACK #-} !Int
52+ {-# UNPACK #-} !Int64
53+
54+diffTimeOfDay64 :: DiffTime -> TimeOfDay64
55+diffTimeOfDay64 t = TOD (fromIntegral h) (fromIntegral m) s
56+ where (h,mp) = fromIntegral pico `quotRem` 3600000000000000
57+ (m,s) = mp `quotRem` 60000000000000
58+ pico = unsafeCoerce t :: Integer
59+
60+toTimeOfDay64 :: TimeOfDay -> TimeOfDay64
61+toTimeOfDay64 (TimeOfDay h m s) = TOD h m (fromIntegral (fromPico s))
iso8601-attoparsec/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2011, MailRank, Inc.
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions
7+are met:
8+
9+1. Redistributions of source code must retain the above copyright
10+ notice, this list of conditions and the following disclaimer.
11+
12+2. Redistributions in binary form must reproduce the above copyright
13+ notice, this list of conditions and the following disclaimer in the
14+ documentation and/or other materials provided with the distribution.
15+
16+3. Neither the name of the author nor the names of his contributors
17+ may be used to endorse or promote products derived from this software
18+ without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS ``AS IS'' AND ANY EXPRESS
21+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR
24+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
26+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
27+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
28+STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
29+ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30+POSSIBILITY OF SUCH DAMAGE.
iso8601-attoparsec/README.mdnew file+19
@@ -0,0 +1,19 @@
1+Parsing of ISO 8601 dates.
2+
3+This package is used to parse dates in aeson. It is split into a
4+separate package to be shared by other projects that want to parse
5+dates like aeson does.
6+
7+For now, this project is located in the aeson repository and aeson
8+itself uses the source of this package without pulling in the package
9+as a dependency.
10+
11+## Stability
12+
13+Since aeson depends on this package we want to be very careful about
14+changing the format.
15+
16+There may be breaking changes if we find that the format is
17+incorrectly too lenient. We consider widening of the allowed input a
18+non-breaking addition since all previously valid input will still
19+parse correctly.
iso8601-attoparsec/iso8601-attoparsec.cabalnew file+51
@@ -0,0 +1,51 @@
1+name: iso8601-attoparsec
2+version: 1.0.0.0
3+synopsis: Parsing of ISO 8601 dates, originally from aeson.
4+description: Parsing of ISO 8601 dates, originally from aeson.
5+homepage: https://github.com/bergmark/iso8601-attoparsec#readme
6+license: BSD3
7+license-file: LICENSE
8+author: Adam Bergmark
9+maintainer: adam@bergmark.nl
10+copyright: 2016
11+category: Web
12+build-type: Simple
13+extra-source-files: README.md
14+cabal-version: >=1.10
15+
16+flag developer
17+ description: operate in developer mode
18+ default: False
19+ manual: True
20+
21+flag fast
22+ description: compile without optimizations
23+ default: False
24+ manual: True
25+
26+library
27+ hs-source-dirs: .
28+ default-language: Haskell2010
29+ ghc-options: -Wall
30+ exposed-modules:
31+ Data.Attoparsec.Time.Internal
32+ Data.Attoparsec.Time
33+ build-depends:
34+ attoparsec >= 0.13.0.1,
35+ base >= 4.5 && < 5,
36+ base-compat >= 0.9.1 && < 0.10,
37+ text >= 1.1.1.0,
38+ time >= 1.1.1.4
39+
40+ if flag(fast)
41+ ghc-options: -O0
42+ else
43+ ghc-options: -O2
44+
45+ if flag(developer)
46+ ghc-options: -Werror
47+ ghc-prof-options: -auto-all
48+
49+source-repository head
50+ type: git
51+ location: git://github.com/bos/aeson.git
stack-lts6.yaml+3
@@ -1,11 +1,14 @@
11resolver: lts-6.30
22packages:
33- '.'
4+- iso8601-attoparsec
45extra-deps:
56- semigroups-0.18.2
67- integer-logarithms-1
78flags:
89 aeson:
910 fast: true
11+ iso8601-attoparsec:
12+ fast: true
1013 semigroups:
1114 bytestring-builder: false
stack-lts7.yaml+3
@@ -1,8 +1,11 @@
11resolver: lts-7.19
22packages:
33- '.'
4+- iso8601-attoparsec
45extra-deps:
56- integer-logarithms-1
67flags:
78 aeson:
89 fast: true
10+ iso8601-attoparsec:
11+ fast: true
stack-lts8.yaml+3
@@ -1,6 +1,9 @@
11resolver: lts-8.1
22packages:
33- '.'
4+- iso8601-attoparsec
45flags:
56 aeson:
67 fast: true
8+ iso8601-attoparsec:
9+ fast: true
stack-nightly.yaml+4−3
@@ -1,8 +1,9 @@
1−resolver: nightly-2017-02-29
1+resolver: nightly-2017-03-25
22packages:
33- '.'
4−extra-deps:
5−- integer-logarithms-1
4+- iso8601-attoparsec
65flags:
76 aeson:
87 fast: true
8+ iso8601-attoparsec:
9+ fast: true