Skip to content

Instantly share code, notes, and snippets.

@tyndyll
Created April 23, 2019 17:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tyndyll/fc7d8e9f23123259790c388e9c31bf4f to your computer and use it in GitHub Desktop.
Save tyndyll/fc7d8e9f23123259790c388e9c31bf4f to your computer and use it in GitHub Desktop.
English Premier League Table Viewer

English Premier League Table Viewer

Abstract

This is a simple implementation of a problem presented in a conversation that arose about hiring questions that I was nerdsniped on. This is just a basic textual output instead of the requested HTML output, but that could have been implemented using html/template instead of text/template.

Problem Definition

The English Premier League is the top-tier league of soccer (football) teams, which consists of 20 teams. Every year, each team in the league play 38 matches (2 matches against every other team).

At the end of the season, the team with the most points wins the Premier League title. The bottom three teams are relegated to the Championship League, which is the league one tier below the Premier League. The top 4 teams are eligible to play in the UEFA Champions League the next season, and teams ranked 5 and 6 are eligible to play in the Europa League.

Each win is worth 3 points, each tie is worth 1 point, and a loss does not give a team any points.

We would like you to create a function which generates the Premier League table (ie: the standings).

Given the full list of matches in the season from https://github.com/openfootball/football.json/blob/master/2016-17/en.1.json your script must create a sorted table containing the following information for each team:

rank (1, 2, 3 …. 20) team name wins draws losses goals for goals against goal difference points

The table must be sorted by rank. The rank is determined by points, then goal difference, then goals scored.

The resulting table should be displayed within a Grid Control in a web page. If possible, the grid control should let you sort each of the columns. If the grid control supports it, highlight the UEFA-eligible teams with a light green background the Europa-eligible teams in a light yellow background, and the relegated teams in a light red background.

The data should be stored in a relational database on your laptop. The design of the schema is up to you. The schema should be flexible enough to store the results of matches for future seasons, and perhaps even flexible enough to add new leagues, such as La Liga, Bundesliga, etc. (You may even want to expand it to include information about who scored goals in the matches, but that is not required for this project.)

If you find yourself with extra time at the end of the test, create a RESTful API for your server. It should support fetching the complete table, or fetching the data for a certain team. If you have information about the goal scorers, you may want to add REST APIs so that someone could find out who the top goal scorers are.

Implementation Notes

There are a number of shortcomings in this implementation

  • URL of data is hardcoded. This could have been passed as an environmental variable or command line arg
  • There is no database involved. This was just a proof of concept so the database would have been overkill
  • Points and GoalDifference fields could be implemented in the get_standings method rather than methods.
  • JSON decode is not limited e.g. as a buffered reader. This is for the sake of simplicity.

Running

Clone this repository. Assuming that you have the Go SDK installed it should be a simple matter of running

