packages feed

gotta-go-fast-0.1.2.0: src/GottaGoFast.hs

module GottaGoFast
  ( Character(..)
  , Line
  , Page
  , State
  , accuracy
  , applyBackspace
  , applyBackspaceWord
  , applyChar
  , applyEnter
  , applyTab
  , atEndOfLine
  , cursor
  , hasEnded
  , hasStarted
  , initialState
  , isComplete
  , isErrorFree
  , noOfChars
  , onLastLine
  , page
  , seconds
  , startClock
  , stopClock
  , wpm
  ) where

import Data.List (isPrefixOf)
import Data.Maybe (isJust, fromJust)
import Data.Time (UTCTime, diffUTCTime)

-- It is often useful to know whether the line / character etc we are
-- considering is "BeforeCursor" or "AfterCursor". More granularity turns out
-- to be unnecessary.
data Position = BeforeCursor | AfterCursor

data State = State
  { target :: String
  , input :: String
  , start :: Maybe UTCTime
  , end :: Maybe UTCTime
  , strokes :: Integer
  , hits :: Integer
  }

-- For ease of rendering a character in the UI, we tag it as a Hit, Miss, or
-- Empty. Corresponding to the cases of being correctly typed, incorrectly
-- typed (or skipped), or not yet typed.
data Character = Hit Char | Miss Char | Empty Char
type Line = [Character]
type Page = [Line]

startClock :: UTCTime -> State -> State
startClock now s = s { start = Just now }

stopClock :: UTCTime -> State -> State
stopClock now s = s { end = Just now }

hasStarted :: State -> Bool
hasStarted = isJust . start

hasEnded :: State -> Bool
hasEnded = isJust . end

cursorCol :: State -> Int
cursorCol = length . takeWhile (/= '\n') . reverse . input

cursorRow :: State -> Int
cursorRow = length . filter (== '\n') . input

cursor :: State -> (Int, Int)
cursor s = (cursorCol s, cursorRow s)

atEndOfLine :: State -> Bool
atEndOfLine s = cursorCol s == length (lines (target s) !! cursorRow s)

onLastLine :: State -> Bool
onLastLine s = cursorRow s + 1 == length (lines $ target s)

isComplete :: State -> Bool
isComplete s = input s == target s

isErrorFree :: State -> Bool
isErrorFree s = input s `isPrefixOf` target s

applyChar :: Char -> State -> State
applyChar c s = s' { hits = hits s' + if isErrorFree s' then 1 else 0 }
  where
    s' = s { input = input s ++ [c] , strokes = strokes s + 1 }

applyEnter :: State -> State
applyEnter = applyTab . applyChar '\n'

backspace :: String -> String
backspace "" = ""
backspace xs = init xs

applyBackspace :: State -> State
applyBackspace s = s { input = backspace $ input s }

backspaceWord :: String -> String
backspaceWord xs = reverse $ dropWhile (/= ' ') $ reverse $ backspace xs

applyBackspaceWord :: State -> State
applyBackspaceWord s = s { input = backspaceWord $ input s }

applyTab :: State -> State
applyTab s = s { input = input s ++ drop (cursorCol s) leadingSpaces }
  where
    leadingSpaces = takeWhile (== ' ') $ lines (target s) !! cursorRow s

initialState :: String -> State
initialState t = applyTab State
  { target = t
  , input = ""
  , start = Nothing
  , end = Nothing
  , strokes = 0
  , hits = 0
  }

character :: Position -> (Maybe Char, Maybe Char) -> Character
character _ (Just t, Just i)
  | t == i = Hit t
  | t /= i = Miss i
character _ (Nothing, Just i) = Miss i
character BeforeCursor (Just t, Nothing) = Miss t
character AfterCursor (Just t, Nothing) = Empty t

line :: Position -> (String, String) -> Line
line _ ("", "") = []
line p (ts, is) = map (character p) charPairs
  where
    charPairs = take maxLen $ zip (nothingsForever ts) (nothingsForever is)
    nothingsForever x = map Just x ++ repeat Nothing
    maxLen = max (length ts) (length is)

page :: State -> Page
page s = linesBeforeCursor ++ linesAfterCursor
  where
    linesBeforeCursor = map (line BeforeCursor) $ take (cursorRow s) linePairs
    linesAfterCursor = map (line AfterCursor) $ drop (cursorRow s) linePairs
    linePairs = zip (lines $ target s) (lines (input s) ++ repeat "")

noOfChars :: State -> Int
noOfChars = length . input

-- The following functions are only safe to use when both hasStarted and
-- hasEnded hold.

seconds :: State -> Rational
seconds s = toRational $ diffUTCTime (fromJust $ end s) (fromJust $ start s)

wpm :: State -> Rational
wpm s = fromIntegral (length $ target s) / (5 * seconds s / 60)

accuracy :: State -> Rational
accuracy s = fromIntegral (hits s) / fromIntegral (strokes s)