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(<>, [], row, rows), do: inside(rest, [], row, rows) defp outside(<>, field, row, rows), do: outside(rest, [], [flush(field) | row], rows) defp outside(<>, field, row, rows), do: outside(rest, [], [], [Enum.reverse([flush(field) | row]) | rows]) defp outside(<>, field, row, rows), do: outside(rest, [], [], [Enum.reverse([flush(field) | row]) | rows]) defp outside(<>, field, row, rows), do: outside(rest, [field, char], row, rows) defp inside(<<>>, field, row, rows), do: emit(field, row, rows) defp inside(<>, field, row, rows), do: inside(rest, [field, ?"], row, rows) defp inside(<>, field, row, rows), do: outside(rest, field, row, rows) defp inside(<>, 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