|
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()) |
|
} |
|
} |