summaryrefslogtreecommitdiffstats
path: root/lib/cepis/csv.ex
blob: 58b554f7cc342ffe6b39cd45ad656173304bce44 (plain)
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