Avatar of exklamationmark

exklamationmark's solution

to Tournament in the Go Track

Published at Aug 24 2018 · 0 comments
Instructions
Test suite
Solution

Tally the results of a small football competition.

Based on an input file containing which team played against which and what the outcome was, create a file with a table like this:

Team                           | MP |  W |  D |  L |  P
Devastating Donkeys            |  3 |  2 |  1 |  0 |  7
Allegoric Alaskans             |  3 |  2 |  0 |  1 |  6
Blithering Badgers             |  3 |  1 |  0 |  2 |  3
Courageous Californians        |  3 |  0 |  1 |  2 |  1

What do those abbreviations mean?

  • MP: Matches Played
  • W: Matches Won
  • D: Matches Drawn (Tied)
  • L: Matches Lost
  • P: Points

A win earns a team 3 points. A draw earns 1. A loss earns 0.

The outcome should be ordered by points, descending. In case of a tie, teams are ordered alphabetically.

Input

Your tallying program will receive input that looks like:

Allegoric Alaskans;Blithering Badgers;win
Devastating Donkeys;Courageous Californians;draw
Devastating Donkeys;Allegoric Alaskans;win
Courageous Californians;Blithering Badgers;loss
Blithering Badgers;Devastating Donkeys;loss
Allegoric Alaskans;Courageous Californians;win

The result of the match refers to the first team listed. So this line

Allegoric Alaskans;Blithering Badgers;win

Means that the Allegoric Alaskans beat the Blithering Badgers.

This line:

Courageous Californians;Blithering Badgers;loss

Means that the Blithering Badgers beat the Courageous Californians.

And this line:

Devastating Donkeys;Courageous Californians;draw

Means that the Devastating Donkeys and Courageous Californians tied.

Running the tests

To run the tests run the command go test from within the exercise directory.

If the test suite contains benchmarks, you can run these with the --bench and --benchmem flags:

go test -v --bench . --benchmem

Keep in mind that each reviewer will run benchmarks on a different machine, with different specs, so the results from these benchmark tests may vary.

Further information

For more detailed information about the Go track, including how to get help if you're having trouble, please visit the exercism.io Go language page.

Submitting Incomplete Solutions

It's possible to submit an incomplete solution so you can see how others have completed the exercise.

tournament_test.go

package tournament

import (
	"bytes"
	"io"
	"strings"
	"testing"
)

// Define a function Tally(io.Reader, io.Writer) error.
//
// Note that unlike other tracks the Go version of the tally function
// should not ignore errors. It's not idiomatic Go to ignore errors.

var _ func(io.Reader, io.Writer) error = Tally

// These test what testers call the happy path, where there's no error.
var happyTestCases = []struct {
	description string
	input       string
	expected    string
}{
	{
		description: "good",
		input: `
Allegoric Alaskians;Blithering Badgers;win
Devastating Donkeys;Courageous Californians;draw
Devastating Donkeys;Allegoric Alaskians;win
Courageous Californians;Blithering Badgers;loss
Blithering Badgers;Devastating Donkeys;loss
Allegoric Alaskians;Courageous Californians;win
`,
		expected: `
Team                           | MP |  W |  D |  L |  P
Devastating Donkeys            |  3 |  2 |  1 |  0 |  7
Allegoric Alaskians            |  3 |  2 |  0 |  1 |  6
Blithering Badgers             |  3 |  1 |  0 |  2 |  3
Courageous Californians        |  3 |  0 |  1 |  2 |  1
`[1:], // [1:] = strip initial readability newline
	},
	{
		description: "ignore comments and newlines",
		input: `

Allegoric Alaskians;Blithering Badgers;win
Devastating Donkeys;Allegoric Alaskians;win
# Catastrophic Loss of the Californians
Courageous Californians;Blithering Badgers;loss

Blithering Badgers;Devastating Donkeys;loss
Allegoric Alaskians;Courageous Californians;win
Devastating Donkeys;Courageous Californians;draw


`,
		expected: `
Team                           | MP |  W |  D |  L |  P
Devastating Donkeys            |  3 |  2 |  1 |  0 |  7
Allegoric Alaskians            |  3 |  2 |  0 |  1 |  6
Blithering Badgers             |  3 |  1 |  0 |  2 |  3
Courageous Californians        |  3 |  0 |  1 |  2 |  1
`[1:],
	},
	{
		// A complete competition has all teams play eachother once or twice.
		description: "incomplete competition",
		input: `
Allegoric Alaskians;Blithering Badgers;win
Devastating Donkeys;Allegoric Alaskians;win
Courageous Californians;Blithering Badgers;loss
Allegoric Alaskians;Courageous Californians;win
`,
		expected: `
Team                           | MP |  W |  D |  L |  P
Allegoric Alaskians            |  3 |  2 |  0 |  1 |  6
Blithering Badgers             |  2 |  1 |  0 |  1 |  3
Devastating Donkeys            |  1 |  1 |  0 |  0 |  3
Courageous Californians        |  2 |  0 |  0 |  2 |  0
`[1:],
	},
	{
		description: "tie for first and last place",
		input: `
Courageous Californians;Devastating Donkeys;win
Allegoric Alaskians;Blithering Badgers;win
Devastating Donkeys;Allegoric Alaskians;loss
Courageous Californians;Blithering Badgers;win
Blithering Badgers;Devastating Donkeys;draw
Allegoric Alaskians;Courageous Californians;draw
`,
		expected: `
Team                           | MP |  W |  D |  L |  P
Allegoric Alaskians            |  3 |  2 |  1 |  0 |  7
Courageous Californians        |  3 |  2 |  1 |  0 |  7
Blithering Badgers             |  3 |  0 |  1 |  2 |  1
Devastating Donkeys            |  3 |  0 |  1 |  2 |  1
`[1:],
	},
}

var errorTestCases = []string{
	"Bla;Bla;Bla",
	"Devastating Donkeys_Courageous Californians;draw",
	"Devastating Donkeys@Courageous Californians;draw",
	"Devastating Donkeys;Allegoric Alaskians;dra",
}

func TestTallyHappy(t *testing.T) {
	for _, tt := range happyTestCases {
		reader := strings.NewReader(tt.input)
		var buffer bytes.Buffer
		err := Tally(reader, &buffer)
		actual := buffer.String()
		// We don't expect errors for any of the test cases
		if err != nil {
			t.Fatalf("Tally for input named %q returned error %q. Error not expected.",
				tt.description, err)
		}
		if actual != tt.expected {
			t.Fatalf("Tally for input named %q was expected to return...\n%s\n...but returned...\n%s",
				tt.description, tt.expected, actual)
		}
	}
}

func TestTallyError(t *testing.T) {
	for _, s := range errorTestCases {
		reader := strings.NewReader(s)
		var buffer bytes.Buffer
		err := Tally(reader, &buffer)
		if err == nil {
			t.Fatalf("Tally for input %q should have failed but didn't.", s)
		}
		var _ error = err
	}
}

func BenchmarkTally(b *testing.B) {
	for i := 0; i < b.N; i++ {
		for _, tt := range happyTestCases {
			var buffer bytes.Buffer
			Tally(strings.NewReader(tt.input), &buffer)
		}
		for _, s := range errorTestCases {
			var buffer bytes.Buffer
			Tally(strings.NewReader(s), &buffer)
		}
	}
}
package tournament

import (
	"bufio"
	"fmt"
	"io"
	"sort"
	"strings"
)

// teamStats stores the stats of each team.
type teamStats struct {
	played, won, drawn, lost, points int
}

// Tally parses the score file && prints the aggregated result.
func Tally(rr io.Reader, ww io.Writer) error {
	stats := make(map[string]*teamStats)

	r := bufio.NewReader(rr)
	for {
		l, _, err := r.ReadLine()
		if err == io.EOF {
			break
		}
		if err != nil {
			return fmt.Errorf("cannot read line; err= %v", err)
		}
		if isEmpty := len(l) < 1; isEmpty {
			continue
		}
		if isComment := l[0] == '#'; isComment {
			continue
		}

		if err := updateStatsWithResult(stats, string(l)); err != nil {
			return err
		}
	}

	teams := byDescPointAndAscName(stats)

	header := "Team                           | MP |  W |  D |  L |  P\n"
	tmpl := "%-30s | %2d | %2d | %2d | %2d | %2d\n"
	w := bufio.NewWriter(ww)
	w.WriteString(header)
	for _, t := range teams {
		w.WriteString(fmt.Sprintf(tmpl, t, stats[t].played, stats[t].won, stats[t].drawn, stats[t].lost, stats[t].points))
	}
	w.Flush()

	return nil
}

// updateStatsWithResult parses a score line and update the stats map.
func updateStatsWithResult(stats map[string]*teamStats, line string) error {
	parts := strings.FieldsFunc(line, func(rn rune) bool {
		return rn == ';'
	})
	if len(parts) != 3 {
		return fmt.Errorf("line %q does not have 3 fields", line)
	}

	team0, team1, res := parts[0], parts[1], parts[2]
	if _, exist := stats[team0]; !exist {
		stats[team0] = &teamStats{}
	}
	if _, exist := stats[team1]; !exist {
		stats[team1] = &teamStats{}
	}
	switch res {
	default:
		return fmt.Errorf("%q is not a valid result", res)
	case "win":
		stats[team0].won++
		stats[team1].lost++
		stats[team0].points += 3
	case "draw":
		stats[team0].drawn++
		stats[team1].drawn++
		stats[team0].points += 1
		stats[team1].points += 1
	case "loss":
		stats[team0].lost++
		stats[team1].won++
		stats[team1].points += 3
	}
	stats[team0].played++
	stats[team1].played++

	return nil
}

// byDescPointAndAscName returns the list of teams, sorted by descending points && ascending name.
func byDescPointAndAscName(stats map[string]*teamStats) []string {
	teams := make([]string, 0, len(stats))
	for k, _ := range stats {
		teams = append(teams, k)
	}

	sort.Slice(teams, func(i, j int) bool {
		switch {
		default:
			return false
		case stats[teams[i]].points > stats[teams[j]].points:
			return true
		case stats[teams[i]].points == stats[teams[j]].points && teams[i] < teams[j]:
			return true
		}

	})

	return teams
}

Community comments

Find this solution interesting? Ask the author a question to learn more.

What can you learn from this solution?

A huge amount can be learned from reading other people’s code. This is why we wanted to give exercism users the option of making their solutions public.

Here are some questions to help you reflect on this solution and learn the most from it.

  • What compromises have been made?
  • Are there new concepts here that you could read more about to improve your understanding?