Current section
Files
Jump to
Current section
Files
lib/xlsx.ex
defmodule CsvParser.Xlsx do
alias __MODULE__
@moduledoc false
defstruct [:sheet, :strings, :opts]
def new(path, opts) do
with {:ok, xlsx} <- :zip.unzip(String.to_charlist(path), [:memory]),
{:ok, sheet} <- extract_sheet(xlsx, opts)
do
shared_string_data = Enum.find_value(xlsx, fn
{'xl/sharedStrings.xml', data} -> data
_ -> nil
end)
strings = parse_strings(shared_string_data)
{:ok, %Xlsx{sheet: sheet, strings: strings, opts: opts}}
else
nil -> {:error, :unknown_sheet}
_ -> {:error, :invalid_format}
end
end
defp extract_sheet(xlsx, opts) do
suffix = case opts[:sheet_index] do
nil -> nil
index -> String.to_charlist("#{index}.xml")
end
Enum.find_value(xlsx, fn
{[?x, ?l, ?/, ?w, ?o, ?r, ?k, ?s, ?h, ?e, ?e, ?t, ?s, ?/, ?s, ?h, ?e, ?e, ?t | rest], data} ->
case suffix == nil || suffix == rest do
true -> {:ok, data}
false -> nil
end
_ -> nil
end)
end
# not sure if this can happen, but no harm to handle it
defp parse_strings(nil), do: {}
defp parse_strings(data) do
{:ok, {strings, _, _}, _} =
:erlsom.parse_sax(data, {[], false, false}, fn event, {acc, item?, text?} ->
case simplify(event) do
{:startElement, 'si', _} -> {acc, true, false}
{:startElement, 't', _} -> {acc, item?, true}
{:characters, value} ->
case item? && text? do
true -> {[String.Chars.to_string(value) | acc], true, true}
false -> {acc, item?, text?}
end
{:endElement, 't'} -> {acc, item?, false}
{:endElement, 'si'} -> {acc, false, false}
_ -> {acc, item?, text?}
end
end)
strings
|> Enum.reverse()
|> List.to_tuple()
end
def reduce(xlsx, acc, fun) do
strings = xlsx.strings
{:ok, {acc, _, _, _}, _} =
:erlsom.parse_sax(xlsx.sheet, {acc, nil, nil, false}, fn event, {acc, row, type, val?} ->
case simplify(event) do
{:startElement, 'row', _} -> {acc, [], nil, false}
{:startElement, 'c', attr} -> {acc, row, get_attribute(attr, 't'), false}
{:startElement, 'v', _} -> {acc, row, type, true}
{:characters, value} ->
case row != nil && type != nil && val? do
true ->
value = case type do
's' -> elem(strings, elem(:string.to_integer(value), 0))
_ -> String.Chars.to_string(value)
end
{acc, [value | row], true, true}
false -> {acc, row, type, val?}
end
{:endElement, 'v'} -> {acc, row, type, false}
{:endElement, 'c'} -> {acc, row, nil, false}
{:endElement, 'row'} -> {fun.({:ok, Enum.reverse(row)}, acc), nil, nil, false}
_ -> {acc, row, type, val?}
end
end)
acc
end
defp simplify({:startElement, _url, name, _prefix, attributes}), do: {:startElement, name, attributes}
defp simplify({:endElement, _url, name, _prefix}), do: {:endElement, name}
defp simplify(event), do: event
defp get_attribute(attributes, type) do
Enum.find_value(attributes, fn
{:attribute, ^type, [], [], value} -> value
_ -> nil
end)
end
end