exchangealgebra-0.5.1.0: test/Algebra/ProjWildcardSpec.hs
{-# LANGUAGE ScopedTypeVariables #-}
-- | The @proj@ family matches wildcards one way: only the query's wildcards
-- act as a pattern, and a wildcard stored in a ledger base is an ordinary
-- value. The result must not depend on the shape of the ledger (a single
-- element or a 'Liner').
module Algebra.ProjWildcardSpec (runTests) where
import Control.Monad (unless)
import qualified Data.List as List
import qualified Data.Map.Strict as Map
import System.Exit (exitFailure)
import Test.QuickCheck hiding (label)
import ExchangeAlgebra.Algebra hiding (map, filter)
import qualified ExchangeAlgebra.Algebra as Algebra
-- | Account and unit coordinates; either axis may hold a ledger wildcard.
type TestBase = HatBase (AccountTitles, CountUnit)
type TestAlg = Alg Double TestBase
-- | Generated postings (ledger wildcards allowed) and a query set. Values are
-- small whole numbers, so every 'Double' sum below is exact.
data ProjCase = ProjCase [(Double, TestBase)] [TestBase]
deriving (Show)
genTitle :: Gen AccountTitles
genTitle = frequency [(1, pure wildcard), (4, elements [Cash, Products, Sales])]
genUnit :: Gen CountUnit
genUnit = frequency [(1, pure wildcard), (4, elements [Yen, Amount, Dollar])]
genEntryBase :: Gen TestBase
genEntryBase = (:<) <$> elements [Hat, Not] <*> ((,) <$> genTitle <*> genUnit)
genQueryBase :: Gen TestBase
genQueryBase = (:<) <$> elements [Hat, Not, HatNot] <*> ((,) <$> genTitle <*> genUnit)
genProjCase :: Gen ProjCase
genProjCase = do
count <- chooseInt (0, 12)
entries <- vectorOf count ((,) <$> (fromIntegral <$> chooseInt (1, 1000)) <*> genEntryBase)
queryCount <- chooseInt (1, 4)
ProjCase entries <$> vectorOf queryCount genQueryBase
-- | The same generator restricted to concrete ledger bases.
genConcreteCase :: Gen ProjCase
genConcreteCase = do
ProjCase entries queries <- genProjCase
pure (ProjCase (filter (not . haveWildcard . snd) entries) queries)
-- | Reference predicate, written per axis and independent of the library's
-- 'ignoreWildcard': a query axis matches when it is a wildcard or equals the
-- entry axis. A ledger wildcard is matched only by a query wildcard.
oneWay :: TestBase -> TestBase -> Bool
oneWay (queryHat :< (queryTitle, queryUnit)) (entryHat :< (entryTitle, entryUnit)) =
axis queryHat entryHat && axis queryTitle entryTitle && axis queryUnit entryUnit
where
axis :: Element a => a -> a -> Bool
axis query entry = isWildcard query || query == entry
-- | The old symmetric predicate, for the concrete-ledger regression.
symmetric :: TestBase -> TestBase -> Bool
symmetric = (.==)
build :: [(Double, TestBase)] -> TestAlg
build entries = fromList [value :@ postingBase | (value, postingBase) <- entries]
-- | Raw postings keyed by the rendered base. 'HatBase' orders wildcards as
-- equal to anything, so the base itself cannot be the key here.
observe :: TestAlg -> Map.Map String [Double]
observe = Map.map List.sort
. foldEntries (\result value postingBase ->
Map.insertWith (++) (show postingBase) [value] result) Map.empty
selected :: (TestBase -> TestBase -> Bool) -> ProjCase -> [(Double, TestBase)]
selected match (ProjCase entries queries) =
[entry | entry@(_, postingBase) <- entries, any (`match` postingBase) queries]
-- | Bar-netted norm of the selected entries, per base.
netNorm :: [(Double, TestBase)] -> Double
netNorm entries = sum (Map.elems (Map.map abs perBase))
where
perBase = Map.fromListWith (+)
[ (show (base postingBase), if hat postingBase == Hat then negate value else value)
| (value, postingBase) <- entries ]
propLedger :: ProjCase -> Bool
propLedger testCase@(ProjCase entries queries) =
observe (proj queries (build entries)) == observe (build (selected oneWay testCase))
propSingle :: ProjCase -> Bool
propSingle (ProjCase entries queries) = all agrees entries
where
agrees entry@(value, postingBase) =
observe (proj queries (value :@ postingBase))
== observe (build (selected oneWay (ProjCase [entry] queries)))
propNetNorm :: ProjCase -> Bool
propNetNorm testCase@(ProjCase entries queries) =
projNetNorm queries (build entries) == netNorm (selected oneWay testCase)
&& all single entries
where
single entry@(value, postingBase) =
projNetNorm queries (value :@ postingBase)
== netNorm (selected oneWay (ProjCase [entry] queries))
propConcreteRegression :: ProjCase -> Bool
propConcreteRegression testCase =
selected oneWay testCase == selected symmetric testCase && propLedger testCase
assertTest :: String -> Bool -> IO ()
assertTest label success = do
unless success $ do
putStrLn ("[FAIL] " ++ label)
exitFailure
putStrLn ("[PASS] " ++ label)
quickProperty :: Testable property => String -> property -> IO ()
quickProperty label proposition = do
result <- quickCheckWithResult stdArgs { maxSuccess = 500, chatty = False } proposition
unless (isSuccess result) $ do
putStrLn ("[FAIL] " ++ label ++ ": " ++ output result)
exitFailure
putStrLn ("[PASS] " ++ label)
-- | The reported table: cash carries no unit axis in the ledger.
testReportedTable :: IO ()
testReportedTable = do
let cash = 10 :@ Not :< (Cash, wildcard) :: TestAlg
both = cash .+ 3 :@ Not :< (Products, Yen)
anyTitleYen = [Not :< (wildcard, Yen)]
cashYen = [Not :< (Cash, Yen)]
cashAny = [Not :< (Cash, wildcard)]
products = 3 :@ Not :< (Products, Yen) :: TestAlg
assertTest "proj: wildcard-title query skips ledger-wildcard unit (single)"
(Algebra.isZero (proj anyTitleYen cash))
assertTest "proj: wildcard-title query skips ledger-wildcard unit (Liner)"
(observe (proj anyTitleYen both) == observe products)
assertTest "proj: concrete query does not match a ledger wildcard (single)"
(Algebra.isZero (proj cashYen cash))
assertTest "proj: concrete query does not match a ledger wildcard (Liner)"
(Algebra.isZero (proj cashYen both))
assertTest "proj: query wildcard picks up the ledger wildcard (single)"
(observe (proj cashAny cash) == observe cash)
assertTest "proj: query wildcard picks up the ledger wildcard (Liner)"
(observe (proj cashAny both) == observe cash)
assertTest "projNetNorm: single element and Liner agree on a ledger wildcard"
(projNetNorm cashYen cash == 0 && projNetNorm cashYen both == 0
&& projNetNorm cashAny cash == 10 && projNetNorm cashAny both == 10)
assertTest "proj: projecting a projection is stable"
(observe (proj cashYen (proj cashAny both)) == Map.empty)
-- | 'projByAccountTitle' follows the same one-way rule on the title axis.
testProjByAccountTitle :: IO ()
testProjByAccountTitle = do
let noTitle = 7 :@ Not :< wildcard :: Alg Double (HatBase AccountTitles)
cash = 10 :@ Not :< Cash
ledger = noTitle .+ cash
postings = List.sort . foldEntries (\result value postingBase ->
(show postingBase, value) : result) []
assertTest "projByAccountTitle: concrete title skips a ledger wildcard (single)"
(Algebra.isZero (projByAccountTitle Cash noTitle))
assertTest "projByAccountTitle: concrete title skips a ledger wildcard (Liner)"
(postings (projByAccountTitle Cash ledger) == postings cash)
assertTest "projByAccountTitle: wildcard title selects every entry"
(postings (projByAccountTitle wildcard ledger) == postings ledger)
-- | 'Algebra.map' keeps a rewrite between a wildcard and a concrete coordinate
-- in both directions and in both ledger shapes.
testMapRewritesWildcardAxis :: IO ()
testMapRewritesWildcardAxis = do
let cash = 10 :@ Not :< (Cash, wildcard) :: TestAlg
cashYen = 10 :@ Not :< (Cash, Yen) :: TestAlg
products = 3 :@ Not :< (Products, Amount) :: TestAlg
setCashUnit unit entry@(value :@ (h :< (title, _)))
| title == Cash = value :@ (h :< (title, unit))
| otherwise = entry
setCashUnit _ entry = entry
assertTest "map: wildcard axis rewritten to a concrete value (single)"
(observe (Algebra.map (setCashUnit Yen) cash) == observe cashYen)
assertTest "map: wildcard axis rewritten to a concrete value (Liner)"
(observe (Algebra.map (setCashUnit Yen) (cash .+ products))
== observe (cashYen .+ products))
assertTest "map: concrete axis rewritten to the wildcard (Liner)"
(observe (Algebra.map (setCashUnit wildcard) (cashYen .+ products))
== observe (cash .+ products))
runTests :: IO ()
runTests = do
testReportedTable
testProjByAccountTitle
testMapRewritesWildcardAxis
quickProperty "proj: Liner result equals the one-way reference (ledger wildcards)"
(forAll genProjCase propLedger)
quickProperty "proj: single-element result equals the one-way reference"
(forAll genProjCase propSingle)
quickProperty "projNetNorm: equals the one-way reference for both shapes"
(forAll genProjCase propNetNorm)
quickProperty "proj: concrete ledgers keep the old symmetric result"
(forAll genConcreteCase propConcreteRegression)