@@ -14,6 +14,7 @@ import Data.Aeson.Types
1414import Data.Aeson.TH
1515import Data.Attoparsec.Number
1616import Data.Data (Typeable, Data)
17+import Data.Function (on)
1718import Data.Text (Text)
1819import Test.Framework (Test, defaultMain, testGroup)
1920import Test.Framework.Providers.QuickCheck2 (testProperty)
@@ -25,8 +26,10 @@ import qualified Data.ByteString.Lazy.Char8 as L
2526import qualified Data.Text as T
2627import qualified Data.Map as Map
2728import qualified Data.HashMap.Strict as H
28−import Data.Time (ZonedTime(..), LocalTime(..), TimeZone(..), hoursToTimeZone, Day(..), TimeOfDay(..))
29−
29+import Data.Time.Calendar (Day)
30+import Data.Time.Clock (DiffTime, UTCTime(..), picosecondsToDiffTime)
31+import Data.Time (ZonedTime(..), LocalTime(..), TimeZone(..),
32+ hoursToTimeZone, Day(..), TimeOfDay(..))
3033import Options
3134
3235#ifdef GENERICS
@@ -57,13 +60,24 @@ genericTo _ v = G.toJSON v == toJSON v
5760genericFrom :: (Eq a, Data a, ToJSON a) => a -> a -> Bool
5861genericFrom _ v = G.fromJSON (toJSON v) == Success v
5962
60−approxEq :: Double -> Double -> Bool
61−approxEq a b = a == b ||
62− d < maxAbsoluteError ||
63− d / max (abs b) (abs a) <= maxRelativeError
64− where d = abs (a - b)
65− maxAbsoluteError = 1e-15
66− maxRelativeError = 1e-15
63+approxEq :: (Fractional a, Ord a) => a -> a -> Bool
64+approxEq = approxEqWith 1e-15 1e-15
65+
66+approxEqWith :: (Fractional a, Ord a) => a -> a -> a -> a -> Bool
67+approxEqWith maxAbsoluteError maxRelativeError a b =
68+ a == b || d < maxAbsoluteError ||
69+ d / max (abs b) (abs a) <= maxRelativeError
70+ where d = abs (a - b)
71+
72+-- Compare equality to within a millisecond, allowing for rounding
73+-- error (ECMA 262 requires milliseconds to rounded to zero, not
74+-- rounded to nearest).
75+approxEqUTC :: UTCTime -> UTCTime -> Bool
76+approxEqUTC a b = ((==) `on` utctDay) a b &&
77+ (approxEqWith 1 1 `on` ((* 1e3) . utctDayTime)) a b
78+
79+approxEqNet :: DotNetTime -> DotNetTime -> Bool
80+approxEqNet (DotNetTime a) (DotNetTime b) = approxEqUTC a b
6781
6882toFromJSON :: (Arbitrary a, Eq a, FromJSON a, ToJSON a) => a -> Bool
6983toFromJSON x = case fromJSON . toJSON $ x of
@@ -297,6 +311,18 @@ instance Arbitrary TimeZone where
297311 offset <- choose (0,2) :: Gen Int
298312 return $ hoursToTimeZone offset
299313
314+instance Arbitrary Day where
315+ arbitrary = ModifiedJulianDay `liftM` arbitrary
316+
317+instance Arbitrary DiffTime where
318+ arbitrary = picosecondsToDiffTime `liftM` choose (0, 86400000000000000)
319+
320+instance Arbitrary UTCTime where
321+ arbitrary = liftM2 UTCTime arbitrary arbitrary
322+
323+instance Arbitrary DotNetTime where
324+ arbitrary = DotNetTime `liftM` arbitrary
325+
300326instance Arbitrary ZonedTime where
301327 arbitrary = liftM2 ZonedTime arbitrary arbitrary
302328
@@ -345,6 +371,8 @@ tests = [
345371 , testProperty "String" $ roundTripEq (""::String)
346372 , testProperty "Text" $ roundTripEq T.empty
347373 , testProperty "Foo" $ roundTripEq (undefined::Foo)
374+ , testProperty "DotNetTime" $ roundTrip approxEqNet undefined
375+ , testProperty "UTCTime" $ roundTrip approxEqUTC undefined
348376 , testProperty "ZonedTime" $ roundTripEq (undefined::ZonedTime)
349377 ],
350378 testGroup "toFromJSON" [