packages feed

exchangealgebra-0.6.0.0: test/Algebra/AccumSpec.hs

{-# LANGUAGE ScopedTypeVariables #-}

-- | Exact accumulator laws in the Algebra layer. The public HatVal methods
-- are observed against Rational, with no tolerance and no readout operations.
-- Generators cover binary64 boundaries and decimal scales before the fixed cases.
module Algebra.AccumSpec (runTests) where

import Control.Monad (forM_, unless)
import qualified Data.Decimal as Decimal
import Data.Hashable (hashWithSalt)
import qualified Data.List as List
import Data.Maybe (isJust)
import Data.Ratio ((%), denominator)
import GHC.Float (castDoubleToWord64, castWord64ToDouble)
import System.Exit (exitFailure)
import Test.QuickCheck hiding (label)

import ExchangeAlgebra.Algebra.Value
    ( HatVal(..)
    , MoneyDecimal(..)
    , MoneyDouble(..)
    , RationalAccum(..)
    , Rounding(..)
    , ValueError(..)
    , isErrorValue
    , rationalAddAccum
    , rationalRoundAccum
    , roundScaled
    , sumValues
    )
import ExchangeAlgebra.Algebra.Posting
    ( PostedError(..)
    , posted
    , unPosted
    )

-- * Inputs and observations

-- | Largest finite binary64 value.
maximumFinite :: Double
maximumFinite = encodeFloat (2 ^ (53 :: Int) - 1) 971

-- | Smallest positive binary64 value.
subnormal :: Double
subnormal = encodeFloat 1 (-1074)

-- | First integer whose successor is not representable in binary64.
threshold :: Double
threshold = 2 ^ (53 :: Int)

-- | Accumulate without intermediate rounding; signed inputs are intentional.
accumulate :: HatVal n => [n] -> Accum n
accumulate = List.foldl' (flip addAccum) emptyAccum

-- | Bias toward zero, subnormals, 2^53, maximum values, and two decimal places.
genFinite :: Gen Double
genFinite = frequency
    [ (2, elements [0, -0])
    , (3, do
        coefficient <- chooseInteger (1, 2 ^ (52 :: Int) - 1)
        sign <- elements [1, -1]
        pure (sign * encodeFloat coefficient (-1074)))
    , (3, do
        offset <- chooseInteger (-8, 8)
        sign <- elements [1, -1]
        pure (sign * fromRational (toRational threshold + fromInteger offset)))
    , (3, do
        offset <- chooseInteger (0, 8)
        sign <- elements [1, -1]
        pure (sign * encodeFloat (2 ^ (53 :: Int) - 1 - offset) 971))
    , (5, do
        cents <- chooseInteger (-1000000, 1000000)
        pure (fromRational (cents % 100)))
    , (4, do
        coefficient <- chooseInteger (1, 2 ^ (53 :: Int) - 1)
        power <- chooseInt (-1074, 971)
        sign <- elements [1, -1]
        pure (sign * encodeFloat coefficient power))
    ]

-- | Include the scale limit and large exact coefficients as well as cents.
genDecimal :: Gen MoneyDecimal
genDecimal = do
    places <- frequency [(5, pure 2), (2, elements [0, 1, 254, 255]), (1, choose (0, 255))]
    coefficient <- frequency
        [ (6, chooseInteger (-1000000, 1000000))
        , (1, elements [0, 1, -1, 10 ^ (300 :: Int), negate (10 ^ (300 :: Int))])
        ]
    pure (MoneyDecimal (Decimal.Decimal places coefficient))

-- | Rational inputs include nonbinary fractions and floating overflow boundaries.
genRatio :: Gen Rational
genRatio = frequency
    [ (5, do
        numerator <- chooseInteger (-1000000, 1000000)
        divisor <- chooseInteger (1, 10000)
        pure (numerator % divisor))
    , (3, do
        value <- genFinite
        divisor <- chooseInteger (1, 17)
        pure (toRational value / fromInteger divisor))
    , (2, elements
        [ toRational maximumFinite + toRational (encodeFloat 1 970 :: Double)
        , 2 * toRational maximumFinite
        , toRational subnormal / 2
        , 3 * toRational subnormal / 2
        ])
    ]

-- | Report a fixed-case failure with the exact observed values.
check :: (Eq a, Show a) => String -> a -> a -> IO ()
check label expected actual = unless (expected == actual) $ do
    putStrLn ("[FAIL] accum " ++ label ++ ": expected " ++ show expected ++ ", got " ++ show actual)
    exitFailure

-- | Run enough cases to repeatedly exercise the biased exponent and scale ranges.
propertyCheck :: String -> Property -> IO ()
propertyCheck label proposition = do
    result <- quickCheckWithResult stdArgs { maxSuccess = 600, chatty = False } proposition
    unless (isSuccess result) $ do
        putStrLn ("[FAIL] accum " ++ label ++ ": " ++ output result)
        exitFailure
    putStrLn ("[PASS] accum " ++ label ++ " (600 cases)")

-- * Laws

-- | A1-A6, A5', E1, E2, T3, S1, V1-V3, B1, and P1 for each finite value type.
-- Exact sums and signs use Rational; rounded observations use the value type's ==.
propFiniteLaws :: forall n. (HatVal n, Real n) => Gen n -> Property
propFiniteLaws generator =
    forAll (chooseInt (0, 20) >>= flip vectorOf generator) $ \values ->
    forAll (shuffle values) $ \permuted ->
    forAll generator $ \value ->
    forAll (chooseInt (0, length values)) $ \firstCut ->
    forAll (chooseInt (firstCut, length values)) $ \secondCut ->
        let (left, rest) = splitAt firstCut values
            (middle, right) = splitAt (secondCut - firstCut) rest
            first = accumulate left
            second = accumulate middle
            third = accumulate right
            total = accumulate values
            expected = sum (map toRational values)
            merged = mergeAccum first (mergeAccum second third)
            rounded = roundAccum total
            roundedFinite = isJust (exactAccum (addAccum rounded emptyAccum))
            successfulSplit = expected > 0 && roundedFinite
            checked = fmap unPosted (posted value)
            expectedPosted = case valueError value of
                Just failure -> Left (InvalidValue failure)
                Nothing -> case postingUpperBound :: Maybe n of
                    Just bound | value > bound -> Left AboveBound
                    _ | value == zeroValue -> Right zeroValue
                      | otherwise -> Right value
        in conjoin
            [ counterexample "A1 commutative" $
                exactAccum (mergeAccum first second) === exactAccum (mergeAccum second first)
            , counterexample "A1 associative" $
                exactAccum (mergeAccum (mergeAccum first second) third) === exactAccum merged
            , counterexample "A1 left identity" $
                exactAccum (mergeAccum emptyAccum total) === Just expected
            , counterexample "A1 right identity" $
                exactAccum (mergeAccum total emptyAccum) === Just expected
            , counterexample "A2 singleton merge" $
                exactAccum (addAccum value total)
                    === exactAccum (mergeAccum total (addAccum value emptyAccum))
            , counterexample "A3 involution" $
                exactAccum (negateAccum (negateAccum total)) === Just expected
            , counterexample "A3/A6 cancellation" $
                exactAccum (mergeAccum total (negateAccum total)) === Just 0
            , counterexample "A3 signed oracle" $
                exactAccum (negateAccum total) === Just (negate expected)
            , counterexample "A4 singleton" $
                roundAccum (addAccum value emptyAccum) === value
            , counterexample "A5/A5' domain and reconstruction" $ case splitAccum total of
                Nothing -> property (not successfulSplit)
                Just components -> conjoin
                    [ property successfulSplit
                    , property (all (\x -> x > 0 && valueError x == Nothing) components)
                    , sum (map toRational components) === expected
                    ]
            , counterexample "A5 path independence" $
                splitAccum total === splitAccum (accumulate permuted)
            , counterexample "A5 merge independence" $
                splitAccum total === splitAccum merged
            , counterexample "E1 exact value" $ exactAccum total === Just expected
            , counterexample "E1 permutation" $
                exactAccum (accumulate permuted) === Just expected
            , counterexample "E1 merge tree" $ exactAccum merged === Just expected
            , counterexample "exact sign" $ signAccum total === Just (compare expected 0)
            , counterexample "E2 finite rounding" $ case roundRational Nearest expected of
                Just result -> rounded === result
                Nothing -> property (not roundedFinite)
            , counterexample "T3 representable input" $
                roundRational Nearest (toRational value) === Just value
            , counterexample "S1 definition" $
                sumValues values === roundAccum (foldr addAccum emptyAccum values)
            , counterexample "S1 permutation" $ sumValues values === sumValues permuted
            , counterexample "V1 zero" $ valueError (zeroValue :: n) === Nothing
            , counterexample "V2 valid is finite and nonnegative" $
                property (valueError value /= Nothing ||
                    (value >= zeroValue && isJust (exactAccum (addAccum value emptyAccum))))
            , counterexample "V3 finite signed values" $
                valueError value === case compare value zeroValue of
                    LT -> Just NegativeValue
                    _ -> Nothing
            , counterexample "isErrorValue is a function of valueError" $
                isErrorValue value === isJust (valueError value)
            , counterexample "B1 bound" $ case postingUpperBound :: Maybe n of
                Nothing -> property True
                Just bound -> property (bound > zeroValue && valueError bound == Nothing)
            , counterexample "P1 priority and accepted value" $ checked === expectedPosted
            ]

-- | A5 additionally fixes each Double component to the largest value below the remainder.
propGreedySplit :: Property
propGreedySplit = forAll (chooseInt (0, 20) >>= flip vectorOf genFinite) $ \values ->
    let expected = sum (map toRational values)
        verify _ [] = True
        verify residual (component : rest) =
            toRational component <= residual
                && (isInfinite successor || toRational successor > residual)
                && verify (residual - toRational component) rest
          where
            successor = castWord64ToDouble (castDoubleToWord64 component + 1)
    in case splitAccum (accumulate values) of
        Nothing -> property (expected <= 0 || isInfinite (fromRational expected :: Double))
        Just components -> property (verify expected components)

-- | T1 and T3 for a positive exact unit, including ties and negative multiples.
propDecimalUnit :: Property
propDecimalUnit = forAll genDecimal $ \rawUnit ->
    forAll genRatio $ \value ->
    forAll (chooseInteger (-10000, 10000)) $ \multiple ->
        let unit = abs rawUnit + MoneyDecimal (Decimal.Decimal 255 1)
            rationalUnit = toRational unit
            tied = (fromInteger multiple + 1 % 2) * rationalUnit
            evenMultiple = multiple + multiple `mod` 2
            checkUnit rational = case roundRational (ToMultipleOf unit) rational of
                Nothing -> counterexample "positive exact unit rejected" False
                Just rounded ->
                    let quotient = toRational rounded / rationalUnit
                    in conjoin
                        [ denominator quotient === 1
                        , property (abs (toRational rounded - rational) <= rationalUnit / 2)
                        , property (abs (toRational rounded - rational) /= rationalUnit / 2
                            || even (round quotient :: Integer))
                        ]
        in conjoin
            [ checkUnit value
            , checkUnit tied
            , fmap toRational (roundRational (ToMultipleOf unit) tied)
                === Just (fromInteger evenMultiple * rationalUnit)
            , roundRational (ToMultipleOf unit) (fromInteger multiple * rationalUnit)
                === Just (fromInteger multiple * unit)
            , roundRational (ToMultipleOf (negate unit)) value === Nothing
            , roundRational (ToMultipleOf (0 :: MoneyDecimal)) value === Nothing
            ]

-- | T2: a reduced denominator is representable precisely when it divides 10^255.
-- This oracle does not repeat the implementation's stepwise factorization.
propDecimalNearest :: Property
propDecimalNearest =
    forAll (chooseInteger (-10000, 10000)) $ \coefficient ->
    forAll (chooseInt (0, 260)) $ \twos ->
    forAll (chooseInt (0, 260)) $ \fives ->
    forAll (elements [1, 3, 7, 11 :: Integer]) $ \other ->
        let value = coefficient % (2 ^ twos * 5 ^ fives * other)
            result = roundRational Nearest value :: Maybe MoneyDecimal
            representable = 10 ^ (255 :: Int) `mod` denominator value == 0
        in conjoin
            [ isJust result === representable
            , case result of
                Nothing -> property True
                Just rounded -> toRational rounded === value
            ]

-- | T4 and T5 for both floating types; the oracle rounds Rational directly.
propFloatingRounding :: Property
propFloatingRounding = forAll genRatio $ \value -> forAll genFinite $ \unit ->
    let rounded = fromRational value :: Double
        expected
            | isInfinite rounded = Nothing
            | otherwise = Just rounded
    in conjoin
        [ (roundRational Nearest value :: Maybe Double) === expected
        , roundRational Nearest value === fmap MoneyDouble expected
        , roundRational (ToMultipleOf unit) value === Nothing
        , roundRational (ToMultipleOf (MoneyDouble unit)) value === Nothing
        ]

-- | The Rational helper preserves finite values, signs, and the supplied conversion.
propRationalHelper :: Property
propRationalHelper = forAll (chooseInt (0, 20) >>= flip vectorOf genFinite) $ \values ->
    let state = List.foldl' (flip rationalAddAccum) (RationalAccum 0) values
        expected = sum (map toRational values)
    in conjoin
        [ state === RationalAccum expected
        , rationalRoundAccum id state === expected
        , rationalRoundAccum negate state === negate expected
        ]

-- * Fixed boundaries

-- | Integer overflow, cancellation, two decimal places, subnormals, and greedy splits.
-- Each case also checks the MoneyDouble delegation against the same Rational oracle.
testFloatingBoundaries :: IO ()
testFloatingBoundaries = do
    let machineHalf = encodeFloat 1 (finiteBitSizeInt - 2)
        machineMinimum = fromIntegral (minBound :: Int) :: Double
        belowMidpoint = encodeFloat (2 ^ (53 :: Int) - 1) 917
        midpoint = encodeFloat 1 970
        aboveMidpoint = encodeFloat (2 ^ (52 :: Int) + 1) 918
        cases =
            [ ("Int positive overflow", [machineHalf, machineHalf])
            , ("Int negative overflow", [machineMinimum, -1])
            , ("above 2^53 then fraction", [threshold, 1, 0.25])
            , ("above 2^53 then fraction then cancel", [threshold, 1, 0.25, -threshold])
            , ("two decimal places", [12.34, 56.78, -12.34, 0.01])
            , ("0.1+0.2", [0.1, 0.2])
            , ("M+M-M", [maximumFinite, maximumFinite, -maximumFinite])
            , ("M+s", [maximumFinite, subnormal])
            , ("M+M", [maximumFinite, maximumFinite])
            , ("subnormal cancellation", [maximumFinite, subnormal, -maximumFinite])
            , ("smallest normal minus subnormal", [encodeFloat 1 (-1022), -subnormal])
            , ("three residuals", [0.1, encodeFloat 1 (-60), subnormal])
            , ("tie down", [threshold, 1])
            , ("tie up", [threshold + 2, 1])
            , ("beyond tie", [threshold, 1, subnormal])
            , ("overflow midpoint cancellation", [maximumFinite, midpoint, -midpoint])
            , ("zero", [-0, 0])
            ]
    forM_ cases $ \(label, values) -> do
        let expected = sum (map toRational values)
            state = accumulate values
            wrapped = accumulate (map MoneyDouble values)
        check (label ++ " exact") (Just expected) (exactAccum state)
        check (label ++ " round") (fromRational expected :: Double) (roundAccum state)
        check (label ++ " sign") (Just (compare expected 0)) (signAccum state)
        check (label ++ " wrapped exact") (Just expected) (exactAccum wrapped)
        check (label ++ " wrapped round") (MoneyDouble (fromRational expected)) (roundAccum wrapped)
        check (label ++ " negate") (Just (negate expected)) (exactAccum (negateAccum state))
        check (label ++ " split delegation") (fmap (map MoneyDouble) (splitAccum state))
            (splitAccum wrapped)
    check "minBound negation" (Just (negate (toRational machineMinimum)))
        (exactAccum (negateAccum (accumulate [machineMinimum])))
    check "minBound negation rounds" (negate machineMinimum)
        (roundAccum (negateAccum (accumulate [machineMinimum])))
    check "negative zero normalized" False (isNegativeZero (sumValues [-0 :: Double]))
    check "0.1+0.2 greedy" (Just [0.3, encodeFloat 1 (-55)])
        (splitAccum (accumulate [0.1 :: Double, 0.2]))
    check "M+s greedy" (Just [maximumFinite, subnormal])
        (splitAccum (accumulate [maximumFinite, subnormal]))
    check "below overflow midpoint split" (Just [maximumFinite, belowMidpoint])
        (splitAccum (accumulate [maximumFinite, belowMidpoint]))
    check "at overflow midpoint split" Nothing
        (splitAccum (accumulate [maximumFinite, midpoint]))
    check "above overflow midpoint split" Nothing
        (splitAccum (accumulate [maximumFinite, aboveMidpoint]))
    check "M+M split" Nothing (splitAccum (accumulate [maximumFinite, maximumFinite]))
    check "M+M scaled once" (Just maximumFinite)
        (roundScaled Nearest (1 % 2) (accumulate [maximumFinite, maximumFinite]))
    check "M+M scaled once wrapped" (Just (MoneyDouble maximumFinite))
        (roundScaled Nearest (1 % 2) (accumulate (map MoneyDouble [maximumFinite, maximumFinite])))
    check "positive subnormal split" (Just [subnormal]) (splitAccum (accumulate [subnormal]))
    check "negative state split" Nothing (splitAccum (accumulate [-subnormal]))
    check "zero state split" Nothing (splitAccum (accumulate [0 :: Double]))
    check "subnormal tie to zero" (Just 0 :: Maybe Double)
        (roundRational Nearest (toRational subnormal / 2))
    check "subnormal tie to even" (Just (2 * subnormal))
        (roundRational Nearest (3 * toRational subnormal / 2))
  where
    finiteBitSizeInt = length (takeWhile (> 0) (iterate (`div` 2) (maxBound :: Int))) + 1

-- | A6 and V3 over the IEEE table, for add, merge, negation, and both floating types.
testSpecialValues :: IO ()
testSpecialValues = do
    let values = [0, 1, -1, 1 / 0, -1 / 0, 0 / 0 :: Double]
        observe value
            | isNaN value = Nothing
            | otherwise = Just value
    forM_ values $ \left -> do
        let singleton = accumulate [left]
            expectedError
                | isNaN left || isInfinite left = Just NonFiniteValue
                | left < 0 = Just NegativeValue
                | otherwise = Nothing
        check "V3 Double" expectedError (valueError left)
        check "V3 MoneyDouble" expectedError (valueError (MoneyDouble left))
        check "special negation" (observe (negate left))
            (observe (roundAccum (negateAccum singleton)))
        forM_ values $ \right -> do
            let expected = left + right
                states = [addAccum right singleton, mergeAccum singleton (accumulate [right])]
                wrapped = accumulate (map MoneyDouble [left, right])
            forM_ states $ \state -> do
                check "A6 addition table" (observe expected) (observe (roundAccum state))
                check "A6 exact classification" (not (isNaN expected || isInfinite expected))
                    (isJust (exactAccum state))
                check "A6 sign" (fmap (`compare` 0) (observe expected)) (signAccum state)
            check "A6 wrapped table" (fmap MoneyDouble (observe expected))
                (case roundAccum wrapped of
                    MoneyDouble value -> fmap MoneyDouble (observe value))
            check "A6 wrapped merge" (observe expected)
                (case roundAccum (mergeAccum (accumulate [MoneyDouble left])
                                            (accumulate [MoneyDouble right])) of
                    MoneyDouble value -> observe value)
    forM_ [1 / 0, -1 / 0, 0 / 0 :: Double] $ \special -> do
        let state = accumulate [special]
        check "nonfinite split" Nothing (splitAccum state)
        check "nonfinite exact" Nothing (exactAccum state)
        check "nonfinite scaled" Nothing (roundScaled Nearest 0 state)
        check "P1 nonfinite before bound or sign" (Left (InvalidValue NonFiniteValue))
            (fmap unPosted (posted special))
        check "P1 wrapped nonfinite before bound or sign" (Left (InvalidValue NonFiniteValue))
            (fmap unPosted (posted (MoneyDouble special)))
        forM_ [[], [0.1], [0.1, 0.2], [maximumFinite, maximumFinite]] $ \finite -> do
            let finiteState = accumulate finite
            check "special absorbs finite state" (observe special)
                (observe (roundAccum (mergeAccum state finiteState)))
            check "finite state absorbs special" (observe special)
                (observe (roundAccum (mergeAccum finiteState state)))
    forM_ [0, -1, 0.01, 1 / 0, -1 / 0, 0 / 0 :: Double] $ \unit -> do
        check "T5 Double all units" Nothing (roundRational (ToMultipleOf unit) (1 % 3))
        check "T5 MoneyDouble all units" Nothing
            (roundRational (ToMultipleOf (MoneyDouble unit)) (1 % 3))

-- | T1-T3 at exact decimal scale boundaries and the Eq/Hashable contract.
testDecimalBoundaries :: IO ()
testDecimalBoundaries = do
    let tiny = MoneyDecimal (Decimal.Decimal 255 1)
        cents = MoneyDecimal (Decimal.Decimal 2 1)
        one = MoneyDecimal (Decimal.Decimal 1 10)
        sameOne = MoneyDecimal (Decimal.Decimal 2 100)
    check "T2 10^-255" (Just tiny) (roundRational Nearest (1 % 10 ^ (255 :: Int)))
    check "T2 10^-256" (Nothing :: Maybe MoneyDecimal)
        (roundRational Nearest (1 % 10 ^ (256 :: Int)))
    check "T2 reduced denominator" (Just tiny)
        (roundRational Nearest (10 % 10 ^ (256 :: Int)))
    check "T2 1/3" (Nothing :: Maybe MoneyDecimal) (roundRational Nearest (1 % 3))
    check "T2 2^-255" (Just (1 % 2 ^ (255 :: Int)))
        (toRational <$> (roundRational Nearest (1 % 2 ^ (255 :: Int)) :: Maybe MoneyDecimal))
    check "T2 5^-255" (Just (1 % 5 ^ (255 :: Int)))
        (toRational <$> (roundRational Nearest (1 % 5 ^ (255 :: Int)) :: Maybe MoneyDecimal))
    check "T2 signed boundary" (Just (negate tiny))
        (roundRational Nearest ((-1) % 10 ^ (255 :: Int)))
    check "A5' decimal positive" (Just [1 + tiny]) (splitAccum (accumulate [1, tiny]))
    check "A5' decimal zero" Nothing (splitAccum (accumulate [tiny, negate tiny]))
    check "A5' decimal negative" Nothing (splitAccum (accumulate [negate tiny]))
    check "S1 decimal cents" (3 % 10)
        (toRational (sumValues [MoneyDecimal (Decimal.Decimal 1 1),
                               MoneyDecimal (Decimal.Decimal 1 2)]))
    check "A6 decimal exact cancellation" (Just 0)
        (exactAccum (mergeAccum (accumulate [1, tiny]) (negateAccum (accumulate [1, tiny]))))
    forM_ [(1 % 200, 0), (3 % 200, 2), ((-1) % 200, 0), ((-3) % 200, -2)] $
        \(value, multiple) ->
            check "T1 ties to even cents" (Just (fromInteger multiple * cents))
                (roundRational (ToMultipleOf cents) value)
    check "T1 nonpositive unit" Nothing (roundRational (ToMultipleOf (0 :: MoneyDecimal)) 1)
    check "decimal representations equal" True (one == sameOne)
    forM_ [0, 1, -1, maxBound, minBound :: Int] $ \salt -> do
        check "equal decimal hash" (hashWithSalt salt one) (hashWithSalt salt sameOne)
        check "decimal zero hash" (hashWithSalt salt (MoneyDecimal (Decimal.Decimal 0 0)))
            (hashWithSalt salt (MoneyDecimal (Decimal.Decimal 255 0)))

-- | B1 and P1 at adjacent floating values and an unbounded decimal magnitude.
testPostingBoundaries :: IO ()
testPostingBoundaries = do
    let bound = 2 ^ (900 :: Int) :: Double
        below = castWord64ToDouble (castDoubleToWord64 bound - 1)
        above = castWord64ToDouble (castDoubleToWord64 bound + 1)
        huge = MoneyDecimal (Decimal.Decimal 0 (10 ^ (1000 :: Int)))
    check "B1 Double bound" (Just bound) (postingUpperBound :: Maybe Double)
    check "B1 MoneyDouble bound" (Just (MoneyDouble bound)) (postingUpperBound :: Maybe MoneyDouble)
    check "B1 MoneyDecimal unbounded" Nothing (postingUpperBound :: Maybe MoneyDecimal)
    forM_ [0, -0, below, bound] $ \value -> do
        check "P1 accepted Double" (Right value) (fmap unPosted (posted value))
        check "P1 accepted MoneyDouble" (Right (MoneyDouble value))
            (fmap unPosted (posted (MoneyDouble value)))
    check "P1 above Double bound" (Left AboveBound) (fmap unPosted (posted above))
    check "P1 above MoneyDouble bound" (Left AboveBound)
        (fmap unPosted (posted (MoneyDouble above)))
    check "P1 decimal has no bound" (Right huge) (fmap unPosted (posted huge))
    check "P1 negative decimal" (Left (InvalidValue NegativeValue))
        (fmap unPosted (posted (negate huge)))
    check "P1 positive zero Double" (Right False)
        (fmap (isNegativeZero . unPosted) (posted (-0 :: Double)))
    check "P1 positive zero MoneyDouble" (Right False)
        (fmap (\checked -> case unPosted checked of MoneyDouble value -> isNegativeZero value)
            (posted (MoneyDouble (-0))))

-- | Run the laws and fixed cases from the package's test suite.
runTests :: IO ()
runTests = do
    testFloatingBoundaries
    testSpecialValues
    testDecimalBoundaries
    testPostingBoundaries
    propertyCheck "Double A1-A6/A5'/E1/E2/T3/S1/V1-V3/B1/P1" (propFiniteLaws genFinite)
    propertyCheck "MoneyDouble A1-A6/A5'/E1/E2/T3/S1/V1-V3/B1/P1"
        (propFiniteLaws (MoneyDouble <$> genFinite))
    propertyCheck "MoneyDecimal A1-A6/A5'/E1/T3/S1/V1-V3/B1/P1" (propFiniteLaws genDecimal)
    propertyCheck "A5 greedy Double split" propGreedySplit
    propertyCheck "T1/T3 decimal unit and ties" propDecimalUnit
    propertyCheck "T2 decimal representability" propDecimalNearest
    propertyCheck "T4/T5 floating rounding" propFloatingRounding
    propertyCheck "RationalAccum helper" propRationalHelper
    putStrLn "[PASS] accumulator boundaries and IEEE table"