packages feed

codeforces-cli-0.1.0: src/Codeforces/API.hs

--------------------------------------------------------------------------------

module Codeforces.API
    ( module Codeforces.Types
    , ResponseError(..)
    , handleAPI

    -- * Contests
    , getContests
    , getContestStandings
    , getContestStandings'
    , StandingsParams(..)

    -- * Problems
    , getAllProblemData
    , getProblems
    , getProblemStats
    , getContestProblems

    -- * Ratings and ranks
    , getContestRatingChanges
    , getUserRatingHistory

    -- * Problem submissions
    , getContestSubmissions
    , getUserStatus

    -- * User details
    , getUser
    , getUsers
    , getFriends

    -- * Virtual rating calculation
    , calculateVirtualResult
    , Delta
    , Seed
    , VirtualResult(..)

    -- * Configuration options
    , UserConfig(..)
    ) where

import           Codeforces.Config
import           Codeforces.Error
import           Codeforces.Response
import           Codeforces.Types
import           Codeforces.Virtual

import           Control.Arrow                  ( left )
import           Control.Monad.Trans.Except

import qualified Data.ByteString.Char8         as BC
import qualified Data.Map                      as M
import qualified Data.Text                     as T
import qualified Data.Text.Encoding            as T

--------------------------------------------------------------------------------

handleAPI :: IO (Either ResponseError a) -> ExceptT CodeforcesError IO a
handleAPI m = ExceptT $ left ResponseError <$> m

--------------------------------------------------------------------------------

-- | Query parameters for retrieving contest standings.
data StandingsParams = StandingsParams
    {
    -- | ID of the contest
      paramContestId  :: ContestId
    -- | The starting index of the ranklist (1-based)
    , paramFrom       :: Maybe Int
    -- | The number of standing rows to return
    , paramRowCount   :: Maybe Int
    -- | If specified, only standings of this room are returned
    , paramRoom       :: Maybe Int
    -- | If true, all participations are included. Otherwise only 'Contestant'
    -- participations are included.
    , paramUnofficial :: Bool
    -- | If specified, the standings includes only these users.
    , paramHandles    :: Maybe [Handle]
    }
    deriving Show

--------------------------------------------------------------------------------

-- | 'getContests' @isGym@ returns a list of contests that may or may not be gym
-- contests.
getContests :: Bool -> IO (Either ResponseError [Contest])
getContests isGym = getData "/contest.list" [("gym", argBool isGym)]

-- | 'getContestStandings' @standingsParams@ returns information about the
-- contest and a part of the standings list.
getContestStandings :: StandingsParams -> IO (Either ResponseError Standings)
getContestStandings StandingsParams {..} = getData
    "/contest.standings"
    [ ("contestId"     , argContestId paramContestId)
    , ("from"          , argInt =<< paramFrom)
    , ("count"         , argInt =<< paramRowCount)
    , ("room"          , argInt =<< paramRoom)
    , ("showUnofficial", argBool paramUnofficial)
    , ("handles"       , argHandles =<< paramHandles)
    ]

-- | Like 'getContestStandings' but returns the standings and the
-- 'RatingChange's for each user participating.
getContestStandings'
    :: StandingsParams
    -> IO (Either ResponseError (Standings, M.Map Handle RatingChange))
getContestStandings' params = runExceptT $ do
    ss  <- ExceptT $ getContestStandings params
    rcs <- ExceptT $ getContestRatingChanges (paramContestId params)

    let rcsMap = M.fromList $ map (rcHandle >>= (,)) rcs

    pure (ss, rcsMap)

-- | 'getContestSubmissions' @contestId handle@ returns the submissions made by
-- the user in the contest given by @contestId@
getContestSubmissions
    :: ContestId -> Handle -> IO (Either ResponseError [Submission])
getContestSubmissions cId h = getData
    "/contest.status"
    [("contestId", argContestId cId), ("handle", argHandle h)]

--------------------------------------------------------------------------------

-- | 'getAllProblemData' @tags@ returns a 'ProblemsResponse' filtered by the
-- @tags@, if supplied.
getAllProblemData :: [ProblemTag] -> IO (Either ResponseError ProblemsResponse)
getAllProblemData ts = getData "/problemset.problems" [("tags", argTags ts)]

-- | 'getProblems' @tags@ returns a list of 'Problem's containing the @tags@, if
-- provided.
getProblems :: [ProblemTag] -> IO (Either ResponseError [Problem])
getProblems ts = fmap prProblems <$> getAllProblemData ts

