packages feed

dataframe-3.7.0.0: tests/Operations/MonomorphicLoops.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | Per-type column loops checked against list references and null handling.
module Operations.MonomorphicLoops where

import Control.Exception (throw)
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Unboxed as VU
import Data.Word (Word64)
import GHC.Float (castDoubleToWord64, castFloatToWord32)

import DataFrame.Internal.Column (
    Column,
    Columnable,
    findIndices,
    foldLinearGroups,
    foldl1Column,
    foldl1DirectGroups,
    foldlColumn,
    fromList,
    fromUnboxedVector,
    headColumn,
    ifoldrColumn,
    imapColumn,
    mapColumn,
    toVector,
    zipColumns,
    zipWithColumns,
 )

import Test.HUnit

sizes :: [Int]
sizes = [37, 200003]

doublesOf :: Int -> [Double]
doublesOf n = take n (cycle [0 / 0, -(1 / 0), -1.5, -0.0, 0.0, 5.0e-324, 0.5, 2.5, 1 / 0])

intsOf :: Int -> [Int]
intsOf n = take n (cycle [minBound, -3, 0, 1, 7, maxBound])

boolsOf :: Int -> [Bool]
boolsOf n = take n (cycle [True, False, False])

col :: (Columnable a, VU.Unbox a) => [a] -> Column
col = fromUnboxedVector . VU.fromList

out :: forall a e. (Columnable a) => Either e Column -> [a]
out (Left _) = error "column op failed"
out (Right c) = V.toList (either throw id (toVector @a @V.Vector c))

-- | Element results compare by bits for floating types, by '==' otherwise.
class Same a where
    same :: [a] -> [a] -> Bool

instance Same Double where
    same a b = map castDoubleToWord64 a == map castDoubleToWord64 b
instance Same Int where
    same = (==)
instance Same Bool where
    same = (==)
instance Same Float where
    same a b = map castFloatToWord32 a == map castFloatToWord32 b

bits :: [Double] -> [Word64]
bits = map castDoubleToWord64

-- | map, imap and zip of one (element, result) pairing.
pairCases ::
    forall a c.
    (Columnable a, VU.Unbox a, Columnable c, Same c) =>
    String -> (Int -> [a]) -> (a -> c) -> (Int -> a -> c) -> (a -> a -> c) -> [Test]
pairCases label gen f fi f2 =
    [ TestList
        [ TestCase
            ( assertBool
                (label ++ " map n=" ++ show n)
                (same (out @c (mapColumn f (col xs))) (map f xs))
            )
        , TestCase
            ( assertBool
                (label ++ " imap n=" ++ show n)
                (same (out @c (imapColumn fi (col xs))) (zipWith fi [0 ..] xs))
            )
        , TestCase
            ( assertBool
                (label ++ " zip n=" ++ show n)
                (same (out @c (zipWithColumns f2 (col xs) (col ys))) (zipWith f2 xs ys))
            )
        ]
    | n <- sizes
    , let xs = gen n
          ys = reverse xs
    ]

foldCases :: [Test]
foldCases =
    concat
        [ [ TestCase
                ( assertEqual
                    ("foldl DD n=" ++ show n)
                    (bits [foldl (+) 0.25 ds])
                    (bits [fold (+) 0.25 ds])
                )
          , TestCase
                ( assertEqual
                    ("foldl DI n=" ++ show n)
                    (foldl (\k d -> k + fromEnum (d > 0)) 0 ds)
                    (fold (\k d -> k + fromEnum (d > 0)) 0 ds)
                )
          , TestCase
                ( assertEqual
                    ("foldl ID n=" ++ show n)
                    (bits [foldl (\a i -> a + fromIntegral i) 0 is])
                    (bits [fold (\a i -> a + fromIntegral i) (0 :: Double) is])
                )
          , TestCase
                (assertEqual ("foldl II n=" ++ show n) (foldl (+) 1 is) (fold (+) 1 is))
          , TestCase
                ( assertEqual
                    ("foldl1 D n=" ++ show n)
                    (bits [foldl1 (-) ds])
                    (bits [fold1 (-) ds])
                )
          , TestCase (assertEqual ("foldl1 I n=" ++ show n) (maximum is) (fold1 max is))
          , TestCase
                ( assertEqual
                    ("foldl Float n=" ++ show n)
                    (castFloatToWord32 (sum fs))
                    (castFloatToWord32 (fold (+) (0 :: Float) fs))
                )
          ]
        | n <- sizes
        , let ds = doublesOf n
              is = intsOf n
              fs = map realToFrac ds :: [Float]
        ]
  where
    fold ::
        (Columnable a, VU.Unbox a, Columnable b) => (b -> a -> b) -> b -> [a] -> b
    fold f z xs = either throw id (foldlColumn f z (col xs))
    fold1 :: (Columnable a, VU.Unbox a) => (a -> a -> a) -> [a] -> a
    fold1 f xs = either throw id (foldl1Column f (col xs))

-- | Mapping at the element type over a nullable column keeps its bitmap.
nullableCases :: [Test]
nullableCases =
    [ TestCase
        ( assertEqual
            ("nullable map n=" ++ show n)
            (map (fmap (castDoubleToWord64 . (* 2))) xs)
            ( map
                (fmap castDoubleToWord64)
                (out @(Maybe Double) (mapColumn @Double (* 2) (fromList xs)))
            )
        )
    | n <- sizes
    , let xs =
            take n (cycle [Just 1.5, Nothing, Just (-0.0), Just (0 / 0)]) :: [Maybe Double]
    ]