go run main.go
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"sort"
"text/template"
)
const (
// table_format is the simple text template we'll use to render the table. It could be shortened and tidied up
// with blocks etc
table_format = `
| {{ printf "%20s" "Name" }} | {{ printf "%5s" "Win" }} | {{ printf "%5s" "Draw" }} | {{ printf "%5s" "Loss" }} | {{ printf "%5s" "Goal+" }} | {{ printf "%5s" "Goal-" }} | {{ printf "%5s" "Diff" }} | {{ printf "%5s" "Points" }} |
{{ range . }}| {{ printf "%20s" .Team.Name }} | {{ printf "%5d" .Wins }} | {{ printf "%5d" .Draws }} | {{ printf "%5d" .Losses }} | {{ printf "%5d" .GoalsFor }} | {{ printf "%5d" .GoalsAgainst }} | {{ printf "%5d" .GoalDifference }} | {{ printf "%6d" .Points }} |
{{ end }}`
// win_points is the number of points allocated for a win
win_points = 3
// draw_points is the number of points allocated for a win
draw_points = 1
remoteURL = "https://raw.githubusercontent.com/openfootball/football.json/master/2016-17/en.1.json"
)
// ResultMap holds the parsing from JSON file we will receive.
type ResultMap struct {
// Name of the result set
Name string
// List of rounds of matches
Rounds []*LeagueRound
}
// LeagueRound holds a round of league matches
type LeagueRound struct {
// Name of the round
Name string
// List of the matches played
Matches []*Match
}
// Match contains the match result.
type Match struct {
// Team1 is the home team
Team1 *Team
// Team2 is the away team
Team2 *Team
// Score1 is the number of goals scored by the home team
Score1 int
// Score2 is the number of goals scored by the away team
Score2 int
}
// Team contains the team details
type Team struct {
// Slug of the name
Key string
// Textual representation of the team name
Name string
// Three letter code identifying the team
Code string
}
// Standing is the results for a team in a league
type Standing struct {
Team *Team
GoalsFor int
GoalsAgainst int
Wins int
Losses int
Draws int
}
// GoalDifference calculates the difference in the number of goals scored by a team versus the nunber of goals scored
// against. This could easily be a field in the Standing struct, and probably should be as the calculation ends up
// being performed many times, but for the sake of demonstrating methods on structs it's a nice example
func (s *Standing) GoalDifference() int {
return s.GoalsFor - s.GoalsAgainst
}
// Points calculates the number of points a team has based on the number of wins and losses. Like GoalDifference, this
// could be a field in the struct
func (s *Standing) Points() int {
return s.Wins*win_points + s.Draws*draw_points
}
// StandingList is a container for the league standings. It implements the Sort interface for the purposes of
// calculating league positions. It is an alias of a list of Standing types. We create this as a type so we can
// implement the Sort interface (https://golang.org/pkg/sort/#Interface)
type StandingList []*Standing
// Len returns the length of the StandingList. It is a requirement of the Sort interface
func (list StandingList) Len() int {
return len(list)
}
// Less takes two indexes and compares the values at those indexes, returning true if the item at the first index is
// less than the value of the second index. It is a requirement of the Sort interface
//
// In this method we implement the logic required by the problem, where two indexes have the same points value, and
// therefore goal difference and number of goals scored are taken into consideration.
func (list StandingList) Less(i, j int) bool {
// Are the number of points the same?
if list[i].Points() == list[j].Points() {
// Points are the same. Check if the goal difference is the same
if list[i].GoalDifference() == list[j].GoalDifference() {
// Goal difference is the same. Use the number of goals scored to determine position
return list[i].GoalsFor < list[j].GoalsFor
}
// Goal difference is different. Peform the comparison
return list[i].GoalDifference() < list[j].GoalDifference()
}
// Perform the points comparison
return list[i].Points() < list[j].Points()
}
// Swap switches list values in place
func (list StandingList) Swap(i, j int) {
list[i], list[j] = list[j], list[i]
}
// getData fetches JSON data from the remote URL and decodes it into a ResultMap. Returns the result or any error that
// is raised
func getData(remoteURL string) (*ResultMap, error) {
response, err := http.Get(remoteURL)
if err != nil {
return nil, err
}
results := &ResultMap{}
decoder := json.NewDecoder(response.Body)
if err := decoder.Decode(results); err != nil {
return nil, err
}
return results, nil
}
// getStandings takes the result map and generates the list of Standings.
func getStandings(data *ResultMap) *StandingList {
standings := map[string]*Standing{}
standingList := make(StandingList, 0)
// getStanding is a simple closure function that determines if a team exists in the map. If it does it is returned.
// If it is not then the team is created and then returned
getStanding := func(team *Team) *Standing {
standing, ok := standings[team.Code]
if !ok {
standings[team.Code] = &Standing{Team: team}
standing = standings[team.Code]
standingList = append(standingList, standing)
}
return standing
}
for _, round := range data.Rounds {
for _, match := range round.Matches {
team1 := getStanding(match.Team1)
team2 := getStanding(match.Team2)
if match.Score1 > match.Score2 {
team1.Wins++
team2.Losses++
} else if match.Score1 < match.Score2 {
team2.Wins++
team1.Losses++
} else {
team1.Draws++
team2.Draws++
}
team1.GoalsFor += match.Score1
team1.GoalsAgainst += match.Score2
team2.GoalsFor += match.Score2
team2.GoalsAgainst += match.Score1
}
}
return &standingList
}
// PrintTable fetches data from the remote URL and prints the table format. This function is public
func PrintTable(remoteURL string) error {
data, err := getData(remoteURL)
if err != nil {
return err
}
standings := getStandings(data)
// This Sort function takes advantage of the Sort interface we implemented on the StandingList type.
sort.Sort(sort.Reverse(standings))
tmpl, err := template.New("table").Parse(table_format)
if err != nil {
return err
}
err = tmpl.Execute(os.Stdout, *standings)
if err != nil {
return err
}
return nil
}
func main() {
if err := PrintTable(remoteURL); err != nil {
log.Fatalf("Could not print table: %s \n", err.Error())
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment