Skip to content

Commit 8b1fe80

Browse files
authored
Merge pull request #945 from haskell/pr930
PR 930
2 parents 2f6e05a + fbb5093 commit 8b1fe80

File tree

7 files changed

+134
-8
lines changed

7 files changed

+134
-8
lines changed

aeson.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: aeson
2-
version: 2.0.3.0
2+
version: 2.1.0.0
33
license: BSD3
44
license-file: LICENSE
55
category: Text, Web, JSON

attoparsec-iso8601/attoparsec-iso8601.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: attoparsec-iso8601
2-
version: 1.0.2.1
2+
version: 1.1.0.0
33
synopsis: Parsing of ISO 8601 dates, originally from aeson
44
description: Parsing of ISO 8601 dates, originally from aeson.
55
license: BSD3

attoparsec-iso8601/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
For the latest version of this document, please see [https://github.com/bos/aeson/blob/master/attoparsec-iso8601/changelog.md](https://github.com/bos/aeson/blob/master/attoparsec-iso8601/changelog.md).
22

3+
### 1.1.0.0
4+
5+
- Change parsers of types with year (`Day`, `UTCTime`) to require years with at least 4 digits.
6+
37
### 1.0.2.1
48

59
* Code (re)organization.

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

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module Data.Attoparsec.Time
1919
, timeZone
2020
, utcTime
2121
, zonedTime
22+
, year
2223
, month
2324
, quarter
2425
) where
@@ -27,42 +28,53 @@ import Prelude.Compat
2728

