Skip to content

Commit 3e6cee2

Browse files
authored
Merge pull request #1032 from haskell/text-iso8601
Text iso8601
2 parents 212c324 + f0be72f commit 3e6cee2

File tree

16 files changed

+1004
-111
lines changed

16 files changed

+1004
-111
lines changed

.github/workflows/haskell-ci.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ jobs:
213213
touch cabal.project
214214
echo "packages: $GITHUB_WORKSPACE/source/." >> cabal.project
215215
echo "packages: $GITHUB_WORKSPACE/source/attoparsec-iso8601" >> cabal.project
216+
echo "packages: $GITHUB_WORKSPACE/source/text-iso8601" >> cabal.project
216217
echo "packages: $GITHUB_WORKSPACE/source/examples" >> cabal.project
217218
echo "packages: $GITHUB_WORKSPACE/source/benchmarks" >> cabal.project
218219
cat cabal.project
@@ -230,6 +231,8 @@ jobs:
230231
echo "PKGDIR_aeson=${PKGDIR_aeson}" >> "$GITHUB_ENV"
231232
PKGDIR_attoparsec_iso8601="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/attoparsec-iso8601-[0-9.]*')"
232233
echo "PKGDIR_attoparsec_iso8601=${PKGDIR_attoparsec_iso8601}" >> "$GITHUB_ENV"
234+
PKGDIR_text_iso8601="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/text-iso8601-[0-9.]*')"
235+
echo "PKGDIR_text_iso8601=${PKGDIR_text_iso8601}" >> "$GITHUB_ENV"
233236
PKGDIR_aeson_examples="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/aeson-examples-[0-9.]*')"
234237
echo "PKGDIR_aeson_examples=${PKGDIR_aeson_examples}" >> "$GITHUB_ENV"
235238
PKGDIR_aeson_benchmarks="$(find "$GITHUB_WORKSPACE/unpacked" -maxdepth 1 -type d -regex '.*/aeson-benchmarks-[0-9.]*')"
@@ -239,20 +242,23 @@ jobs:
239242
touch cabal.project.local
240243
echo "packages: ${PKGDIR_aeson}" >> cabal.project
241244
echo "packages: ${PKGDIR_attoparsec_iso8601}" >> cabal.project
245+
echo "packages: ${PKGDIR_text_iso8601}" >> cabal.project
242246
echo "packages: ${PKGDIR_aeson_examples}" >> cabal.project
243247
echo "packages: ${PKGDIR_aeson_benchmarks}" >> cabal.project
244248
echo "package aeson" >> cabal.project
245249
echo " ghc-options: -Werror=missing-methods" >> cabal.project
246250
echo "package attoparsec-iso8601" >> cabal.project
247251
echo " ghc-options: -Werror=missing-methods" >> cabal.project
252+
echo "package text-iso8601" >> cabal.project
253+
echo " ghc-options: -Werror=missing-methods" >> cabal.project
248254
echo "package aeson-examples" >> cabal.project
249255
echo " ghc-options: -Werror=missing-methods" >> cabal.project
250256
echo "package aeson-benchmarks" >> cabal.project
251257
echo " ghc-options: -Werror=missing-methods" >> cabal.project
252258
cat >> cabal.project <<EOF
253259
allow-newer: hermes-json:attoparsec-iso8601
254260
EOF
255-
$HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(aeson|aeson-benchmarks|aeson-examples|attoparsec-iso8601)$/; }' >> cabal.project.local
261+
$HCPKG list --simple-output --names-only | perl -ne 'for (split /\s+/) { print "constraints: $_ installed\n" unless /^(aeson|aeson-benchmarks|aeson-examples|attoparsec-iso8601|text-iso8601)$/; }' >> cabal.project.local
256262
cat cabal.project
257263
cat cabal.project.local
258264
- name: dump install plan
@@ -282,6 +288,7 @@ jobs:
282288
run: |
283289
if [ $((HCNUMVER >= 90200 && HCNUMVER < 90400)) -ne 0 ] ; then (cd ${PKGDIR_aeson} && hlint -h ${GITHUB_WORKSPACE}/source/.hlint.yaml -XHaskell2010 src attoparsec-iso8601/src src-pure) ; fi
284290
if [ $((HCNUMVER >= 90200 && HCNUMVER < 90400)) -ne 0 ] ; then (cd ${PKGDIR_attoparsec_iso8601} && hlint -h ${GITHUB_WORKSPACE}/source/.hlint.yaml -XHaskell2010 src) ; fi
291+
if [ $((HCNUMVER >= 90200 && HCNUMVER < 90400)) -ne 0 ] ; then (cd ${PKGDIR_text_iso8601} && hlint -h ${GITHUB_WORKSPACE}/source/.hlint.yaml -XHaskell2010 src) ; fi
285292
if [ $((HCNUMVER >= 90200 && HCNUMVER < 90400)) -ne 0 ] ; then (cd ${PKGDIR_aeson_examples} && hlint -h ${GITHUB_WORKSPACE}/source/.hlint.yaml -XHaskell2010 src/) ; fi
286293
if [ $((HCNUMVER >= 90200 && HCNUMVER < 90400)) -ne 0 ] ; then (cd ${PKGDIR_aeson_benchmarks} && hlint -h ${GITHUB_WORKSPACE}/source/.hlint.yaml -XHaskell2010 .) ; fi
287294
if [ $((HCNUMVER >= 90200 && HCNUMVER < 90400)) -ne 0 ] ; then (cd ${PKGDIR_aeson_benchmarks} && hlint -h ${GITHUB_WORKSPACE}/source/.hlint.yaml -XHaskell2010 bench examples/src) ; fi

