Skip to content

Instantly share code, notes, and snippets.

@AlbertMoscow
Created June 16, 2013 09:27
Show Gist options
  • Save AlbertMoscow/5791522 to your computer and use it in GitHub Desktop.
Save AlbertMoscow/5791522 to your computer and use it in GitHub Desktop.
Grand Champion Standings: A Short Elixir Program by J David Eisenberg Adopted from here: http://langintro.com/elixir/article1/
defmodule Standings do
defrecord Competitor, surname: "", given_name: "", team: "",
total: 0, points: []
@moduledoc """
`Standings` analyzes a CSV file of results of an athletic competition
and produces a summary of their points towards a "grand champion" award.
Each row of the source CSV file consists of a competitor's name and
team affiliation (if any), followed by his placement at each of several
tournaments. If the number is positive, that is his place at a local
tournament. If the number is negative, that his her place at a state-level
tournament.
For local tournaments, first place is worth 3 points, second place worth
2, and third place worth 1 point.
For state tournaments, first place is worth 5 points, second place worth 4,
third place worth 3, fourth place worth 2, and fifth through eight place
worth 1 point.
"""
@doc """
From a CSV file as input, generate an HTML table giving the
tandings for competitors.
"""
@doc """
Opens a CSV with the given `filename` and produces a list of
column headings and a list of competitors
sorted by total points.
"""
@spec read_csv(binary) :: {[binary],[Competitor.t]}
def read_csv(filename) do
input_file = File.open!(filename, [:read, :utf8])
IO.readline(input_file) # ignore age group
headings = String.split(chomp(IO.readline(input_file)), "\t")
{headings, process_file(input_file, [])}
end
@doc """
Process a file one row at a time; sort data structure at end of file.
"""
@spec process_file(File, [Competitor.t]) :: [Competitor.t]
def process_file(input_file, namelist) do
row = IO.readline(input_file)
if (row != :eof) do
process_file(input_file, [process_row(row) | namelist])
else
File.close(input_file)
Enum.sort(namelist, by_points(&1, &2))
end
end
@doc """
Convert placement to points, sum, and create a competitor with
appropriate data.
"""
@spec process_row(binary) :: Competitor.t
def process_row(row) do
[first, last, team | placing] = chomp(row) |> String.split("\t")
{points, sum} = Enum.map_reduce(placing, 0, place_points(&1, &2))
Competitor.new(surname: last, given_name: first, team: team,
total: sum, points: points)
end
@doc """
Convert a place (1st through 8th) to points.
If an entry in the CSV file is the empty string, count it as a zero.
Otherwise, convert to integer. If positive, take 4 - value; if negative,
take 6 - value.
"""
@spec place_points(binary, integer) :: {term, integer}
def place_points(item, accumulator) when item == "" do
{0, accumulator}
end
def place_points(item, accumulator) do
value = binary_to_integer(item)
if value < 0 do
n = max(1, 6 + value) # state tournament
{n, accumulator + n}
else
n = max(1, 4 - value) # local tournament
{n, accumulator + n}
end
end
@doc """
Sort competitors by total points. If those are equal, sort by
last name, first name, and team until you can make a decision.
"""
@spec by_points(Competitor.t, Competitor.t) :: boolean
def by_points(a, b) do
if a.total == b.total do
if a.surname == b.surname do
if a.given_name == b.given_name do
a.team < b.team
else
a.given_name < b.given_name
end
else
a.surname < b.surname
end
else
a.total > b.total
end
end
@doc """
Remove trailing newline character(s) from the end of a string.
Recognizes `\n`, `\r`, or `\r\n` as newlines.
"""
@spec chomp(binary) :: binary
def chomp(str) do
Regex.replace(%r/\r?\n\z|\r\z/, str, "", [{:global, false}])
end
@spec html_output(binary) :: :ok
def html_output(input_filename) do
ends_with_csv = %r/\.csv\z/
if input_filename =~ ends_with_csv do
output_filename = Regex.replace(ends_with_csv, input_filename, ".html", [])
else
output_filename = input_filename <> ".html"
end
output_file = File.open!(output_filename, [:write, :utf8])
{headings, data} = read_csv(input_filename)
IO.puts output_file, """
<html>
<head>
<title>#{output_filename}</title>
</head>
<body>
<table border="1">
<thead>
#{make_header_row(headings)}
</thead>
<tbody>
"""
emit_html_rows(output_file, data)
IO.puts output_file, """
</tbody>
</table>
</body>
</html>
"""
File.close(output_file)
end
@spec make_header_row(List) :: binary
defp make_header_row([first, last, team | points]) do
"<tr><td>" <>
Enum.join([first, last, team, "Total" | points], "</td><td>") <>
"</td></tr>"
end
@spec emit_html_rows(File.t, [Competitor.t]) :: :ok
defp emit_html_rows(_output_file, []) do
:ok
end
defp emit_html_rows(output_file, [person | remainder]) do
Competitor[given_name: first, surname: last, team: team,
total: total, points: points] = person
str = "<tr><td>" <>
Enum.join([first, last, team], "</td><td>") <>
"</td><td>" <>
Enum.join(Enum.map([total | points], html_cell(&1)), "</td><td>") <>
"</td></tr>"
IO.puts(output_file, str)
emit_html_rows(output_file, remainder)
end
@spec html_cell(integer) :: binary
defp html_cell(item) do
if (item == 0) do
"<br />"
else
to_binary(item)
end
end
end
ExUnit.start
defmodule StandingsTest do
use ExUnit.Case
test "Testing target function" do
assert :ok == Standings.html_output "open_standings.csv"
end
end
We can make this file beautiful and searchable if this error is corrected: No commas found in this CSV file in line 0.
Open
First Last Team Hollister Santa Cruz Oak Grove Open State Watsonville James Lick Silver Creek
Mark Arnhelm Knights 1 1
William Alvarez Woodside 1 3 1 1
Ross Carter Alliance 1 2 2 3 2 1
Jasper Lawrence Chimera -3 1
Chris Nguyen Monterey -3
Jason Orozco DSC 1 1
Shohei Takamura Athlete Nation 2 1 2 2 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment