{-# OPTIONS -Wall -fno-warn-orphans #-} {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE TypeFamilies #-} module Main (main) where import Data.RangeSpace import Control.Applicative import Control.Arrow ((&&&)) import Data.List (sort) import Data.Time.Calendar (Day(..)) import Data.Time.Clock import Test.QuickCheck import Test.Framework (Test, defaultMain, testGroup) import Test.Framework.Providers.QuickCheck2 (testProperty) ---------------------------------------------------------------------- -- * Derive some instances instance (ApproxEq t, Ord t, AffineSpace t) => ApproxEq (Range t) where approxEq tol r1 r2 = approxEq tol (toBounds r1) (toBounds r2) deriving instance Arbitrary p => Arbitrary (Point p) deriving instance ApproxEq p => ApproxEq (Point p) deriving instance Arbitrary Day instance Arbitrary UTCTime where arbitrary = do day <- arbitrary diff <- arbitrary return $ UTCTime { utctDay = day, utctDayTime = diff } shrink (UTCTime day diff) = UTCTime <$> shrink day <*> shrink diff instance Arbitrary DiffTime where arbitrary = picosecondsToDiffTime <$> arbitrary instance Arbitrary NominalDiffTime where arbitrary = diffUTCTime <$> arbitrary <*> arbitrary instance (Arbitrary a, Arbitrary b) => Arbitrary (D2V a b) where arbitrary = D2V <$> arbitrary <*> arbitrary shrink (D2V x y) = D2V <$> shrink x <*> shrink y instance (ApproxEq a, ApproxEq b) => ApproxEq (D2V a b) where approxEq tol (D2V x1 y1) (D2V x2 y2) = approxEq tol (x1,y1) (x2,y2) instance ApproxEq UTCTime where approxEq tol t1 t2 = abs (diffUTCTime t1 t2) < realToFrac (tol*2) instance ApproxEq NominalDiffTime where approxEq tol t1 t2 = abs (t2 - t1) <= realToFrac (tol*20) -- Although Ranges created by @fromBounds@ should always have a positive -- distance, there's no reason for that to be true in general, so we allow -- Arbitrary to create Ranges with negative distance. instance (Arbitrary r) => Arbitrary (Range r) where arbitrary = Range <$> arbitrary <*> arbitrary -- Don't want to drag in more dependencies for just this. class ApproxEq a where approxEq :: Double -> a -> a -> Bool instance ApproxEq Double where approxEq tol d1 d2 = m == 0.0 || d/m < tol where m = max (abs d1) (abs d2) d = abs (d1 - d2) instance (ApproxEq a, ApproxEq b) => ApproxEq (a,b) where approxEq tol (l1,r1) (l2,r2) = approxEq tol l1 l2 && approxEq tol r1 r2 infix 4 === (===) :: (ApproxEq a) => a -> a -> Bool (===) = approxEq {-pretty equal-}1.0e-10 type DoubleP = Point (D2V Double Double) ---------------------------------------------------------------------- -- Some orphans instance AdditiveGroup NominalDiffTime where zeroV = 0 (^+^) = (+) negateV = negate -- We want an AffineSpace instance for UTCTime, because -- then we can use 'Range UTCTime' instance AffineSpace UTCTime where type Diff UTCTime = NominalDiffTime (.-.) = diffUTCTime (.+^) = flip addUTCTime -- | Having Scalar NominalDiffTime = Double means that scaling -- and basis decomposition have to go through Rational. -- Maybe this should be revisited. instance VectorSpace NominalDiffTime where -- Scalar is Double so we can form a basis like -- D2V (DataX, NominalDiffTime) type Scalar NominalDiffTime = Double s *^ difftime = (realToFrac s) * difftime instance AffineSpace NominalDiffTime where type Diff NominalDiffTime = NominalDiffTime (.-.) = (-) (.+^) = (+) instance HasBasis NominalDiffTime where type Basis NominalDiffTime = () basisValue () = 1 decompose dtime = [((), realToFrac dtime)] decompose' dtime () = realToFrac dtime ---------------------------------------------------------------------- -- * Range tests -- | When creating '(s0,s1)', s1 >= 0 orderedBoundsRange :: (Ord a, AffineSpace a) => Range a -> Bool orderedBoundsRange rng = rmax >= rmin where (rmin,rmax) = toBounds rng -- | A created range should always have a non-negative distance orderedRangeBounds :: (Ord (Diff t), Ord t, AffineSpace t) => Bounds t -> Bool orderedRangeBounds bounds = (max1 .-. min1) >= zeroV where (min1,max1) = toBounds $ fromBounds bounds roundTripRange :: (Ord a, ApproxEq a, AffineSpace a) => a -> a -> Bool roundTripRange s0 s1 = if s1 >= s0 then (s0,s1) === (toBounds $ fromBoundsC s0 s1 ) else (s1,s0) === (toBounds $ fromBoundsC s0 s1 ) roundTripSpan :: (Ord a, ApproxEq a, ApproxEq (Diff a), AffineSpace a) => a -> Diff a -> Bool roundTripSpan s0 diff = (s0,diff) === (toSpan $ fromSpanC s0 diff) propEnds :: (Ord a, ApproxEq a) => a -> a -> Bool propEnds s0 s1 = if s1 >= s0 then let rng = fromBoundsC s0 s1 in (s0,s1) === (rangeStart rng, rangeEnd rng) else let rng = fromBoundsC s0 s1 in (s1,s0) === (rangeStart rng, rangeEnd rng) ---------------------------------------------------------------------- -- newRange tests newRange2D :: DoubleP -> DoubleP -> Bool newRange2D p1 p2 = newRange p1 p2 === testRange where (p1x,p1y) = (xAxis &&& yAxis) $ unPoint p1 (p2x,p2y) = (xAxis &&& yAxis) $ unPoint p2 toP x y = P (D2V x y) testRange = Range (toP (min p1x p2x) (min p1y p2y)) (toP (max p1x p2x) (max p1y p2y)) ---------------------------------------------------------------------- -- maskRange tests maskOuter1D :: (Eq (Basis (Diff t)), Num (Scalar (Diff t)), Ord (Scalar (Diff t)), Ord t, HasBasis (Diff t), AffineSpace t, ApproxEq t) => t -> t -> t -> t -> Property maskOuter1D v1 v2 v3 v4 = True ==> rng === maskRange maskion rng where maskion = fromBounds (r0,r1) -- normalized maskion rng = fromBounds (s0,s1) [r0,s0,s1,r1] = sort [v1,v2,v3,v4] maskInner1D :: (Eq (Basis (Diff t)), Num (Scalar (Diff t)), Ord (Scalar (Diff t)), Ord t, HasBasis (Diff t), AffineSpace t, ApproxEq t) => t -> t -> t -> t -> Property maskInner1D v1 v2 v3 v4 = True ==> maskion === maskRange maskion rng where maskion = fromBounds (r0,r1) -- normalized maskion rng = fromBounds (s0,s1) [s0,r0,r1,s1] = sort [v1,v2,v3,v4] maskLeft1D :: (Eq (Basis (Diff t)), Num (Scalar (Diff t)), Ord (Scalar (Diff t)), Ord t, HasBasis (Diff t), AffineSpace t, ApproxEq t) => t -> t -> t -> t -> Property maskLeft1D v1 v2 v3 v4 = True ==> fromBounds (r0,s1) === maskRange (fromBounds (r0,r1)) (fromBounds (s0,s1)) where [s0,r0,s1,r1] = sort [v1,v2,v3,v4] maskRight1D :: (Eq (Basis (Diff t)), Num (Scalar (Diff t)), Ord (Scalar (Diff t)), Ord t, HasBasis (Diff t), AffineSpace t, ApproxEq t) => t -> t -> t -> t -> Property maskRight1D v1 v2 v3 v4 = True ==> fromBounds (s0,r1) === maskRange (fromBounds (r0,r1)) (fromBounds (s0,s1)) where [r0,s0,r1,s1] = sort [v1,v2,v3,v4] maskNeg :: (Eq (Basis (Diff t)), Num (Scalar (Diff t)), Ord (Scalar (Diff t)), Ord t, HasBasis (Diff t), AffineSpace t, ApproxEq t) => Range t -> Range t -> Bool maskNeg rng1 rng2 = maskRange rng1 rng2 === maskRange rng1' rng2' where rng1' = fromBounds $ toBounds rng1 rng2' = fromBounds $ toBounds rng2 maskMiss1D :: (Eq (Basis (Diff t)), Num (Scalar (Diff t)), Ord (Scalar (Diff t)), Ord t, HasBasis (Diff t), AffineSpace t, ApproxEq t) => t -> t -> t -> t -> Property maskMiss1D v1 v2 v3 v4 = True ==> fromBounds (r0,r0) === maskRange (fromBounds (r0,r1)) (fromBounds (s0,s1)) -- flip the positions to check misses on both sides && fromBounds (s0,s0) === maskRange (fromBounds (s0,s1)) (fromBounds (r0,r1)) where [r0,r1,s0,s1] = sort [v1,v2,v3,v4] -- 2D tests maskD2V :: (Eq (Basis (Diff t1)), Eq (Basis (Diff t)), Num (Scalar (Diff t)), Ord (Scalar (Diff t)), Ord t, Ord t1, HasBasis (Diff t1), HasBasis (Diff t), AffineSpace t, AffineSpace t1, ApproxEq t1, ApproxEq t, Scalar (Diff t1) ~ Scalar (Diff t)) => Range t1 -> Range t1 -> Range t -> Range t -> Bool maskD2V xr1 xr2 yr1 yr2 = rngMin === (D2V minx miny) && rngMax === D2V maxx maxy where Range rngMin rngMax = maskRange r2d1 r2d2 (minx,maxx) = toBounds $ maskRange (xr1) (xr2) (miny,maxy) = toBounds $ maskRange (yr1) (yr2) r2d1 = range2D xr1 yr1 r2d2 = range2D xr2 yr2 -- union tests unionRangeBounds :: (Num (Scalar (Diff t)), Ord (Scalar (Diff t)), Ord t, HasBasis (Diff t), AffineSpace t, ApproxEq t) => Range t -> Range t -> Bool unionRangeBounds r1 r2 = toBounds (unionRange r1 r2) === (min min1 min2,max max1 max2) where (min1,max1) = toBounds r1 (min2,max2) = toBounds r2 ---------------------------------------------------------------------- -- querying tests inRange2D :: DoubleP -> DoubleP -> DoubleP -> Bool inRange2D p0 p1 px = inRange px (newRange p0 p1) == checkRange where checkRange = (pMinX <= pxX && pxX <= pMaxX) && (pMinY <= pxY && pxY <= pMaxY) (p0X,p0Y) = (xAxis &&& yAxis) $ unPoint p0 (p1X,p1Y) = (xAxis &&& yAxis) $ unPoint p1 (pxX,pxY) = (xAxis &&& yAxis) $ unPoint px pMinX = min p0X p1X pMaxX = max p0X p1X pMinY = min p0Y p1Y pMaxY = max p0Y p1Y inRangeProp :: (Ord a, ApproxEq a) => a -> Range a -> Bool inRangeProp x rng = inOrdRange x rng == checkRange where checkRange = x >= minV && x <= maxV (minV,maxV) = toBounds rng ---------------------------------------------------------------------- -- Test harness main :: IO () main = defaultMain tests -- do a *whole lot* of tests, to make sure that the instances are all correct. tests :: [Test] tests = [ testGroup "bounds" [ testGroup "construction" [ testProperty "Double" (orderedBoundsRange :: Range Double -> Bool) , testProperty "DoubleP" (orderedBoundsRange :: Range DoubleP -> Bool) ] ] , testGroup "endpoints" [ testProperty "Double" (propEnds :: Double -> Double -> Bool) , testProperty "DoubleP" (propEnds :: DoubleP -> DoubleP -> Bool) ] , testGroup "range" [ testGroup "construction" [ testProperty "Double" (orderedRangeBounds :: (Double,Double)->Bool) , testProperty "DoubleP" (orderedRangeBounds :: (DoubleP ,DoubleP)->Bool) ] ] , testGroup "roundTrip" [ testProperty "Double" (roundTripRange :: Double->Double->Bool) , testProperty "DoubleP" (roundTripRange :: DoubleP->DoubleP->Bool) ] , testGroup "span" [ testProperty "Double" (roundTripSpan :: Double -> Double -> Bool) , testProperty "DoubleP" (roundTripSpan :: DoubleP -> D2V Double Double -> Bool) ] , testProperty "newRange" newRange2D , testGroup "union" -- only testing with a few types, if there are problems with instances -- they'll show up in maskRange tests [ testProperty "Double" (unionRangeBounds :: Range Double->Range Double->Bool) , testProperty "NominalDiffTime" (unionRangeBounds :: Range NominalDiffTime->Range NominalDiffTime->Bool) ] , testGroup "maskRange" [ testGroup "maskOuter1D" [ testProperty "Double" (maskOuter1D :: Double->Double->Double->Double->Property) , testProperty "NominalDiffTime" (maskOuter1D :: NominalDiffTime->NominalDiffTime ->NominalDiffTime->NominalDiffTime->Property) ] , testGroup "maskInner1D" [ testProperty "Double" (maskInner1D :: Double->Double->Double->Double->Property) , testProperty "NominalDiffTime" (maskInner1D :: NominalDiffTime->NominalDiffTime ->NominalDiffTime->NominalDiffTime->Property) ] , testGroup "maskLeft1D" [ testProperty "Double" (maskLeft1D :: Double->Double->Double->Double->Property) , testProperty "NominalDiffTime" (maskLeft1D :: NominalDiffTime->NominalDiffTime ->NominalDiffTime->NominalDiffTime->Property) ] , testGroup "maskMiss1D" [ testProperty "Double" (maskMiss1D :: Double->Double->Double->Double->Property) ] , testGroup "RestrictNeg" [ testProperty "Double" (maskRight1D :: Double->Double->Double->Double->Property) , testProperty "NominalDiffTime" (maskRight1D :: NominalDiffTime->NominalDiffTime ->NominalDiffTime->NominalDiffTime->Property) ] , testGroup "maskNeg" [ testProperty "Double" (maskNeg :: Range Double->Range Double->Bool) , testProperty "NominalDiffTime" (maskNeg :: Range NominalDiffTime->Range NominalDiffTime->Bool) ] , testGroup "mask2D" [ testProperty "Double/Double" (maskD2V :: Range Double->Range Double->Range Double ->Range Double->Bool) , testProperty "Double/NominalDiffTime" (maskD2V :: Range Double->Range Double->Range NominalDiffTime ->Range NominalDiffTime->Bool) ] ] , testGroup "queries" [ testProperty "inRange" inRange2D , testProperty "inOrdRange" (inRangeProp :: Double -> Range Double -> Bool) ] ]