2829
import Control.Applicative ((<|>))
2930
import Control.Monad (void, when)
30-
import Data.Attoparsec.Text (Parser, char, decimal, digit, option, anyChar, peekChar, peekChar', takeWhile1, satisfy)
31+
import Data.Attoparsec.Text (Parser, char, digit, option, anyChar, peekChar, peekChar', takeWhile1, satisfy)
3132
import Data.Attoparsec.Time.Internal (toPico)
3233
import Data.Bits ((.&.))
3334
import Data.Char (isDigit, ord)
3435
import Data.Fixed (Pico)
3536
import Data.Int (Int64)
3637
import Data.Maybe (fromMaybe)
3738
import Data.Time.Calendar (Day, fromGregorianValid)
39+
import Data.Time.Calendar.Compat (Year)
3840
import Data.Time.Calendar.Quarter.Compat (Quarter, QuarterOfYear (..), fromYearQuarter)
3941
import Data.Time.Calendar.Month.Compat (Month, fromYearMonthValid)
4042
import Data.Time.Clock (UTCTime(..))
4143
import qualified Data.Text as T
4244
import qualified Data.Time.LocalTime as Local
4345

4446
-- | Parse a date of the form @[+,-]YYYY-MM-DD@.
47+
--
48+
-- The year must contain at least 4 digits, to avoid the Y2K problem:
49+
-- a two-digit year @YY@ may mean @YY@, @19YY@, or @20YY@, and we make it
50+
-- an error to prevent the ambiguity.
51+
-- Years from @0000@ to @0999@ must thus be zero-padded.
52+
-- The year may have more than 4 digits.
4553
day :: Parser Day
4654
day = do
4755
absOrNeg <- negate <$ char '-' <|> id <$ char '+' <|> pure id
48-
y <- (decimal <* char '-') <|> fail "date must be of form [+,-]YYYY-MM-DD"
56+
y <- (year <* char '-') <|> fail "date must be of form [+,-]YYYY-MM-DD"
4957
m <- (twoDigits <* char '-') <|> fail "date must be of form [+,-]YYYY-MM-DD"
5058
d <- twoDigits <|> fail "date must be of form [+,-]YYYY-MM-DD"
5159
maybe (fail "invalid date") return (fromGregorianValid (absOrNeg y) m d)
5260

5361
-- | Parse a month of the form @[+,-]YYYY-MM@.
62+
--
63+
-- See also 'day' for details about the year format.
5464
month :: Parser Month
5565
month = do
5666
absOrNeg <- negate <$ char '-' <|> id <$ char '+' <|> pure id
57-
y <- (decimal <* char '-') <|> fail "month must be of form [+,-]YYYY-MM"
67+
y <- (year <* char '-') <|> fail "month must be of form [+,-]YYYY-MM"
5868
m <- twoDigits <|> fail "month must be of form [+,-]YYYY-MM"
5969
maybe (fail "invalid month") return (fromYearMonthValid (absOrNeg y) m)
6070

6171
-- | Parse a quarter of the form @[+,-]YYYY-QN@.
72+
--
73+
-- See also 'day' for details about the year format.
6274
quarter :: Parser Quarter
6375
quarter = do
6476
absOrNeg <- negate <$ char '-' <|> id <$ char '+' <|> pure id
65-
y <- (decimal <* char '-') <|> fail "month must be of form [+,-]YYYY-MM"
77+
y <- (year <* char '-') <|> fail "month must be of form [+,-]YYYY-MM"
6678
_ <- char 'q' <|> char 'Q'
6779
q <- parseQ
6880
return $! fromYearQuarter (absOrNeg y) q
@@ -72,6 +84,19 @@ quarter = do
7284
<|> Q3 <$ char '3'
7385
<|> Q4 <$ char '4'
7486

87+
-- | Parse a year @YYYY@, with at least 4 digits. Does not include any sign.
88+
--
89+
-- Note: 'Year' is a type synonym for 'Integer'.
90+
--
91+
-- @since 1.1.0.0
92+
year :: Parser Year
93+
year = do
94+
ds <- takeWhile1 isDigit
95+
if T.length ds < 4 then
96+
fail "expected year with at least 4 digits"
97+
else
98+
pure (txtToInteger ds)
99+
75100
-- | Parse a two-digit integer (e.g. day of month, hour).
76101
twoDigits :: Parser Int
77102
twoDigits = do
@@ -172,3 +197,53 @@ zonedTime = Local.ZonedTime <$> localTime <*> (fromMaybe utc <$> timeZone)
172197

173198
utc :: Local.TimeZone
174199
utc = Local.TimeZone 0 False ""
200+
201+
------------------ Copy-pasted and adapted from base ------------------------
202+
203+
txtToInteger :: T.Text -> Integer
204+
txtToInteger bs
205+
| l > 40 = valInteger 10 l [ fromIntegral (ord w - 48) | w <- T.unpack bs ]
206+
| otherwise = txtToIntegerSimple bs
207+
where
208+
l = T.length bs
209+
210+
txtToIntegerSimple :: T.Text -> Integer
211+
txtToIntegerSimple = T.foldl' step 0 where
212+
step a b = a * 10 + fromIntegral (ord b - 48) -- 48 = '0'
213+
214+
-- A sub-quadratic algorithm for Integer. Pairs of adjacent radix b
215+
-- digits are combined into a single radix b^2 digit. This process is
216+
-- repeated until we are left with a single digit. This algorithm
217+
-- performs well only on large inputs, so we use the simple algorithm
218+
-- for smaller inputs.
219+
valInteger :: Integer -> Int -> [Integer] -> Integer
220+
valInteger = go
221+
where
222+
go :: Integer -> Int -> [Integer] -> Integer
223+
go _ _ [] = 0
224+
go _ _ [d] = d
225+
go b l ds
226+
| l > 40 = b' `seq` go b' l' (combine b ds')
227+
| otherwise = valSimple b ds
228+
where
229+
-- ensure that we have an even number of digits
230+
-- before we call combine:
231+
ds' = if even l then ds else 0 : ds
232+
b' = b * b
233+
l' = (l + 1) `quot` 2
234+
235+
combine b (d1 : d2 : ds) = d `seq` (d : combine b ds)
236+
where
237+
d = d1 * b + d2
238+
combine _ [] = []
239+
combine _ [_] = errorWithoutStackTrace "this should not happen"
240+
241+
-- The following algorithm is only linear for types whose Num operations
242+
-- are in constant time.
243+
valSimple :: Integer -> [Integer] -> Integer
244+
valSimple base = go 0
245+
where
246+
go r [] = r
247+
go r (d : ds) = r' `seq` go r' ds
248+
where
249+
r' = r * base + fromIntegral d

changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
For the latest version of this document, please see [https://github.com/haskell/aeson/blob/master/changelog.md](https://github.com/haskell/aeson/blob/master/changelog.md).
22

3+
### 2.1.0.0
4+
5+
- Change time instances of types with year (`Day`, `UTCTime`) to require years with at least 4 digits.
6+
37
### 2.0.3.0
48

59
* `text-2.0` support

src/Data/Aeson.hs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ module Data.Aeson
3434

3535
-- ** Direct encoding
3636
-- $encoding
37+
38+
-- * Remarks on specific encodings
39+
-- ** Time
40+
-- $time
41+
42+
-- * Main encoding and decoding functions
3743
decode
3844
, decode'
3945
, eitherDecode
@@ -528,3 +534,36 @@ eitherDecodeFileStrict' =
528534
-- > > import Data.Sequence as Seq
529535
-- > > encode (Seq.fromList [1,2,3])
530536
-- > "[1,2,3]"
537+
538+
-- $time
539+
--
540+
-- This module contains instances of 'ToJSON' and 'FromJSON' for types from
541+
-- the <https://hackage.haskell.org/package/time time> library.
542+
--
543+
-- Those instances encode time as JSON strings in
544+
-- <https://en.wikipedia.org/wiki/ISO_8601 ISO 8601> formats, with the
545+
-- following general form for 'Data.Time.Clock.UTCTime' and
546+
-- 'Data.Time.LocalTime.ZonedTime', while other time types use subsets of those
547+
-- fields:
548+
--
549+
-- > [+,-]YYYY-MM-DDThh:mm[:ss[.sss]]Z
550+
--
551+
-- where
552+
--
553+
-- - @[+,-]@ is an optional sign, @+@ or @-@.
554+
-- - @YYYY@ is the year, which must have at least 4 digits to prevent Y2K problems.
555+
-- Years from @0000@ to @0999@ must thus be zero-padded.
556+
-- - @MM@ is a two-digit month.
557+
-- - @DD@ is a two-digit day.
558+
-- - @T@ is a literal @\'T\'@ character separating the date and the time of
559+
-- day. It may be a space instead.
560+
-- - @hh@ is a two-digit hour.
561+
-- - @mm@ is a two-digit minute.
562+
-- - @ss@ is a two-digit second.
563+
-- - @sss@ is a decimal fraction of a second; it may have any nonzero number of digits.
564+
-- - @Z@ is a time zone; it may be preceded by an optional space.
565+
--
566+
-- For more information, see <https://en.wikipedia.org/wiki/ISO_8601 ISO 8601>
567+
-- <https://hackage.haskell.org/package/time time>,
568+
-- and <https://hackage.haskell.org/package/attoparsec-iso8601 attoparsec-iso8601>
569+
-- (where the relevant parsers are defined).

tests/SerializationFormatSpec.hs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ import Data.Scientific (Scientific)
3131
import Data.Tagged (Tagged(..))
3232
import Data.Text (Text)
3333
import Data.These (These (..))
34-
import Data.Time (fromGregorian)
35-
import Data.Time.Calendar.Month.Compat (fromYearMonth)
34+
import Data.Time (Day, fromGregorian)
35+
import Data.Time.Calendar.Month.Compat (Month, fromYearMonth)
3636
import Data.Time.Calendar.Quarter.Compat (fromYearQuarter, QuarterOfYear (..))
3737
import Data.Time.Calendar.Compat (CalendarDiffDays (..), DayOfWeek (..))
3838
import Data.Time.LocalTime.Compat (CalendarDiffTime (..))
@@ -159,12 +159,14 @@ jsonExamples =
159159
, example "Maybe String" "\"foo\"" (pure "foo" :: Maybe String)
160160
, example "Maybe [Identity Char]" "\"xy\"" (pure [pure 'x', pure 'y'] :: Maybe [Identity Char])
161161

162+
, example "Day; year >= 10000" "\"10000-01-01\"" (fromGregorian 10000 1 1)
162163
, example "Day; year >= 1000" "\"1999-10-12\"" (fromGregorian 1999 10 12)
163164
, example "Day; year > 0 && < 1000" "\"0500-03-04\"" (fromGregorian 500 3 4)
164165
, example "Day; year == 0" "\"0000-02-20\"" (fromGregorian 0 2 20)
165166
, example "Day; year < 0" "\"-0234-01-01\"" (fromGregorian (-234) 1 1)
166167
, example "Day; year < -1000" "\"-1234-01-01\"" (fromGregorian (-1234) 1 1)
167168

169+
, example "Month; year >= 10000" "\"10000-01\"" (fromYearMonth 10000 1)
168170
, example "Month; year >= 1000" "\"1999-10\"" (fromYearMonth 1999 10)
169171
, example "Month; year > 0 && < 1000" "\"0500-03\"" (fromYearMonth 500 3)
170172
, example "Month; year == 0" "\"0000-02\"" (fromYearMonth 0 2)
@@ -297,6 +299,8 @@ jsonDecodingExamples = [
297299
, MaybeExample "Word8 300" "300" (Nothing :: Maybe Word8)
298300
-- Negative zero year, encoding never produces such:
299301
, MaybeExample "Day -0000-02-03" "\"-0000-02-03\"" (Just (fromGregorian 0 2 3))
302+
, MaybeExample "Day; year too short" "\"10-10-10\"" (Nothing :: Maybe Day)
303+
, MaybeExample "Month; year too short" "\"10-10\"" (Nothing :: Maybe Month)
300304
]
301305

302306
data Example where

0 commit comments

Comments
 (0)