packages feed

palindromes-1.0: tests/QuickCheckGenerators.hs

{- This program has been developed by students from the bachelor Computer Science at Utrecht
University within the Software Project course.

© Copyright Utrecht University (Department of Information and Computing Sciences) and Johan Jeuring -}

module QuickCheckGenerators
    ( generatePalindromes
    ) where

import Data.List (intercalate)
import Data.Vector (Vector (..), fromList, toList, (//))
import Test.QuickCheck
    ( Arbitrary (..)
    , Gen
    , Property
    , arbitrary
    , choose
    , chooseInt
    , forAll
    , generate
    , listOf
    , oneof
    , suchThat
    , vectorOf
    )
import Test.QuickCheck.Gen (elements, genFloat)

import Data.Algorithms.Palindromes.DNA (DNA (..), dnaToChar)
import Data.Algorithms.Palindromes.Finders
    ( Algorithm (..)
    , OutputFormat (..)
    , Variant (VarDNA, VarPlain, VarText, VarWord)
    )
import Data.Algorithms.Palindromes.PalEq (PalEq (..))
import Data.Algorithms.Palindromes.Settings (Settings (..))

maxPalInPalGeneration, minWordLength, maxWordLength :: Int
-- the maximum amount of palInPal depth the generated palindrome will have
-- a Depth of 1 gives a palindrome with one level of palindrome (pallap)
-- a Depth of 2 gives a palindrome with two levels of palindromes (pallappallap) etc
maxPalInPalGeneration = 2
-- The minimum and maximum length of the generated words
minWordLength = 1
maxWordLength = 7

-- | Constructs a Gen String for palindromes, based on the algorithm settings being used
generatePalindromes :: Settings -> Gen String
generatePalindromes settings = case variant settings of
    VarPlain -> generatePalindromeString id plainCharGenerator settings
    VarDNA -> map dnaToChar <$> generatePalindromeString compDNA dnaCharGenerator settings
    VarWord -> wordToString $ generatePalindromeString id wordGenerator settings
    _ -> generatePalindromeString id puncCharGenerator settings

-- | Converts the DNA datatype to it's complement
compDNA :: DNA -> DNA
compDNA A = T
compDNA T = A
compDNA C = G
compDNA G = C
compDNA N = N

-- | Converts a Gen [String] to a Gen String by concatenating the strings in the list with a space
wordToString :: Gen [[Char]] -> Gen String
wordToString gen = do unwords <$> gen

-- | Generates random strings for punctuation palindromes, these can be anything
puncCharGenerator :: Gen Char
puncCharGenerator = choose (' ', '~') `suchThat` (`notElem` ['\\', '"', ' ', '\n'])

-- | Generates random strings for plain palindromes
alphabetPlain = ['a' .. 'z'] ++ ['A' .. 'Z']

plainCharGenerator :: Gen Char
plainCharGenerator = elements alphabetPlain

-- makes DNA an instance of Arbitrary, so random DNA strings can be generated
instance Arbitrary DNA where
    arbitrary = elements [A, T, C, G]

-- | Simple Gen [DNA]
dnaCharGenerator :: Gen DNA
dnaCharGenerator = arbitrary :: Gen DNA

-- | Randomly generates one word with length between 2 and 7
wordGenerator :: Gen [Char]
wordGenerator = do
    randomWordLength <- chooseInt (minWordLength, maxWordLength)
    vectorOf randomWordLength $
        choose (' ', '~') `suchThat` (`notElem` ['\\', '"', ' ', '\n'])

{- | Generates either a string, palindrome or palInPal palindrome with random characters
around them. The function passes a function palComp all the way down to palInPal. This is
a bit weird, but defining a complement function in the PalEq datatype means that the (=:=)
function must be injective, which is an unnecessary restriction otherwise.
-}
generatePalindromeString
    :: (Arbitrary a, PalEq a) => (a -> a) -> Gen a -> Settings -> Gen [a]
generatePalindromeString palComp charGenerator settings = do
    -- get the gapSize and maxErrors settings from the algorithm settings
    let (gapSize, maxErrors) = case algorithm settings of
            AlgQuadratic{algGapSize = gapSize, algMaxErrors = maxErrors} -> (gapSize, maxErrors)
            _ -> (0, 0)

    -- generate random string to add noise in front of the palindrome
    randomStart <- listOf charGenerator
    -- generate the palindrome
    palGenerator <-
        oneof
            [ listOf charGenerator
            , addErrors maxErrors charGenerator $ generatePalindrome palComp charGenerator gapSize
            , addErrors maxErrors charGenerator $ multiPalInPal palComp charGenerator gapSize
            ]
    -- generate random string to add noise behind the palindrome
    randomEnd <- listOf charGenerator
    return $ randomStart ++ palGenerator ++ randomEnd

-- | Generates a palindrome
generatePalindrome :: (Arbitrary a, PalEq a) => (a -> a) -> Gen a -> Int -> Gen [a]
generatePalindrome palComp charGenerator gapSize = do
    randomString <- listOf charGenerator
    palInPal palComp charGenerator gapSize 1 randomString

-- | generates a palindrome with a random amount of palInPal depth, note that a non palindrome can be generated if the random number is 0
multiPalInPal :: (Arbitrary a, PalEq a) => (a -> a) -> Gen a -> Int -> Gen [a]
multiPalInPal palComp charGenerator gapSize = do
    randomString <- listOf charGenerator
    palInPalDepth <- choose (0, maxPalInPalGeneration) -- generate a random int between 0 and maxDepth
    palInPal palComp charGenerator gapSize palInPalDepth randomString

{- | Generate a palindrome from string with Int amount of palindromes -
a depth of 0 gives the input back, (pal) -
a depth of 1 gives a palindrome with one level of palindrome (pallap) -
a depth of 2 gives a palindrome with two levels of palindrome (pallappallap)
-}
palInPal :: (Arbitrary a, PalEq a) => (a -> a) -> Gen a -> Int -> Int -> [a] -> Gen [a]
palInPal palComp charGenerator gapSize depth string = do
    case depth of
        0 -> return string
        1 -> do
            unevenOrGap <- generateGap charGenerator gapSize -- allows for uneven palindromes and gaps
            return $ string ++ unevenOrGap ++ reversePal palComp string
        _ -> do
            uneven <- generateGap charGenerator 1 -- allows for uneven palInPals
            palInPal palComp charGenerator gapSize (depth - 1) $
                string ++ uneven ++ reversePal palComp string

reversePal :: (PalEq a) => (a -> a) -> [a] -> [a]
reversePal palComp = reverse . map palComp

{- | generates a string with a max length of 'maxGapSize' -
the string will be used to make gapped palindromes or uneven palindrome -
even pals are reprented by this string being empty
-}
generateGap :: (Arbitrary a) => Gen a -> Int -> Gen [a]
generateGap charGenerator maxGapSize = do
    let maxGapSize' = fromIntegral $ max maxGapSize 1 -- the maxGapSize' is always set to at least 1 to account for uneven palindromes
    randomGapSize <- choose (0, maxGapSize')
    vectorOf randomGapSize charGenerator -- generates a string of length 'randomGapSize'

-- | Generate x amount of errors in a string
addErrors :: Int -> Gen a -> Gen [a] -> Gen [a]
addErrors maxErrors charGenerator palGenerator = case maxErrors of
    0 -> palGenerator
    _ -> do
        randomErrorCount <- choose (0, maxErrors)
        _palGenerator <- palGenerator
        -- generate x indices on which the errors will be applied
        errorIndices <- vectorOf randomErrorCount $ choose (0, max 0 $ -1 + length _palGenerator)
        -- generate x random characters to replace the characters at the error indices
        replacementChars <- vectorOf randomErrorCount charGenerator
        let -- replace the characters at the error indices with the replacement characters
            -- we do this by converting into and from a vector
            errorZip = zip errorIndices replacementChars
            replaceErrors = toList $ replaceErrors' errorZip $ fromList _palGenerator
            replaceErrors' :: [(Int, a)] -> Vector a -> Vector a
            replaceErrors' zip _palGenerator = _palGenerator // zip
        if null _palGenerator || randomErrorCount == 0 then palGenerator else return replaceErrors