1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
defmodule Cepis.CSV do
@moduledoc false
@spec parse_with_header(binary()) :: [%{optional(String.t()) => String.t()}]
def parse_with_header(text) do
case parse(text) do
[] -> []
[header | rows] -> Enum.map(rows, &zip(header, &1))
end
end
@spec parse(binary()) :: [[String.t()]]
def parse(text) do
text
|> strip_bom()
|> outside([], [], [])
|> Enum.reject(&(&1 == [""]))
end
defp zip(header, row) do
values = List.to_tuple(row)
width = tuple_size(values)
header
|> Enum.with_index()
|> Map.new(fn {key, index} ->
{key, if(index < width, do: elem(values, index), else: "")}
end)
end
defp strip_bom(<<0xEF, 0xBB, 0xBF, rest::binary>>), do: rest
defp strip_bom(text), do: text
defp outside(<<>>, field, row, rows), do: emit(field, row, rows)
defp outside(<<?", rest::binary>>, [], row, rows), do: inside(rest, [], row, rows)
defp outside(<<?,, rest::binary>>, field, row, rows),
do: outside(rest, [], [flush(field) | row], rows)
defp outside(<<?\r, ?\n, rest::binary>>, field, row, rows),
do: outside(rest, [], [], [Enum.reverse([flush(field) | row]) | rows])
defp outside(<<?\n, rest::binary>>, field, row, rows),
do: outside(rest, [], [], [Enum.reverse([flush(field) | row]) | rows])
defp outside(<<char, rest::binary>>, field, row, rows),
do: outside(rest, [field, char], row, rows)
defp inside(<<>>, field, row, rows), do: emit(field, row, rows)
defp inside(<<?", ?", rest::binary>>, field, row, rows),
do: inside(rest, [field, ?"], row, rows)
defp inside(<<?", rest::binary>>, field, row, rows), do: outside(rest, field, row, rows)
defp inside(<<char, rest::binary>>, field, row, rows),
do: inside(rest, [field, char], row, rows)
defp emit(field, row, rows) do
Enum.reverse([Enum.reverse([flush(field) | row]) | rows])
end
defp flush(field), do: IO.iodata_to_binary(field)
end
|