packages feed

horizontal-rule-0.7.0.0: test/Test/HMock/Multiplicity.hs

{-# LANGUAGE DeriveDataTypeable #-}

-- | This module provides the basic vocabulary for talking about multiplicity,
-- which is the number of times something is allowed to happen.  Multiplicities
-- can be any range of natural numbers, with or without an upper bound.
module Test.HMock.Multiplicity
  ( Multiplicity,
    meetsMultiplicity,
    feasible,
    once,
    anyMultiplicity,
    atLeast,
    atMost,
    between,
  )
where

-- | An acceptable range of number of times for something to happen.
--
-- A multiplicity can have a lower and an upper bound.
data Multiplicity = Multiplicity Int (Maybe Int) deriving (Eq)

instance Show Multiplicity where
  show mult = go (normalize mult)
    where
      go m | not (feasible m) = "infeasible"
      go (Multiplicity 0 (Just 0)) = "never"
      go (Multiplicity 1 (Just 1)) = "once"
      go (Multiplicity 2 (Just 2)) = "twice"
      go (Multiplicity 0 Nothing) = "any number of times"
      go (Multiplicity 1 Nothing) = "at least once"
      go (Multiplicity 2 Nothing) = "at least twice"
      go (Multiplicity n Nothing) = "at least " ++ show n ++ " times"
      go (Multiplicity 0 (Just 1)) = "at most once"
      go (Multiplicity 0 (Just 2)) = "at most twice"
      go (Multiplicity 0 (Just n)) = "at most " ++ show n ++ " times"
      go (Multiplicity m (Just n))
        | m == n = show n ++ " times"
        | m == n - 1 = show m ++ " or " ++ show n ++ " times"
        | otherwise = show m ++ " to " ++ show n ++ " times"

-- | A 'Multiplicity' value representing inconsistent expectations.
infeasible :: Multiplicity
infeasible = Multiplicity 0 (Just (-1))

-- | This is an incomplete instance, provided for convenience.
--
-- >>> meetsMultiplicity 5 4
-- False
-- >>> meetsMultiplicity 5 5
-- True
-- >>> between 4 6 - between 1 2
-- 2 to 5 times
instance Num Multiplicity where
  fromInteger n
    | n < 0 = infeasible
    | otherwise =
      normalize $
        Multiplicity (fromInteger n) (Just (fromInteger n))

  m1@(Multiplicity a b) + m2@(Multiplicity c d)
    | feasible m1 && feasible m2 =
      normalize $ Multiplicity (a + c) ((+) <$> b <*> d)
    | otherwise = infeasible

  m1@(Multiplicity a b) - m2@(Multiplicity c d)
    | feasible m1 && feasible m2 =
      normalize $ Multiplicity (maybe 0 (a -) d) (subtract c <$> b)
    | otherwise = infeasible

  (*) = error "Multiplicities are not closed under multiplication"

  abs = id
  signum x = if x == 0 then 0 else 1

normalize :: Multiplicity -> Multiplicity
normalize m@(Multiplicity a b)
  | not (feasible m) = infeasible
  | otherwise = Multiplicity (max a 0) b

-- | Checks whether a certain number satisfies the 'Multiplicity'.
meetsMultiplicity :: Multiplicity -> Int -> Bool
meetsMultiplicity (Multiplicity lo mbhi) n
  | n < lo = False
  | Just hi <- mbhi, n > hi = False
  | otherwise = True

-- | A 'Multiplicity' that means exactly once.
--
-- >>> meetsMultiplicity once 0
-- False
-- >>> meetsMultiplicity once 1
-- True
-- >>> meetsMultiplicity once 2
-- False
once :: Multiplicity
once = 1

-- | A 'Multiplicity' that means any number of times.
-- >>> meetsMultiplicity anyMultiplicity 0
-- True
-- >>> meetsMultiplicity anyMultiplicity 1
-- True
-- >>> meetsMultiplicity anyMultiplicity 10
-- True
anyMultiplicity :: Multiplicity
anyMultiplicity = atLeast 0

-- | A 'Multiplicity' that means at least this many times.
--
-- >>> meetsMultiplicity (atLeast 2) 1
-- False
-- >>> meetsMultiplicity (atLeast 2) 2
-- True
-- >>> meetsMultiplicity (atLeast 2) 3
-- True
atLeast :: Multiplicity -> Multiplicity
atLeast (Multiplicity n _) = normalize $ Multiplicity n Nothing

-- | A 'Multiplicity' that means at most this many times.
--
-- >>> meetsMultiplicity (atMost 2) 1
-- True
-- >>> meetsMultiplicity (atMost 2) 2
-- True
-- >>> meetsMultiplicity (atMost 2) 3
-- False
atMost :: Multiplicity -> Multiplicity
atMost (Multiplicity _ n) = normalize $ Multiplicity 0 n

-- | A 'Multiplicity' that means any number in this interval, endpoints
-- included.  For example, @'between' 2 3@ means 2 or 3 times, while
-- @'between' n n@ is equivalent to @n@.
--
-- >>> meetsMultiplicity (between 2 3) 1
-- False
-- >>> meetsMultiplicity (between 2 3) 2
-- True
-- >>> meetsMultiplicity (between 2 3) 3
-- True
-- >>> meetsMultiplicity (between 2 3) 4
-- False
between :: Multiplicity -> Multiplicity -> Multiplicity
between (Multiplicity m _) (Multiplicity _ n) = normalize $ Multiplicity m n

-- | Checks whether a 'Multiplicity' is capable of matching any number at all.
--
-- >>> feasible once
-- True
-- >>> feasible 0
-- True
-- >>> feasible (once - 2)
-- False
feasible :: Multiplicity -> Bool
feasible (Multiplicity a b) = maybe True (>= max 0 a) b