loading...

Daily Challenge #174 - Soccer League Table

thepracticaldev profile image dev.to staff ・2 min read

Implement the class LeagueTable.

This class is used to store results of soccer matches played, as well as return and display various stats about each team.

Match results will always be passed through the push method as a string in the format "Home Team 0 - 0 Away Team".

When a new match ends and the scores are tallied, points are awarded to teams as follows:

 3 points for a win
1 point for a draw
0 points for a loss

The class must have the following methods:

 push(match_string)             # insert a new match record

get_points(team_name)          # Returns the no. of points a team has, 0 by default

get_goals_for(team_name)       # Returns the no. of goals a team has scored, 0 by default

get_goals_against(team_name)   # Returns the no. of goals a team has conceded (had scored against them), 0 by default

get_goal_difference(team_name) # Return the no. of goals a team has scored minus the no. of goals a team has conceded, 0 by default

get_wins(team_name)            # Return the no. of wins a team has, 0 by default

get_draws(team_name)           # Return the no. of draws a team has, 0 by default

get_losses(team_name)          # Return the no. of losses a team has, 0 by default

Example

lt = LeagueTable.new

lt.push("Man Utd 3 - 0 Liverpool")

puts lt.get_goals_for("Man Utd") => 3
puts lt.get_points("Man Utd") => 3
puts lt.get_points("Liverpool") => 0
puts lt.get_goal_difference("Liverpool") => -3

lt.push("Liverpool 1 - 1 Man Utd")

puts lt.get_goals_for("Man Utd") => 4
puts lt.get_points("Man Utd") => 4
puts lt.get_points("Liverpool") => 1
puts lt.get_goals_against("Man Utd") => 1

puts lt.get_points("Tottenham") => 0

Test

lt.push("Man Utd 2 - 1 Liverpool")
lt.push("Liverpool 3 - 0 Man Utd")

Good luck!


This challenge comes from Mackay on CodeWars. Thank you to CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge idea for a future post? Email yo+challenge@dev.to with your suggestions!

Posted on by:

thepracticaldev profile

dev.to staff

@thepracticaldev

The hardworking team behind dev.to ❤️

Discussion

markdown guide
 

Well, I attempted this in Haskell. I took the liberty of making each function return a Maybe value, being Nothing when the team isn't in the table instead of defaulting to 0. It uses a state monad to make storing and "mutating" data feasible. I decided to implement my own state monad here, but there are (better) implementations on stackage.

module SoccerLeague ( Game, Name, Team, League
                    , empty, push, points
                    , goalsFor, goalsAgainst, goalDifference
                    , wins, draws, losses
                    , State, runState
                    ) where

import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Bifunctor (bimap)
import Data.Tuple (swap)
import Data.Maybe (fromMaybe)
import Data.Bool (bool)
import Text.ParserCombinators.Parsec ( GenParser, ParseError, parse, many1
                                     , digit, letter, string, space, (<|>))

type Name = String

data Team = Team { t_name :: Name
                 , t_goalsFor :: Int
                 , t_goalsAgainst :: Int
                 , t_wins :: Int
                 , t_draws :: Int
                 , t_losses :: Int
                 } deriving Show

type League = Map Name Team

type Game = ((Name, Int), (Name, Int))

updateTeam :: Name -> (Int, Int) -> State League ()
updateTeam name (for, against) = do
  map <- get
  let t = fromMaybe (Team name 0 0 0 0 0) $ map M.!? name
  let t' = Team { t_name = name
                , t_goalsFor = t_goalsFor t + for
                , t_goalsAgainst = t_goalsAgainst t + against
                , t_wins = t_wins t + bool 0 1 (for > against)
                , t_draws = t_draws t + bool 0 1 (for == against)
                , t_losses = t_losses t + bool 0 1 (for < against)
                }
  put $ M.insert name t' map

empty :: League
empty = M.empty

push :: String -> State League ()
push str = case parseGame str of
  Left e -> error $ "bad push input: " ++ str
  Right ((n1, s1), (n2, s2)) -> do
    updateTeam n1 (s1, s2)
    updateTeam n2 (s2, s1)

-- Lifts a simple function on a team in the league to work on the state
liftToState :: (Team -> a) -> String -> State League (Maybe a)
liftToState f name = get >>=
                     return . fmap f . (flip (M.!?) name)

points :: String -> State League (Maybe Int)
points = liftToState $ sum . flip fmap [(*3) . t_wins, t_draws] . flip ($)

goalsFor :: String -> State League (Maybe Int)
goalsFor = liftToState t_goalsFor

goalsAgainst :: String -> State League (Maybe Int)
goalsAgainst = liftToState t_goalsAgainst

goalDifference :: String -> State League (Maybe Int)
goalDifference = liftToState (\team -> t_goalsFor team - t_goalsAgainst team)

wins :: String -> State League (Maybe Int)
wins = liftToState t_wins

draws :: String -> State League (Maybe Int)
draws = liftToState t_draws

losses :: String -> State League (Maybe Int)
losses = liftToState t_losses

-- GAME STRING PARSER
trimSpaces :: String -> String
trimSpaces = f . f
  where f = dropWhile (==' ') . reverse

number :: GenParser Char st Int
number = many1 digit >>= return . read

name :: GenParser Char st Name
name = many1 (letter <|> space) >>= return . trimSpaces

game :: GenParser Char st Game
game = do
  fstName <- name
  fstScore <- number
  string " - "
  sndScore <- number
  sndName <- name
  return ((fstName, fstScore), (sndName, sndScore))

parseGame :: String -> Either ParseError Game
parseGame = parse game "(unknown)"

-- STATE MONAD IMPLEMENTATION
newtype State s a = State { runState :: s -> (s, a) }

instance Functor (State s) where
  fmap f (State x) = State $ bimap id f . x

instance Applicative (State s) where
  pure x = State $ \s -> (s, x)
  (State sf) <*> (State sx) = State $ \s -> let (s', f) = sf s
                                                (s'', x) = sx s
                                            in  (s'', f x)

instance Monad (State s) where
  return = pure
  sx >>= f = State $ \s -> let (s', x) = runState sx s
                           in  runState (f x) s'

get :: State s s
get = State $ \x -> (x, x)

put :: s -> State s ()
put s = State $ const (s, ())

Additionally, here's the example given in the post translated to my Haskell implementation:

import SoccerLeague
import Data.Maybe (fromMaybe)

example :: State League [Int]
example = do
  push "Man Utd 3 - 0 Liverpool"
  l1 <- sequence [goalsFor "Man Utd", points "Man Utd", points "Liverpool", goalDifference "Liverpool"]
  push "Liverpool 1 - 1 Man Utd"
  l2 <- sequence [goalsFor "Man Utd", points "Man Utd", points "Liverpool", goalsAgainst "Man Utd", points "Tottenham"]
  return $ map (fromMaybe 0) $ l1 ++ l2

main :: IO ()
main = do
  sequence_ $ map print $ snd $ runState example empty
 

Definitely some things missing and room for improvement but here we go. JavaScript.


const calcPoints = Symbol('calcPoints');

class LeagueTable {
    constructor() {
        this.matches = [];
        this.teamMapping = null;
    }

    [calcPoints](a, b) {
        return a > b ? 3 : a === b ? 1 : 0;
    }

    push(matchString) {
        let rawScore = matchString.match(/\d+\s*\-\s*\d+/g)[0];

        let teamGoals = rawScore.split('-').map(x => parseInt(x.trim()));

        let names = matchString.split(rawScore).map(x => x.trim());

        if (!this.teamMapping) {
            this.teamMapping = names.map((x, i) => ({ key: i, value: x }));
        }

        let teamOne = {
            teamId: this.teamMapping.find(x => x.value === names[0]).key,
            goals: teamGoals[0],
            gd: teamGoals[0] - teamGoals[1],
            points: this[calcPoints](teamGoals[0], teamGoals[1])
        };

        let teamTwo = {
            teamId: this.teamMapping.find(x => x.value === names[1]).key,
            goals: teamGoals[1],
            gd: teamGoals[1] - teamGoals[0],
            points: this[calcPoints](teamGoals[1], teamGoals[0])
        };

        this.matches.push(teamOne);
        this.matches.push(teamTwo);
    }

    getPoints(teamName) {
        let teamId = this.teamMapping.find(x => x.value === teamName).key;

        return this.matches
            .filter(x => x.teamId === teamId)
            .reduce((acc, v) => acc + v.points, 0);
    }

    getGoalsFor(teamName) {
        let teamId = this.teamMapping.find(x => x.value === teamName).key;

        return this.matches
            .filter(x => x.teamId === teamId)
            .reduce((acc, v) => acc + v.goals, 0);
    }

    getGoalsAgainst(teamName) {
        let teamId = this.teamMapping.find(x => x.value !== teamName).key;

        return this.matches
            .filter(x => x.teamId === teamId)
            .reduce((acc, v) => acc + v.goals, 0);
    }

    getGoalDifference(teamName) {
        let teamId = this.teamMapping.find(x => x.value !== teamName).key;

        return this.matches
            .filter(x => x.teamId === teamId)
            .reduce((acc, v) => acc + -v.gd, 0);
    }

    getWins(teamName) {
        let teamId = this.teamMapping.find(x => x.value === teamName).key;

        return this.matches
            .filter(x => x.teamId === teamId)
            .reduce((acc, v) => (v.points > 1 ? acc + 1 : acc), 0);
    }

    getDraws(teamName) {
        let teamId = this.teamMapping.find(x => x.value === teamName).key;

        return this.matches
            .filter(x => x.teamId === teamId)
            .reduce((acc, v) => (v.points === 1 ? acc + 1 : acc), 0);
    }

    getLosses(teamName) {
        let teamId = this.teamMapping.find(x => x.value === teamName).key;

        return this.matches
            .filter(x => x.teamId === teamId)
            .reduce((acc, v) => (v.points === 0 ? acc + 1 : acc), 0);
    }
}

let lt = new LeagueTable();
lt.push('Man Utd 3 - 0 Liverpool');
console.log(`Goals : ${lt.getGoalsFor('Man Utd')}`);
console.log(`Points : ${lt.getPoints('Man Utd')}`);
console.log(`Points : ${lt.getPoints('Liverpool')}`);
console.log(`Goal Difference : ${lt.getGoalDifference('Liverpool')}`);
lt.push('Liverpool 1 - 1 Man Utd');
console.log(`Goals : ${lt.getGoalsFor('Man Utd')}`);
console.log(`Points : ${lt.getPoints('Man Utd')}`);
console.log(`Points : ${lt.getPoints('Liverpool')}`);
console.log(`Goals Against : ${lt.getGoalsAgainst('Man Utd')}`);
 

Ruby, concise as usual:

class LeagueTable
    def initialize
        @results = Hash.new(Hash.new(0))
    end
    def push str
        str.match /^(.+) (\d+) - (\d+) (.+)$/
        h_name, a_name = $1, $4
        h_goal, a_goal = $2.to_i, $3.to_i
        result = h_goal <=> a_goal

        @results[h_name] = Hash.new(0) unless @results.has_key? h_name
        @results[a_name] = Hash.new(0) unless @results.has_key? a_name

        @results[h_name].tap do |stats|
            stats[:goals] += h_goal
            stats[:conceded] += a_goal
            stats[:wins] += (result + 1) / 2
            stats[:losses] += (-result + 1) / 2
            stats[:draws] += -(result.abs) + 1
        end
        @results[a_name].tap do |stats|
            stats[:goals] += a_goal
            stats[:conceded] += h_goal
            stats[:wins] += (-result + 1) / 2
            stats[:losses] += (result + 1) / 2
            stats[:draws] += -(result.abs) + 1
        end
    end
    def goals_for team
        @results[team][:goals]
    end
    def goals_against team
        @results[team][:conceded]
    end
    def goal_difference team
        @results[team][:goals] - @results[team][:conceded]
    end
    def get_wins team
        @results[team][:wins]
    end
    def get_draws team
        @results[team][:draws]
    end
    def get_losses team
        @results[team][:losses]
    end
    def get_points team
        3*@results[team][:wins] + @results[team][:draws]
    end
end
 

Javascript:

function LeagueTable() {
    this.matches = []
};

LeagueTable.prototype.push = function (matchStr){
    const teams = matchStr.split(' - ');

    const homeTeam = teams[0].slice(0,-2);
    const awayTeam = teams[1].slice(2);
    const homeGoals = parseInt(teams[0].slice(-2));
    const awayGoals = parseInt(teams[1].slice(0, 2));

    let homeScore = 0;
    let awayScore = 0;

    if(homeGoals == awayGoals) {
        homeScore = 1;
        awayScore = 1;
    } else if(homeGoals > awayGoals){
        homeScore = 3;
    } else {
        awayScore = 0;
    }

    const match = {
        homeTeam,
        awayTeam,
        homeGoals,
        awayGoals,
        homeScore,
        awayScore,
    };

    this.matches.push(match);

    return JSON.stringify(match);
};

LeagueTable.prototype.get_points = function (teamName){
    points = 0;

    this.matches.forEach(match => {
        if(match.homeTeam === teamName){
            points += match.homeScore;
        } else if(match.awayTeam === teamName){
            points += match.awayScore;
        }
    })

    return points;
};

LeagueTable.prototype.get_goals_for = function (teamName){
    goals = 0;

    this.matches.forEach(match => {
        if(match.homeTeam === teamName){
            goals += match.homeGoals;
        } else if(match.awayTeam === teamName){
            goals += match.awayGoals;
        }
    });

    return goals;
};

LeagueTable.prototype.get_goals_against = function (teamName){
    goals = 0;

    this.matches.forEach(match => {
        if(match.homeTeam === teamName){
            goals += match.awayGoals;
        } else if(match.awayTeam === teamName){
            goals += match.homeGoals;
        }
    });

    return goals;
};

LeagueTable.prototype.get_goals_difference = function (teamName){
    diff = 0;

    this.matches.forEach(match => {
        if(match.homeTeam === teamName){
            diff += (match.homeGoals - match.awayGoals);
        } else if(match.awayTeam === teamName){
            diff += (match.awayGoals - match.homeGoals);;
        }
    });

    return diff;
};

LeagueTable.prototype.get_wins = function (teamName){
    wins = 0;

    this.matches.forEach(match => {
        if(match.homeTeam === teamName && match.homeScore === 3){
            wins++
        } else if(match.awayTeam === teamName && match.awayScore === 3){
            wins++
        }
    });

    return wins;
};

LeagueTable.prototype.get_draws = function (teamName){
    draws = 0;

    this.matches.forEach(match => {
        if(match.homeTeam === teamName && match.homeScore === 1){
            draws++
        } else if(match.awayTeam === teamName && match.awayScore === 1){
            draws++
        }
    });

    return draws;
};

LeagueTable.prototype.get_losses = function (teamName){
    losses = 0;

    this.matches.forEach(match => {
        if(match.homeTeam === teamName && match.homeScore === 0){
            losses++
        } else if(match.awayTeam === teamName && match.awayScore === 0){
            losses++
        }
    });

    return losses;
};