.hlint.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,14 @@
1313
name: "Use <=<"
1414
within:
1515
- Data.Aeson.Types.FromJSON
16+
- ignore:
17+
name: "Avoid lambda"
18+
within:
19+
- Data.Time.FromText
20+
- ignore:
21+
name: "Use isDigit"
22+
within:
23+
- Data.Time.FromText
1624

1725
# CPP confuses
1826
- ignore:

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
lint:
2-
./run-hlint.sh --cpp-include include/ src/ attoparsec-iso8601/ benchmarks/ examples/ src-pure/ tests/
2+
./run-hlint.sh --cpp-include include/ src/ attoparsec-iso8601/ benchmarks/ examples/ src-pure/ tests/ text-iso8601/src text-iso8601/tests

aeson.cabal

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ tested-with:
2222
|| ==9.6.2
2323

2424
synopsis: Fast JSON parsing and encoding
25-
cabal-version: >=1.10
25+
cabal-version: 1.12
2626
homepage: https://github.com/haskell/aeson
2727
bug-reports: https://github.com/haskell/aeson/issues
2828
build-type: Simple
@@ -126,6 +126,7 @@ library
126126
, semialign >=1.3 && <1.4
127127
, strict >=0.5 && <0.6
128128
, tagged >=0.8.7 && <0.9
129+
, text-iso8601 >=0.1 && <0.2
129130
, text-short >=0.1.5 && <0.2
130131
, th-abstraction >=0.5.0.0 && <0.6
131132
, these >=1.2 && <1.3

attoparsec-iso8601/attoparsec-iso8601.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ copyright:
1212
author: Bryan O'Sullivan <[email protected]>
1313
maintainer: Adam Bergmark <[email protected]>
1414
stability: experimental
15-
cabal-version: >=1.10
15+
cabal-version: 1.12
1616
homepage: https://github.com/haskell/aeson
1717
bug-reports: https://github.com/haskell/aeson/issues
1818
build-type: Simple

attoparsec-iso8601/src/Data/Attoparsec/Time.hs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ seconds = do
133133

134134
-- | Parse a time zone, and return 'Nothing' if the offset from UTC is
135135
-- zero. (This makes some speedups possible.)
136+
--
137+
-- The accepted formats are @Z@, @+HH@, @+HHMM@, or @+HH:MM@.
138+
--
136139
timeZone :: Parser (Maybe Local.TimeZone)
137140
timeZone = do
138141
ch <- satisfy $ \c -> c == 'Z' || c == '+' || c == '-'
@@ -175,6 +178,7 @@ utcTime = do
175178
in return (UTCTime d tt)
176179
Just tz -> return $! Local.localTimeToUTC tz lt
177180

181+
178182
-- | Parse a date with time zone info. Acceptable formats:
179183
--
180184
-- @YYYY-MM-DD HH:MM Z@

cabal.project

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
with-compiler: ghc
22
packages: .
33
packages: attoparsec-iso8601
4+
packages: text-iso8601
45
packages: examples
56
packages: benchmarks
67
tests: true

src/Data/Aeson/Parser/Time.hs

Lines changed: 20 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,25 @@
1-
module Data.Aeson.Parser.Time
2-
(
3-
run
4-
, day
5-
, month
6-
, quarter
7-
, localTime
8-
, timeOfDay
9-
, timeZone
10-
, utcTime
11-
, zonedTime
12-
) where
1+
module Data.Aeson.Parser.Time (
2+
run,
3+
FT.parseDay,
4+
FT.parseMonth,
5+
FT.parseQuarter,
6+
FT.parseQuarterOfYear,
7+
FT.parseLocalTime,
8+
FT.parseTimeOfDay,
9+
FT.parseUTCTime,
10+
FT.parseZonedTime,
11+
) where
1312

