Skip to content


Subversion checkout URL

You can clone with
Download ZIP


Add a TimeWithZone newtype, similar to the DotNetTime newtype, wrapping the default time format generated by Rails. #58

wants to merge 2 commits into from

3 participants


I've needed to handle the time format produced by Rails by default a couple of times now (Github and Trajectory APIs), so I'm submitting it for more general use.


weird... rails really uses 3-letter timezones as in "2011-12-29T08:34:25UTC"?

Just wondering, why didn't they just use the easier to parse ES5 subset of ISO8601 which uses resolved numeric offsets?


While investigating your comment I discovered that they do use numeric offsets ... sometimes.

It starts with TimeWithZone#as_json:

But the real fun happens in TimeWithZone#formatted_offset, which special-cases UTC:

Will re-work this patch today in light of this.


Revamped, with a property test. Thanks for making me look into that, @hvr.

hvr commented

Btw, to me it looks as if your ToJSON instance for your newtyped ZonedTime produces almost(!) the format as specified in the ECMA-262 specification (see also issue #1), so it might be useful to provide proper ToJSON/FromJSON instances for the "naked" ZonedTime types which follow the strict ECMA-262 spec, while providing newtypes for the Rails-format handling the cases when the format is not compatible w/ ECMA-262.


Sounds like a good suggestion. I'll wait on another refresh before pulling?


Sounds good, I'll try to get to this tomorrow at HacBoston.


Submitted #64, which actually takes care of this in a much better way.

@mike-burns mike-burns closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Dec 29, 2011
  1. @mike-burns

    Add a TimeWithZone newtype, similar to the DotNetTime newtype, wrappi…

    mike-burns authored
    …ng the default time format generated by Rails.
Commits on Jan 4, 2012
  1. @mike-burns

    Converts to and from a Rails-formatted ISO-8601 time, handling the

    mike-burns authored
    special-casing of UTC time.
This page is out of date. Refresh to see the latest.
1  Data/Aeson/Types.hs
@@ -22,6 +22,7 @@ module Data.Aeson.Types
, emptyObject
-- * Convenience types and functions
, DotNetTime(..)
+ , TimeWithZone(..)
, typeMismatch
-- * Type conversion
, Parser
38 Data/Aeson/Types/Class.hs
@@ -30,6 +30,7 @@ module Data.Aeson.Types.Class
-- * Types
, DotNetTime(..)
+ , TimeWithZone(..)
-- * Functions
, fromJSON
, (.:)
@@ -39,7 +40,7 @@ module Data.Aeson.Types.Class
, typeMismatch
) where
-import Control.Applicative ((<$>), (<*>), pure)
+import Control.Applicative ((<$>), (<*>), pure, (<|>), empty)
import Data.Aeson.Functions
import Data.Aeson.Types.Internal
import Data.Attoparsec.Char8 (Number(..))
@@ -50,7 +51,7 @@ import Data.Monoid (Dual(..), First(..), Last(..))
import Data.Ratio (Ratio)
import Data.Text (Text, pack, unpack)
import Data.Text.Encoding (encodeUtf8)
-import Data.Time.Clock (UTCTime)
+import Data.Time (UTCTime, ZonedTime(..),TimeZone(..))
import Data.Time.Format (FormatTime, formatTime, parseTime)
import Data.Traversable (traverse)
import Data.Typeable (Typeable)
@@ -611,6 +612,39 @@ instance FromJSON DotNetTime where
parseJSON v = typeMismatch "DotNetTime" v
{-# INLINE parseJSON #-}
+-- | A newtype wrapper for 'ZonedTime' that uses the same ISO-8601 formats that
+-- Rails uses by default for its TimeWithZone type.
+-- This can be either UTC, in which case it follows the @%FT%T%Z@ format, or
+-- a localtime, in which case it follows the @%FT%T%z@ format (note the @%z@ vs
+-- @%Z@).
+-- TODO
+newtype TimeWithZone = TimeWithZone ZonedTime deriving (Eq, Show)
+instance Eq ZonedTime where
+ x == y =
+ zonedTimeToLocalTime x == zonedTimeToLocalTime y &&
+ zonedTimeZone x == zonedTimeZone y
+instance ToJSON TimeWithZone where
+ toJSON (TimeWithZone t) = String $ pack $ formattedTime
+ where
+ formattedTime
+ | 0 == timeZoneMinutes (zonedTimeZone t) =
+ formatTime defaultTimeLocale "%FT%T%QZ" t
+ | otherwise =
+ formatTime defaultTimeLocale "%FT%T%Q%z" t
+instance FromJSON TimeWithZone where
+ parseJSON (String t) =
+ timeFormat "%FT%T%QZ" <|> timeFormat "%FT%T%Q%z" <|>
+ fail "could not parse Rails-style ISO-8601 date"
+ where
+ timeFormat f =
+ case parseTime defaultTimeLocale f (unpack t) of
+ Just d -> pure $ TimeWithZone d
+ Nothing -> empty
+ parseJSON v = typeMismatch "TimeWithZone" v
instance ToJSON UTCTime where
toJSON t = String (pack (take 23 str ++ "Z"))
where str = formatTime defaultTimeLocale "%FT%T%Q" t
18 tests/Properties.hs
@@ -12,12 +12,13 @@ import Data.Data (Typeable, Data)
import Data.Text (Text)
import Test.Framework (Test, defaultMain, testGroup)
import Test.Framework.Providers.QuickCheck2 (testProperty)
-import Test.QuickCheck (Arbitrary(..))
+import Test.QuickCheck (Arbitrary(..), choose, Gen(..))
import qualified Data.Aeson.Generic as G
import qualified Data.Attoparsec.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L
import qualified Data.Text as T
import qualified Data.Map as Map
+import Data.Time (ZonedTime(..), LocalTime(..), TimeZone(..), utc, hoursToTimeZone, Day(..), TimeOfDay(..))
encodeDouble :: Double -> Double -> Bool
encodeDouble num denom
@@ -100,6 +101,20 @@ instance (Ord k, Arbitrary k, Arbitrary v) => Arbitrary (Map.Map k v) where
instance Arbitrary Foo where
arbitrary = liftM4 Foo arbitrary arbitrary arbitrary arbitrary
+instance Arbitrary LocalTime where
+ arbitrary = return $ LocalTime (ModifiedJulianDay 1) (TimeOfDay 1 2 3)
+instance Arbitrary TimeZone where
+ arbitrary = do
+ offset <- choose (0,2) :: Gen Int
+ return $ hoursToTimeZone offset
+instance Arbitrary ZonedTime where
+ arbitrary = liftM2 ZonedTime arbitrary arbitrary
+instance Arbitrary TimeWithZone where
+ arbitrary = liftM TimeWithZone arbitrary
Test for Data.Aeson.Generic handling '_' names
@@ -141,6 +156,7 @@ tests = [
, testProperty "String" $ roundTripEq (""::String)
, testProperty "Text" $ roundTripEq T.empty
, testProperty "Foo" $ roundTripEq (undefined::Foo)
+ , testProperty "TimeWithZone" $ roundTripEq (undefined::TimeWithZone)
testGroup "toFromJSON" [
testProperty "Integer" (toFromJSON :: Integer -> Bool)
Something went wrong with that request. Please try again.