packages feed

ac-library-hs-1.5.3.1: test/Tests/Extra/AhoCorasick.hs

module Tests.Extra.AhoCorasick where

import AtCoder.Extra.AhoCorasick qualified as Ac
import Data.Char (ord)
import Data.List qualified as L
import Data.Vector qualified as V
import Data.Vector.Algorithms qualified as VA
import Data.Vector.Algorithms.Intro qualified as VAI
import Data.Vector.Generic qualified as VG
import Data.Vector.Unboxed qualified as VU
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck as QC

rng :: (Int, Int)
rng = (1, 32)

-- | Generates non-empty string as a vector.
genString :: Gen (VU.Vector Int)
genString = do
  VU.fromList <$> QC.listOf1 QC.arbitrary

-- | Generates non-empty strings as a vectors.
-- not empty
genStrings :: Gen (V.Vector (VU.Vector Int))
genStrings = do
  n <- QC.chooseInt rng
  V.fromList <$> QC.vectorOf n genString

prop_empty :: QC.Positive Int -> Bool
prop_empty (QC.Positive n) = do
  -- Not dies
  let !ac0 = Ac.build V.empty
   in True

prop_numberOfNodes :: TestTree
prop_numberOfNodes = QC.testProperty "numberOfNodes" $ do
  patterns <- genStrings
  let !ac = Ac.build patterns
  pure $ Ac.size ac <= (1 + VG.sum (VG.map VG.length patterns))

unit_banana :: TestTree
unit_banana = testCase "banana" $ do
  let patterns =
        V.fromList $
          map
            (VU.map ord . VU.fromList)
            ["banana", "benefit", "banapple", "ban", "fit"]
  let text = VU.map ord $ VU.fromList "banana bans, apple benefits."
  --                        positions:
  --                                   0123456789012345678901234567
  --                                   0         1         2
  --                        5 matches:
  --                                   ban*   ban*        benefit*
  --                                   banana*                fit*

  let ac = Ac.build patterns
  let matches = Ac.match ac text
  matches @?= VU.fromList [(3, 3), (6, 0), (10, 3), (26, 1), (26, 4)]

genAlphabet :: Gen [Int]
genAlphabet = do
  n <- QC.chooseInt (1, 26)
  L.nub <$> QC.vectorOf n QC.arbitrary

genPatternIn :: [Int] -> Gen (VU.Vector Int)
genPatternIn alphabet = do
  n <- QC.chooseInt (1, 6)
  VU.fromList <$> QC.vectorOf n (QC.elements alphabet)

genTextIn :: [Int] -> Gen (VU.Vector Int)
genTextIn alphabet = do
  n <- QC.chooseInt (0, 1000)
  VU.fromList <$> QC.vectorOf n (QC.elements alphabet)

-- | Returns a vector of @(endPos, patternId)@
naiveMatch :: (HasCallStack) => V.Vector (VU.Vector Int) -> VU.Vector Int -> VU.Vector (Int, Int)
naiveMatch patterns text = VU.concatMap f $ VU.generate (VU.length text) (+ 1)
  where
    nPatterns = V.length patterns
    f :: (HasCallStack) => Int -> VU.Vector (Int, Int)
    f endPos = VU.map (endPos,) . VU.filter (test endPos) $ VU.generate nPatterns id
    test endPos iPattern = t == pat
      where
        pat = patterns VG.! iPattern
        len = VU.length pat
        t = VU.drop (endPos - len) $ VU.take endPos text

prop_random :: TestTree
prop_random = QC.testProperty "random" $ do
  alphabet <- genAlphabet
  n <- QC.chooseInt (1, 100)
  -- NOTE: Patterns must be deduplicated in order to get exact result
  patterns <- VA.nub . V.fromList <$> QC.vectorOf n (genPatternIn alphabet)
  let !ac = Ac.build patterns
  text <- genTextIn alphabet
  pure
    . QC.counterexample (show (patterns, "in", text))
    $ VG.modify VAI.sort (Ac.match ac text) QC.=== VG.modify VAI.sort (naiveMatch patterns text)

tests :: [TestTree]
tests =
  [ QC.testProperty "empty" prop_empty,
    prop_numberOfNodes,
    unit_banana,
    prop_random
  ]