packages feed

range-space-0.1.2.0: tests/Properties.hs

{-# 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 hiding ((===))

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)
      ]
    ]