packages feed

haskell-fsrs-7.0.0: test/Test/FSRS/Gen.hs

-- | Generators and comparison helpers shared by the test modules.
--
-- Everything here produces /valid/ inputs: parameter vectors inside the box
-- the upstream optimiser clips to, stabilities inside
-- @['stabilityMin', 'stabilityMax']@, difficulties inside @[1, 10]@ and
-- non-negative elapsed times. Properties that should hold for nonsense inputs
-- too say so explicitly.
module Test.FSRS.Gen
  ( -- * Generators
    genParameters
  , genRating
  , genStability
  , genDifficulty
  , genMemoryState
  , genElapsedDays
  , genDesiredRetention
  , genReviewHistory
  , genUTCTime
  , genScheduler

    -- * Approximate comparison
  , approxEqual
  , relativeError
  ) where

import Data.Time.Calendar (addDays, fromGregorian)
import Data.Time.Clock (UTCTime (..), secondsToDiffTime)
import Test.QuickCheck

import FSRS

-- | A parameter vector inside the valid box, biased towards the defaults.
genParameters :: Gen Parameters
genParameters =
  frequency
    [ (1, pure defaultParameters)
    , (3, genRandomParameters)
    ]

genRandomParameters :: Gen Parameters
genRandomParameters = do
  ws <- traverse choose parameterBounds
  -- `clampParameters` only re-establishes the three ordering constraints; the
  -- weights are already inside their individual bounds.
  case clampParameters ws of
    Right p -> pure p
    Left errs -> error ("genRandomParameters: " <> show errs)

genRating :: Gen Rating
genRating = elements allRatings

-- | Log-uniform across the whole legal range, with the endpoints thrown in.
genStability :: Gen Stability
genStability =
  frequency
    [ (1, pure stabilityMin)
    , (1, pure stabilityMax)
    , (1, pure 1)
    , (9, exp <$> choose (log stabilityMin, log stabilityMax))
    ]

genDifficulty :: Gen Difficulty
genDifficulty =
  frequency
    [ (1, pure difficultyMin)
    , (1, pure difficultyMax)
    , (8, choose (difficultyMin, difficultyMax))
    ]

genMemoryState :: Gen MemoryState
genMemoryState = MemoryState <$> genStability <*> genDifficulty

-- | Anything from "the same instant" to ten years, log-uniform in between.
genElapsedDays :: Gen Days
genElapsedDays =
  frequency
    [ (2, pure 0)
    , (8, exp <$> choose (log (1 / 86400), log 3650))
    ]

genDesiredRetention :: Gen Retrievability
genDesiredRetention =
  frequency
    [ (1, choose (0.5, 0.7))
    , (8, choose (0.7, 0.98))
    , (1, choose (0.98, 0.999))
    ]

genReviewHistory :: Gen [(Days, Rating)]
genReviewHistory = sized $ \n -> do
  k <- choose (0, min n 30)
  vectorOf k ((,) <$> genElapsedDays <*> genRating)

genUTCTime :: Gen UTCTime
genUTCTime = do
  day <- choose (0, 3650)
  seconds <- choose (0, 86399)
  pure (UTCTime (addDays day (fromGregorian 2026 1 1)) (secondsToDiffTime seconds))

genScheduler :: Gen Scheduler
genScheduler = do
  params <- genParameters
  retention <- genDesiredRetention
  learning <- genSteps
  relearning <- genSteps
  maxIvl <- choose (1, 36500)
  minIvl <- choose (1 / 86400, maxIvl)
  pure
    Scheduler
      { schedulerParameters = params
      , schedulerDesiredRetention = retention
      , schedulerLearningSteps = learning
      , schedulerRelearningSteps = relearning
      , schedulerMinimumInterval = minIvl
      , schedulerMaximumInterval = maxIvl
      }
  where
    genSteps = do
      k <- choose (0 :: Int, 4)
      vectorOf k (fromInteger <$> choose (60, 86400))

-- | @abs (a - b) <= atol + rtol * abs b@.
approxEqual
  :: Double
  -- ^ Absolute tolerance.
  -> Double
  -- ^ Relative tolerance.
  -> Double
  -- ^ Actual.
  -> Double
  -- ^ Expected.
  -> Bool
approxEqual atol rtol actual expected =
  abs (actual - expected) <= atol + rtol * abs expected

relativeError :: Double -> Double -> Double
relativeError actual expected
  | expected == 0 = abs actual
  | otherwise = abs (actual - expected) / abs expected