{- |
 
Module      : Test.RandomStrings
Description : Generate random strings for testing

A way to generate random character strings for testing. Functions allow
tuning of strings, setting the probability of alphabetic and upper-case
characters in the resulting strings.

Hopefully this is useful for building test and benchmark cases that are
more meaningful than the overly random strings generated by libraries like
'Test.QuickCheck'. 

Note: /Please don't use this to generate passwords!/

Examples:

    Generate a 10-letter word with 1 in 10 upper-case characters

    > word <- randomWord' randomASCII (1%10) 10

    Generate a list of 500 strings from the printable ISO-8859-1 characters
    with random lengths beween 5 and 20 characters.

    > let iso_printable = randomString $ onlyPrintable randomChar8
    > strings <- randomStringsLen iso_printable (5,20) 500
    > 
    > -- benchmarks ...
    >


-}

module Test.RandomStrings
    (
    -- * Character generators
      randomChar
    , randomASCII
    , randomChar8
    
    -- * Specific character generators
    , onlyWith
    , onlyPrintable
    , onlyAlpha
    , onlyAlpha'
    , onlyAlphaNum
    , onlyUpper
    , onlyLower
    , randomClass

    -- * String generators
    , randomWord
    , randomWord'
    , randomString
    , randomString'

    -- * Sets of randomly generated strings
    , randomStrings
    , randomStringsLen
    )
where


import           Data.Bool ( bool )
import           Data.Char
import           Data.Ratio
import           Control.Monad
import           System.Random

import           Test.RandomStrings.Internal



-- | Generate a random Char
randomChar :: IO Char
randomChar = getStdRandom $ random


-- | Generate a random ASCII (7-bit) char in the printable range.
randomASCII :: IO Char
randomASCII = getStdRandom $ randomR (chr 0,chr 127)


-- | Generate a random ISO-8859-1 (8-bit) char
randomChar8 :: IO Char
randomChar8 = getStdRandom $ randomR (chr 0,chr 255)


-- | Random character passing a test
onlyWith
    :: (Char -> Bool)   -- ^ predicate, like 'isAlpha'
    -> IO Char          -- ^ random char generator, like 'randomChar' or 'randomASCII' or 'randomChar8'
    -> IO Char
onlyWith p gen = gen >>= \c -> if p c then return c else onlyWith p gen


-- | Supply a random printable character.
onlyPrintable :: IO Char -> IO Char
onlyPrintable = onlyWith isPrint


-- | Generate a random printable non-alphabet char.
onlyNonAlpha :: IO Char -> IO Char
onlyNonAlpha = onlyWith (not . isAlpha) . onlyPrintable


-- | Generate a random alphabetic char.
onlyAlpha :: IO Char -> IO Char
onlyAlpha = onlyWith isAlpha


-- | Generate a random alphabetic char with a probability of being upper-case.
onlyAlpha'
    :: Rational     -- ^ range 0 to 1; chance of being an upper
    -> IO Char      -- ^ random char generator; 'randomChar', 'randomASCII', 'randomChar8'
    -> IO Char
onlyAlpha' r gen =  randomClass r (onlyUpper gen) (onlyLower gen)


-- | Generate an alphanumeric char.
onlyAlphaNum :: IO Char -> IO Char
onlyAlphaNum = onlyWith isAlphaNum

-- | Randomly generate one of two character types
randomClass
    :: Rational     -- ^ range 0 to 1; chance of using the first generator
    -> IO Char      -- ^ first generator; used if the random value is @True@
    -> IO Char      -- ^ second generator; used if the random value is @False@
    -> IO Char
randomClass r t f = randomBool r >>= bool f t


-- | Generate a random upper-case letter.
onlyUpper :: IO Char -> IO Char
onlyUpper = onlyWith isUpper 


-- | Generate a random lower-case letter.
onlyLower :: IO Char -> IO Char
onlyLower = onlyWith isLower



-- | Generate a random string of alphabetic characters.
randomWord
    :: IO Char      -- ^ random char generator; 'randomChar' or 'randomASCII' or 'randomChar8'
    -> Int          -- ^ length
    -> IO String
randomWord gen len = replicateM len $ onlyAlpha gen

randomWord'
    :: IO Char      -- ^ random char generator; 'randomChar' or 'randomASCII' or 'randomChar8'
    -> Rational     -- ^ range 0 to 1; fraction of upper-case letters
    -> Int          -- ^ length
    -> IO String
randomWord' gen r len = replicateM len $ onlyAlpha' r gen


-- | Generate a random string
randomString
    :: IO Char      -- ^ random char generator; eg. 'randomChar8' or @onlyAlpha randomASCII@
    -> Int          -- ^ length
    -> IO String
randomString = flip replicateM

-- | Generate a random string of printable characters with a balance of
--   alphabetic and upper-case characters.
randomString'
    :: IO Char      -- ^ random char generator; 'randomChar' or 'randomASCII' or 'randomChar8'
    -> Rational     -- ^ range 0 to 1; fraction of alphabetic characters
    -> Rational     -- ^ range 0 to 1; fraction of upper-case letters
    -> Int          -- ^ length
    -> IO String
randomString' gen ra ru len = replicateM len $ randomClass ra (randomClass ru (onlyUpper gen) (onlyLower gen)) (onlyNonAlpha gen)


-- | Generate a list of strings of uniform length.
--
--   > randomStrings (randomString (onlyAlpha randomChar8) 20) 50
--
--   will build a list of 50 alphabetical strings, each 20 characters long.
--
randomStrings
    :: IO String    -- ^ random string generator, eg. 'randomString randomAlpha 20'
    -> Int          -- ^ list length
    -> IO [String]
randomStrings = flip replicateM


-- | Generate a list of strings of variable length.
--   
--   Similar to 'randomStrings', but generates strings with random length.
--   Example:
--
--   > randomStringsLen (randomString' randomASCII (3%4) (1%8)) (10,30) 100
--  
--   Returns a list of 100 strings that are between 10 and 30 characters,
--   with 3/4 of them being alphabetical and 1/8 of those being upper-case.
--
randomStringsLen
    :: (Int -> IO String)   -- ^ random string generator, eg. @randomString randomAlpha@
    -> (Int,Int)            -- ^ range for string length
    -> Int                  -- ^ list length
    -> IO [String]
randomStringsLen gen range len = replicateM len $ getStdRandom (randomR range) >>= gen