Current section
Files
Jump to
Current section
Files
lib/csv.ex
defmodule CsvParser.Csv do
@moduledoc false
alias __MODULE__
require Record
Record.defrecord(:state, acc: nil, fun: nil, row: nil, headers: nil, map: nil)
defstruct [:path, :opts]
def new(path, opts) do
opts = Keyword.put_new(opts, :strip_fields, true)
{:ok, %Csv{path: path, opts: opts}}
end
def reduce(csv, acc, fun) do
s = state(acc: acc, fun: fun, map: csv.opts[:map] || false)
s = csv.path
|> File.stream!()
|> CSV.decode(csv.opts)
|> Enum.reduce(s, &handle_row/2)
state(s, :acc)
end
defp handle_row({:error, _} = err, s) do
acc = state(s, :acc)
acc = state(s, :fun).(err, acc)
state(s, acc: acc)
end
defp handle_row({:ok, row}, s) do
case fix_row(row) do
:empty -> s
ok -> handle_fixed_row(ok, s)
end
end
defp handle_fixed_row(row, s) when state(s, :map) == false do
acc = state(s, :acc)
acc = state(s, :fun).(row, acc)
state(s, acc: acc)
end
defp handle_fixed_row({:ok, row}, s) when state(s, :headers) == nil do
headers = case state(s, :map) do
true -> row
:upper -> Enum.map(row, &String.upcase/1)
:lower -> Enum.map(row, &String.downcase/1)
fun when is_function(fun) -> fun.(row)
end
state(s, headers: headers)
end
defp handle_fixed_row({:ok, row}, s) do
headers = state(s, :headers)
row = Map.new(Enum.zip(headers, row))
acc = state(s, :acc)
acc = state(s, :fun).({:ok, row}, acc)
state(s, acc: acc)
end
defp fix_row(row) do
res = Enum.reduce(row, {[], true}, fn
"", {acc, empty?} -> {[nil | acc], empty?}
value, {acc, _} -> {[value | acc], false}
end)
case res do
{rows, false} -> {:ok, Enum.reverse(rows)}
{rows, true} -> :empty
end
end
end