-- Functions over a column with nulls: f never runs on a null row.
nullSemantics :: [Test]
nullSemantics =
    [ eq
        "map skips null rows"
        [Just 25, Nothing, Just 50]
        (outM @Int (mapColumn @Int (100 `div`) ints3))
    , eq
        "map from boxed nullable"
        [Just 2, Nothing, Just 0]
        (outM @Int (mapColumn @T.Text T.length texts3))
    , eq
        "map to Maybe"
        [Just 4, Nothing, Nothing]
        (outM @Int (mapColumn @Int keepBig ints3))
    , eq
        "imap skips null rows"
        [Just 4, Nothing, Just 4]
        (outM @Int (imapColumn @Int (flip (+)) ints3))
    , eq
        "zip: null if either side is"
        [Just 5, Nothing, Nothing]
        (outM @Int (zipWithColumns @Int @Int (\a b -> a + b `div` b) ints3 other3))
    , eq
        "zip to Maybe"
        [Nothing, Nothing, Just 3]
        ( outM @Int
            ( zipWithColumns @Int @Int
                (\a b -> if a < b then Just (b - a) else Nothing)
                ints3
                other3'
            )
        )
    , eq "foldl skips null rows" (Just 6) (ok $ foldlColumn @Int (+) 0 ints3)
    , eq "foldl1 skips null rows" (Just 4) (ok $ foldl1Column @Int max ints3)
    , TestCase
        ( assertBool
            "foldl1 over only nulls fails"
            ( either
                (const True)
                (const False)
                (foldl1Column @Int (+) (fromList [Nothing, Nothing :: Maybe Int]))
            )
        )
    , eq
        "ifoldr skips null rows"
        (Just [0, 2])
        (ok $ ifoldrColumn @Int (\i _ r -> i : r) [] ints3)
    , eq
        "findIndices skips null rows"
        (Just (VU.fromList [0]))
        (ok $ findIndices @Int (== 4) (fromList [Just 4, Nothing, Just (1 :: Int)]))
    , eq
        "findIndices never matches a null"
        (Just VU.empty)
        (ok $ findIndices @Int (== 0) (fromList [Just 3, Nothing :: Maybe Int]))
    , eq
        "head is the first valid row"
        (Just 3)
        (ok $ headColumn @Int (fromList [Nothing, Just (3 :: Int)]))
    , eq
        "zipColumns keeps nulls"
        [Just (4, 1), Nothing, Nothing]
        (outM @(Int, Int) (Right (zipColumns ints3 other3)))
    , eq
        "foldLinearGroups skips null rows"
        [1, 2]
        ( out @Int
            ( foldLinearGroups @Int
                (+)
                0
                (fromList [Just 1, Nothing, Just 2, Nothing :: Maybe Int])
                (VU.fromList [0, 0, 1, 1])
                2
            )
        )
    , eq
        "foldl1DirectGroups: all-null group is null"
        [Nothing, Just 7]
        ( outM @Int
            ( foldl1DirectGroups @Int
                (+)
                (fromList [Nothing, Nothing, Just 3, Just (4 :: Int)])
                (VU.fromList [0 .. 3])
                (VU.fromList [0, 2, 4])
            )
        )
    , TestCase
        ( assertBool
            "toVector refuses an element read with nulls"
            (either (const True) (const False) (toVector @Int @V.Vector ints3))
        )
    , eq
        "toVector allows a bitmap with no nulls"
        (Just [1, 2])
        (ok $ V.toList <$> toVector @Int @V.Vector (fromList [Just 1, Just (2 :: Int)]))
    ]
  where
    ok :: Either e x -> Maybe x
    ok = either (const Nothing) Just
    eq :: (Eq x, Show x) => String -> x -> x -> Test
    eq label expected actual = TestCase (assertEqual label expected actual)
    ints3 = fromList [Just 4, Nothing, Just (2 :: Int)]
    other3 = fromList [Just 1, Just 0, Nothing :: Maybe Int]
    other3' = fromList [Just 1, Just 0, Just (5 :: Int)]
    texts3 = fromList [Just ("ab" :: T.Text), Nothing, Just ""]
    keepBig x = if x > 3 then Just x else Nothing

outM :: forall a e. (Columnable a) => Either e Column -> [Maybe a]
outM (Left _) = error "column op failed"
outM (Right c) = V.toList (either throw id (toVector @(Maybe a) @V.Vector c))

tests :: [Test]
tests =
    concat
        [ nullableCases
        , nullSemantics
        , pairCases @Double @Double
            "DD"
            doublesOf
            (* 1.5)
            (\i d -> d + fromIntegral i)
            (-)
        , pairCases @Double @Int
            "DI"
            doublesOf
            (fromEnum . (> 0))
            (\i d -> i + fromEnum (d < 0))
            (\a b -> fromEnum (a <= b))
        , pairCases @Double @Bool "DB" doublesOf (<= 0.5) (\i d -> even i || d > 0) (<)
        , pairCases @Int @Double
            "ID"
            intsOf
            fromIntegral
            (\i x -> fromIntegral (x + i))
            (\a b -> fromIntegral a - fromIntegral b)
        , pairCases @Int @Int "II" intsOf (* 3) (+) (-)
        , pairCases @Int @Bool "IB" intsOf odd (flip (>)) (==)
        , pairCases @Bool @Double
            "BD"
            boolsOf
            (\b -> if b then 2.5 else 0 / 0)
            (\i b -> if b then fromIntegral i else -0.0)
            (\a b -> if a && b then 1 else -1)
        , pairCases @Bool @Int
            "BI"
            boolsOf
            fromEnum
            (\i b -> if b then i else -i)
            (\a b -> fromEnum a + fromEnum b)
        , pairCases @Bool @Bool "BB" boolsOf not (\i b -> b /= even i) (/=)
        , pairCases @Float @Float
            "Float fallback"
            (map realToFrac . doublesOf)
            (* 2)
            (\i x -> x + fromIntegral i)
            (+)
        , foldCases
        ]