-- | Like 'getProblems' but returns a list of 'ProblemStats'.
getProblemStats :: [ProblemTag] -> IO (Either ResponseError [ProblemStats])
getProblemStats ts = fmap prStats <$> getAllProblemData ts

-- | 'getContestProblems' @contestId@ returns the list of problems for the given
-- contest.
--
-- This should be used instead of filtering results from 'getProblems' for two
-- main reasons:
--
--     (1) 'problemContestId' can only refer to one contest, so problems
--         appearing in multiple contests may not be filtered correctly.
--     (2) 'getProblems' returns larger output potentially affecting performance
--
getContestProblems :: ContestId -> IO (Either ResponseError [Problem])
getContestProblems cId = fmap standingsProblems <$> getContestStandings
    StandingsParams { paramContestId  = cId
                    , paramFrom       = Just 1
                    , paramRowCount   = Just 1
                    , paramRoom       = Nothing
                    , paramUnofficial = False
                    , paramHandles    = Nothing
                    }

--------------------------------------------------------------------------------

-- | 'getContestRatingChanges' @contestId@ returns a list of 'RatingChange's
-- for the contest.
getContestRatingChanges
    :: ContestId -> IO (Either ResponseError [RatingChange])
getContestRatingChanges cId =
    getData "/contest.ratingChanges" [("contestId", argContestId cId)]

-- | 'getUserRatingHistory' @handle@ returns a list of 'RatingChange's for the
-- requested user
getUserRatingHistory :: Handle -> IO (Either ResponseError [RatingChange])
getUserRatingHistory h = getData "/user.rating" [("handle", argHandle h)]

--------------------------------------------------------------------------------

-- | 'getUser' @handle@ returns the 'User' with the given @handle@
getUser :: Handle -> IO (Either ResponseError User)
getUser h = fmap head <$> getUsers [h]

-- | 'getUsers' @handles@ returns a list of 'User's with the given @handles@
getUsers :: [Handle] -> IO (Either ResponseError [User])
getUsers [] = pure $ Right []
getUsers hs = getData "/user.info" [("handles", argHandles hs)]

-- 'getFriends' @config@ returns the handles of the friends of the currently
-- authenticated user.
getFriends :: UserConfig -> IO (Either ResponseError [Handle])
getFriends cfg = getAuthorizedData cfg "/user.friends" []

-- | 'getUserStatus' @handle from count@ returns the @count@ most recent
-- submissions by the user, starting from the @from@-th one.
getUserStatus :: Handle -> Int -> Int -> IO (Either ResponseError [Submission])
getUserStatus h f n = getData
    "/user.status"
    [("handle", argHandle h), ("from", argInt f), ("count", argInt n)]

--------------------------------------------------------------------------------

-- | 'calculateVirtualResult' @contestId handle points penalty@ computes the
-- rating change the user would gain had they competed in the contest live, and
-- their expected ranking for the contest.
--
calculateVirtualResult
    :: ContestId
    -> Handle
    -> Points
    -> Int
    -> IO (Either ResponseError (User, Maybe VirtualResult))
calculateVirtualResult cId handle points penalty = runExceptT $ do
    rcs       <- ExceptT $ getContestRatingChanges cId

    standings <- ExceptT $ getContestStandings $ StandingsParams
        { paramContestId  = cId
        , paramFrom       = Nothing
        , paramRowCount   = Nothing
        , paramRoom       = Nothing
        , paramUnofficial = False
        , paramHandles    = Nothing
        }

    user <- ExceptT $ getUser handle
    let vUser = VirtualUser { vuPoints  = points
                            , vuPenalty = penalty
                            , vuRating  = userRating user
                            }
        result = calculateResult vUser rcs (standingsRanklist standings)

    pure (user, result)

--------------------------------------------------------------------------------

argBool :: Bool -> Maybe BC.ByteString
argBool = Just . BC.pack . show

argText :: T.Text -> Maybe BC.ByteString
argText = Just . T.encodeUtf8

argTexts :: [T.Text] -> Maybe BC.ByteString
argTexts xs | null xs   = Nothing
            | otherwise = (Just . T.encodeUtf8 . T.intercalate ";") xs

argInt :: Int -> Maybe BC.ByteString
argInt = Just . BC.pack . show

argContestId :: ContestId -> Maybe BC.ByteString
argContestId = argInt . unContestId

argHandle :: Handle -> Maybe BC.ByteString
argHandle = argText . unHandle

argHandles :: [Handle] -> Maybe BC.ByteString
argHandles = argTexts . map unHandle

argTags :: [ProblemTag] -> Maybe BC.ByteString
argTags = argTexts

--------------------------------------------------------------------------------