Created
June 16, 2013 09:27
-
-
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/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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