-module(erl_csv_decoder). -include("erl_csv.hrl"). -export([decode/2]). -record(csv_decoder, { line_break = ?DELIMITER :: <<_:8>> | <<_:16>>, separator = ?SEPARATOR :: <<_:8>>, quotes = ?QUOTES :: <<_:8>> }). -spec decode(iolist() | list(map()), erl_csv:decode_opts()) -> {ok, iolist()} | {error, iolist()}. decode(Chunk, Opts) -> Separator = maps:get(separator, Opts, ?SEPARATOR), Delimiter = maps:get(delimiter, Opts, ?DELIMITER), Quotes = maps:get(qoutes, Opts, ?QUOTES), {ok, Regex} = re:compile( <<"(", Quotes/binary, ")?", "(?(1)", "((", Quotes/binary, "{2}|[^", Quotes/binary, "])*", Quotes/binary, ")", "|[^", Quotes/binary, Separator/binary, Delimiter/binary,"]*)", "(", Separator/binary, "|", Delimiter/binary, ")">>), State = #csv_decoder{separator = Separator, line_break = Delimiter, quotes = Quotes}, Match = re:run(Chunk, Regex, [global, {capture, all, index}]), process_match(Match, State, Chunk). process_match(nomatch, _, Chunk) -> {error, Chunk}; process_match({match, Matches}, State, Chunk) -> case process_chunk(Matches, State, Chunk) of {Decoded, <<>>} -> {ok, Decoded}; {Decoded, Trailer} -> {has_trailer, Decoded, Trailer} end. process_chunk(Matches, #csv_decoder{line_break = LineBreak} = State, Chunk) -> Matches2 = filter_incomplete_lines(Matches, Chunk, LineBreak), process_chunk(Matches2, Chunk, State, [], [], 0). process_chunk([], Chunk, #csv_decoder{}, [], Acc, LenProcessed) -> Size = byte_size(Chunk), NotProcessed = Size - LenProcessed, NewChunk = binary:part(Chunk, Size, - NotProcessed), {lists:reverse(Acc), NewChunk}; process_chunk([ [{Pos, Len} | _] | Matches], Chunk, #csv_decoder{line_break = LineBreak, separator = SepBy} = State, LineAcc, Acc, _) -> Csv = binary:part(Chunk, Pos, Len - 1), Csv2 = format_term(Csv, State), case binary:part(Chunk, Pos + Len, - 1) of LineBreak -> NewLine = lists:reverse([Csv2 | LineAcc]), process_chunk(Matches, Chunk, State, [], [NewLine | Acc], Pos + Len); SepBy -> process_chunk(Matches, Chunk, State, [Csv2 | LineAcc], Acc, Pos + Len) end. filter_incomplete_lines(Matches, Chunk, LineBreak) -> Fun = fun(Match) -> PosLen = lists:last(Match), binary:part(Chunk, PosLen) =/= LineBreak end, lists:reverse(lists:dropwhile(Fun, lists:reverse(Matches))). format_term(CsvTerm, #csv_decoder{quotes = Q}) -> Term1 = case CsvTerm of <> -> Size = byte_size(Rest) - 1, <> = Rest, Term; _ -> CsvTerm end, binary:replace(Term1, <>, Q, [global]). % binary:replace(Term2, LineBreak, <<>>, [global]). % StripFields = maps:get(strip_fields, Opts, true), % Preprocessor = maps:get(preprocessor, Opts, lines), % ValidateRowLength = maps:get(validate_row_length, Opts, true), % EsscapeMaxLines = maps:get(escape_max_lines, Opts, 1000), % NumWorkers = maps:get(num_workers, Opts, erlang:system_info(online_schedulers)), % WorkerWorkRatio = maps:get(validate_row_length, Opts, 5), % Headers = maps:get(headers, Opts, false), % <<>>.