on master help sign in
Merge pull request #270 from lpsmith/utcoffset-fixes
02a104aBryan O'Sullivan
Data/Aeson/Parser/Time.hs+15−12
@@ -98,17 +98,22 @@ timeZone = do
98 if ch == 'Z'
98 if ch == 'Z'
99 then return Nothing
99 then return Nothing
100 else do
100 else do
101− h0 <- twoDigits
101+ h <- twoDigits
102+ mc <- peekChar
103+ m <- case mc of
102− m <- maybeSkip ':' *> twoDigits
104+ Just c | c == ':' -> anyChar *> twoDigits
103− let h | ch == '-' = negate h0
105+ | isDigit c -> twoDigits
104− | otherwise = h0
106+ _ -> return 0
105− case h * 60 + m of
107+ case () of
106− off | off == 0 ->
108+ _ | h == 0 && m == 0 ->
107 return Nothing
109 return Nothing
108− | off < -720 || off > 840 || m > 59 ->
110+ | h > 23 || m > 59 ->
109 fail "invalid time zone offset"
111 fail "invalid time zone offset"
110− | otherwise ->
112+ | otherwise ->
113+ let !absset = h * 60 + m
114+ !offset | ch == '-' = negate absset
115+ | otherwise = absset
111− let !tz = Local.minutesToTimeZone off
116+ !tz = Local.minutesToTimeZone offset
112 in return (Just tz)
117 in return (Just tz)
113
118
114-- | Parse a date and time, of the form @YYYY-MM-DD HH:MM:SS@.
119-- | Parse a date and time, of the form @YYYY-MM-DD HH:MM:SS@.
@@ -135,9 +140,7 @@ utcTime = do
135--
140--
136-- The first space may instead be a @T@, and the second space is
141-- The first space may instead be a @T@, and the second space is
137-- optional. The @Z@ represents UTC. The @Z@ may be replaced with a
142-- optional. The @Z@ represents UTC. The @Z@ may be replaced with a
138−-- time zone offset of the form @+0000@ or @-08:00@, where the first
143+-- time zone offset of the form @±HH@ or @±HH:MM@.
139−-- two digits are hours, the @:@ is optional and the second two digits
140−-- are minutes.
141zonedTime :: Parser Local.ZonedTime
144zonedTime :: Parser Local.ZonedTime
142zonedTime = Local.ZonedTime <$> localTime <*> (fromMaybe utc <$> timeZone)
145zonedTime = Local.ZonedTime <$> localTime <*> (fromMaybe utc <$> timeZone)
143
146
tests/UnitTests.hs+6−2
@@ -102,20 +102,23 @@ utcTimeGood = do
102 assertEqual "utctime" (parseWithRead "%FT%T%QZ" ts2) t2
102 assertEqual "utctime" (parseWithRead "%FT%T%QZ" ts2) t2
103 assertEqual "utctime" (parseWithRead "%F %T%QZ" ts3) t3
103 assertEqual "utctime" (parseWithRead "%F %T%QZ" ts3) t3
104 assertEqual "utctime" (parseWithRead "%F %T%QZ" ts4) t4
104 assertEqual "utctime" (parseWithRead "%F %T%QZ" ts4) t4
105− -- timezones. Both +HHMM and +HH:MM are allowed for timezone offset
105+ -- offsets. ±HH, ±HHMM, and ±HH:MM are allowed for utc offset
106− let ts5 = "2015-01-01T12:30:00.00+0000" :: LT.Text
106+ let ts5 = "2015-01-01T12:30:00.00+00" :: LT.Text
107 let ts6 = "2015-01-01T12:30:00.00+01:15" :: LT.Text
107 let ts6 = "2015-01-01T12:30:00.00+01:15" :: LT.Text
108 let ts7 = "2015-01-01T12:30:00.00-0200" :: LT.Text
108 let ts7 = "2015-01-01T12:30:00.00-0200" :: LT.Text
109 let ts8 = "2015-01-01T22:00:00.00-03:00" :: LT.Text
109 let ts8 = "2015-01-01T22:00:00.00-03:00" :: LT.Text
110+ let ts9 = "2015-01-01T22:00:00.00-04:30" :: LT.Text
110 let (Just (t5 :: UTCTime)) = parseWithAeson ts5
111 let (Just (t5 :: UTCTime)) = parseWithAeson ts5
111 let (Just (t6 :: UTCTime)) = parseWithAeson ts6
112 let (Just (t6 :: UTCTime)) = parseWithAeson ts6
112 let (Just (t7 :: UTCTime)) = parseWithAeson ts7
113 let (Just (t7 :: UTCTime)) = parseWithAeson ts7
113 let (Just (t8 :: UTCTime)) = parseWithAeson ts8
114 let (Just (t8 :: UTCTime)) = parseWithAeson ts8
115+ let (Just (t9 :: UTCTime)) = parseWithAeson ts9
114 assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-01T12:30:00.00Z") t5
116 assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-01T12:30:00.00Z") t5
115 assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-01T11:15:00.00Z") t6
117 assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-01T11:15:00.00Z") t6
116 assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-01T14:30:00Z") t7
118 assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-01T14:30:00Z") t7
117 -- ts8 wraps around to the next day in UTC
119 -- ts8 wraps around to the next day in UTC
118 assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-02T01:00:00Z") t8
120 assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-02T01:00:00Z") t8
121+ assertEqual "utctime" (parseWithRead "%FT%T%QZ" "2015-01-02T02:30:00Z") t9
119 where
122 where
120 parseWithRead :: String -> LT.Text -> UTCTime
123 parseWithRead :: String -> LT.Text -> UTCTime
121 parseWithRead f s =
124 parseWithRead f s =
@@ -136,6 +139,7 @@ utcTimeBad = do
136 verifyFailParse "2015-01-01T12:30:00.00+00Z" -- no Zulu if offset given
139 verifyFailParse "2015-01-01T12:30:00.00+00Z" -- no Zulu if offset given
137 verifyFailParse "2015-01-01T12:30:00.00+00:00Z" -- no Zulu if offset given
140 verifyFailParse "2015-01-01T12:30:00.00+00:00Z" -- no Zulu if offset given
138 verifyFailParse "2015-01-03 12:13:00.Z" -- decimal at the end but no digits
141 verifyFailParse "2015-01-03 12:13:00.Z" -- decimal at the end but no digits
142+ verifyFailParse "2015-01-01 13:30:00-01:60" -- malformed offset
139 where
143 where
140 verifyFailParse (s :: LT.Text) =
144 verifyFailParse (s :: LT.Text) =
141 let (dec :: Maybe UTCTime) = decode . LT.encodeUtf8 $ (LT.concat ["\"", s, "\""]) in
145 let (dec :: Maybe UTCTime) = decode . LT.encodeUtf8 $ (LT.concat ["\"", s, "\""]) in