14-
import Data.Attoparsec.Text (Parser)
1513
import Data.Text (Text)
16-
import Data.Time.Calendar (Day)
17-
import Data.Time.Calendar.Quarter.Compat (Quarter)
18-
import Data.Time.Calendar.Month.Compat (Month)
19-
import Data.Time.Clock (UTCTime(..))
20-
import qualified Data.Aeson.Types.Internal as Aeson
21-
import qualified Data.Attoparsec.Text as A
22-
import qualified Data.Attoparsec.Time as T
23-
import qualified Data.Time.LocalTime as Local
24-
25-
-- | Run an attoparsec parser as an aeson parser.
26-
run :: Parser a -> Text -> Aeson.Parser a
27-
run p t = case A.parseOnly (p <* A.endOfInput) t of
28-
Left err -> fail $ "could not parse date: " ++ err
29-
Right r -> return r
30-
31-
-- | Parse a date of the form @[+,-]YYYY-MM-DD@.
32-
day :: Parser Day
33-
day = T.day
34-
{-# INLINE day #-}
35-
36-
-- | Parse a date of the form @[+,-]YYYY-MM@.
37-
month :: Parser Month
38-
month = T.month
39-
{-# INLINE month #-}
40-
41-
-- | Parse a date of the form @[+,-]YYYY-QN@.
42-
quarter :: Parser Quarter
43-
quarter = T.quarter
44-
{-# INLINE quarter #-}
4514

46-
-- | Parse a time of the form @HH:MM[:SS[.SSS]]@.
47-
timeOfDay :: Parser Local.TimeOfDay
48-
timeOfDay = T.timeOfDay
49-
{-# INLINE timeOfDay #-}
50-
51-
-- | Parse a quarter of the form @[+,-]YYYY-QN@.
52-
53-
-- | Parse a time zone, and return 'Nothing' if the offset from UTC is
54-
-- zero. (This makes some speedups possible.)
55-
timeZone :: Parser (Maybe Local.TimeZone)
56-
timeZone = T.timeZone
57-
{-# INLINE timeZone #-}
58-
59-
-- | Parse a date and time, of the form @YYYY-MM-DD HH:MM[:SS[.SSS]]@.
60-
-- The space may be replaced with a @T@. The number of seconds is optional
61-
-- and may be followed by a fractional component.
62-
localTime :: Parser Local.LocalTime
63-
localTime = T.localTime
64-
{-# INLINE localTime #-}
15+
import qualified Data.Aeson.Types.Internal as Aeson
16+
import qualified Data.Time.FromText as FT
6517

66-
-- | Behaves as 'zonedTime', but converts any time zone offset into a
67-
-- UTC time.
68-
utcTime :: Parser UTCTime
69-
utcTime = T.utcTime
70-
{-# INLINE utcTime #-}
18+
type Parser a = Text -> Either String a
7119

72-
-- | Parse a date with time zone info. Acceptable formats:
73-
--
74-
-- @YYYY-MM-DD HH:MM Z@
75-
-- @YYYY-MM-DD HH:MM:SS Z@
76-
-- @YYYY-MM-DD HH:MM:SS.SSS Z@
77-
--
78-
-- The first space may instead be a @T@, and the second space is
79-
-- optional. The @Z@ represents UTC. The @Z@ may be replaced with a
80-
-- time zone offset of the form @+0000@ or @-08:00@, where the first
81-
-- two digits are hours, the @:@ is optional and the second two digits
82-
-- (also optional) are minutes.
83-
zonedTime :: Parser Local.ZonedTime
84-
zonedTime = T.zonedTime
85-
{-# INLINE zonedTime #-}
20+
-- | Run a @text-iso8601@ parser as an aeson parser.
21+
run :: Parser a -> Text -> Aeson.Parser a
22+
run f t = case f t of
23+
Left err -> fail $ "could not parse date: " ++ err
24+
Right r -> return r
25+
{-# INLINE run #-}

src/Data/Aeson/Types/FromJSON.hs

Lines changed: 19 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2071,49 +2071,49 @@ instance (PM.Prim a,FromJSON a) => FromJSON (PM.PrimArray a) where
20712071
-------------------------------------------------------------------------------
20722072

20732073
instance FromJSON Day where
2074-
parseJSON = withText "Day" (Time.run Time.day)
2074+
parseJSON = withText "Day" (Time.run Time.parseDay)
20752075

20762076
instance FromJSONKey Day where
2077-
fromJSONKey = FromJSONKeyTextParser (Time.run Time.day)
2077+
fromJSONKey = FromJSONKeyTextParser (Time.run Time.parseDay)
20782078

20792079

20802080
instance FromJSON TimeOfDay where
2081-
parseJSON = withText "TimeOfDay" (Time.run Time.timeOfDay)
2081+
parseJSON = withText "TimeOfDay" (Time.run Time.parseTimeOfDay)
20822082

20832083
instance FromJSONKey TimeOfDay where
2084-
fromJSONKey = FromJSONKeyTextParser (Time.run Time.timeOfDay)
2084+
fromJSONKey = FromJSONKeyTextParser (Time.run Time.parseTimeOfDay)
20852085

20862086

20872087
instance FromJSON LocalTime where
2088-
parseJSON = withText "LocalTime" (Time.run Time.localTime)
2088+
parseJSON = withText "LocalTime" (Time.run Time.parseLocalTime)
20892089

20902090
instance FromJSONKey LocalTime where
2091-
fromJSONKey = FromJSONKeyTextParser (Time.run Time.localTime)
2091+
fromJSONKey = FromJSONKeyTextParser (Time.run Time.parseLocalTime)
20922092

20932093

20942094
-- | Supported string formats:
20952095
--
2096-
-- @YYYY-MM-DD HH:MM Z@
2097-
-- @YYYY-MM-DD HH:MM:SS Z@
2098-
-- @YYYY-MM-DD HH:MM:SS.SSS Z@
2096+
-- @YYYY-MM-DD HH:MMZ@
2097+
-- @YYYY-MM-DD HH:MM:SSZ@
2098+
-- @YYYY-MM-DD HH:MM:SS.SSSZ@
20992099
--
21002100
-- The first space may instead be a @T@, and the second space is
21012101
-- optional. The @Z@ represents UTC. The @Z@ may be replaced with a
21022102
-- time zone offset of the form @+0000@ or @-08:00@, where the first
21032103
-- two digits are hours, the @:@ is optional and the second two digits
21042104
-- (also optional) are minutes.
21052105
instance FromJSON ZonedTime where
2106-
parseJSON = withText "ZonedTime" (Time.run Time.zonedTime)
2106+
parseJSON = withText "ZonedTime" (Time.run Time.parseZonedTime)
21072107

21082108
instance FromJSONKey ZonedTime where
2109-
fromJSONKey = FromJSONKeyTextParser (Time.run Time.zonedTime)
2109+
fromJSONKey = FromJSONKeyTextParser (Time.run Time.parseZonedTime)
21102110

21112111

21122112
instance FromJSON UTCTime where
2113-
parseJSON = withText "UTCTime" (Time.run Time.utcTime)
2113+
parseJSON = withText "UTCTime" (Time.run Time.parseUTCTime)
21142114

21152115
instance FromJSONKey UTCTime where
2116-
fromJSONKey = FromJSONKeyTextParser (Time.run Time.utcTime)
2116+
fromJSONKey = FromJSONKeyTextParser (Time.run Time.parseUTCTime)
21172117

21182118

21192119
-- | This instance includes a bounds check to prevent maliciously
@@ -2166,30 +2166,22 @@ instance FromJSONKey DayOfWeek where
21662166
fromJSONKey = FromJSONKeyTextParser parseDayOfWeek
21672167

21682168
instance FromJSON QuarterOfYear where
2169-
parseJSON = withText "DaysOfWeek" parseQuarterOfYear
2170-
2171-
parseQuarterOfYear :: T.Text -> Parser QuarterOfYear
2172-
parseQuarterOfYear t = case T.toLower t of
2173-
"q1" -> return Q1
2174-
"q2" -> return Q2
2175-
"q3" -> return Q3
2176-
"q4" -> return Q4
2177-
_ -> fail "Invalid quarter of year"
2169+
parseJSON = withText "QuarterOfYear" (Time.run Time.parseQuarterOfYear)
21782170

21792171
instance FromJSONKey QuarterOfYear where
2180-
fromJSONKey = FromJSONKeyTextParser parseQuarterOfYear
2172+
fromJSONKey = FromJSONKeyTextParser (Time.run Time.parseQuarterOfYear)
21812173

21822174
instance FromJSON Quarter where
2183-
parseJSON = withText "Quarter" (Time.run Time.quarter)
2175+
parseJSON = withText "Quarter" (Time.run Time.parseQuarter)
21842176

21852177
instance FromJSONKey Quarter where
2186-
fromJSONKey = FromJSONKeyTextParser (Time.run Time.quarter)
2178+
fromJSONKey = FromJSONKeyTextParser (Time.run Time.parseQuarter)
21872179

21882180
instance FromJSON Month where
2189-
parseJSON = withText "Month" (Time.run Time.month)
2181+
parseJSON = withText "Month" (Time.run Time.parseMonth)
21902182

21912183
instance FromJSONKey Month where
2192-
fromJSONKey = FromJSONKeyTextParser (Time.run Time.month)
2184+
fromJSONKey = FromJSONKeyTextParser (Time.run Time.parseMonth)
21932185

21942186
-------------------------------------------------------------------------------
21952187
-- base Monoid/Semigroup

text-iso8601/LICENSE

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Copyright (c) 2023 Oleg Grenrus
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.

0 commit comments

Comments
 (0)