-module(gsv). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/gsv.gleam"). -export([to_lists/2, to_dicts/2, from_lists/3, from_dicts/3]). -export_type([line_ending/0, error/0, splitters/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. -type line_ending() :: windows | unix. -type error() :: {unescaped_quote, integer()} | {missing_closing_quote, integer()}. -type splitters() :: {splitters, binary(), splitter:splitter(), splitter:splitter(), splitter:splitter()}. -file("src/gsv.gleam", 57). -spec line_ending_to_string(line_ending()) -> binary(). line_ending_to_string(Le) -> case Le of windows -> <<"\r\n"/utf8>>; unix -> <<"\n"/utf8>> end. -file("src/gsv.gleam", 213). ?DOC( " This parses an escaped field from the start of the line, once we've already\n" " found the opening `\"`.\n" "\n" " > You might have noticed this also needs the rest of the document and just\n" " > the current line is not enough. This is because an escaped field might\n" " > contain newlines, and so we could end up needing to get more lines from\n" " > the `rest` of the csv to properly parse the row.\n" ). -spec escaped_loop( binary(), binary(), binary(), splitters(), integer(), integer(), binary() ) -> {ok, {binary(), binary(), binary(), integer(), binary()}} | {error, error()}. escaped_loop(Line, Newline, Rest, Splitters, Start, Line_number, Field) -> case splitter_ffi:split(erlang:element(5, Splitters), Line) of {Line_piece, <<""/utf8>>, <<""/utf8>>} -> case splitter_ffi:split(erlang:element(3, Splitters), Rest) of {<<""/utf8>>, <<""/utf8>>, <<""/utf8>>} -> {error, {missing_closing_quote, Start}}; {Line@1, Rest_newline, Rest@1} -> Line@2 = <<<>/binary, Line@1/binary>>, Line_number@1 = Line_number + 1, escaped_loop( Line@2, Rest_newline, Rest@1, Splitters, Start, Line_number@1, Field ) end; {Field_piece, <<"\"\""/utf8>>, Line@3} -> Field@1 = <<<>/binary, "\""/utf8>>, escaped_loop( Line@3, Newline, Rest, Splitters, Start, Line_number, Field@1 ); {Field_piece@1, _, <<""/utf8>>} -> Field@2 = case Field of <<""/utf8>> -> Field_piece@1; _ -> <> end, {ok, {<<""/utf8>>, Newline, Rest, Line_number, Field@2}}; {Field_piece@2, _, Line@4} -> case gleam_stdlib:string_starts_with( Line@4, erlang:element(2, Splitters) ) of false -> {error, {unescaped_quote, Line_number}}; true -> Line@5 = erlang:element( 3, splitter_ffi:split(erlang:element(4, Splitters), Line@4) ), Field@3 = case Field of <<""/utf8>> -> Field_piece@2; _ -> <> end, {ok, {Line@5, Newline, Rest, Line_number, Field@3}} end end. -file("src/gsv.gleam", 161). ?DOC( " This parses all the fields from the given line, returning them along with\n" " the reamining part of the csv that was not parsed.\n" "\n" " > You might have noticed that this doesn't just takes the line to parse as\n" " > input, but it also needs the rest of the csv file. It might sound counter\n" " > intuitive, but its need is explained in more detail in the `escape_loop`\n" " > function.\n" ). -spec field_loop( binary(), binary(), binary(), splitters(), integer(), list(binary()) ) -> {ok, {binary(), integer(), list(binary())}} | {error, error()}. field_loop(Line, Newline, Rest, Splitters, Line_number, Fields) -> case splitter_ffi:split(erlang:element(4, Splitters), Line) of {Field, <<""/utf8>>, <<""/utf8>>} -> {ok, {Rest, Line_number, lists:reverse([Field | Fields])}}; {<<""/utf8>>, <<"\""/utf8>>, Line@1} -> Start = Line_number, Field@1 = escaped_loop( Line@1, Newline, Rest, Splitters, Start, Line_number, <<""/utf8>> ), case Field@1 of {error, Error} -> {error, Error}; {ok, {<<""/utf8>>, _, Rest@1, Line_number@1, Field@2}} -> {ok, {Rest@1, Line_number@1, lists:reverse([Field@2 | Fields])}}; {ok, {Line@2, Newline@1, Rest@2, Line_number@2, Field@3}} -> Fields@1 = [Field@3 | Fields], field_loop( Line@2, Newline@1, Rest@2, Splitters, Line_number@2, Fields@1 ) end; {_, <<"\""/utf8>>, _} -> {error, {unescaped_quote, Line_number}}; {Field@4, _, Line@3} -> Fields@2 = [Field@4 | Fields], field_loop(Line@3, Newline, Rest, Splitters, Line_number, Fields@2) end. -file("src/gsv.gleam", 135). -spec line_loop(binary(), splitters(), integer()) -> {ok, {binary(), integer(), list(binary())}} | {error, error()}. line_loop(Csv, Splitters, Line_number) -> case splitter_ffi:split(erlang:element(3, Splitters), Csv) of {<<""/utf8>>, _, <<""/utf8>>} -> {ok, {<<""/utf8>>, Line_number, []}}; {<<""/utf8>>, _, Rest} -> line_loop(Rest, Splitters, Line_number + 1); {Line, Newline, Rest@1} -> field_loop(Line, Newline, Rest@1, Splitters, Line_number + 1, []) end. -file("src/gsv.gleam", 118). -spec lines_loop(binary(), splitters(), integer(), list(list(binary()))) -> {ok, list(list(binary()))} | {error, error()}. lines_loop(Csv, Splitters, Line_number, Lines) -> case Csv of <<""/utf8>> -> {ok, lists:reverse(Lines)}; _ -> case line_loop(Csv, Splitters, Line_number) of {error, Error} -> {error, Error}; {ok, {Rest, Line_number@1, Line}} -> lines_loop(Rest, Splitters, Line_number@1, [Line | Lines]) end end. -file("src/gsv.gleam", 103). ?DOC( " Parses a csv string into a list of lists of strings: each line of the csv\n" " will be turned into a list with an item for each field.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " \"hello, world\n" " goodbye, mars\"\n" " |> gsv.to_lists(seperator: \",\")\n" " // Ok([\n" " // [\"hello\", \" world\"],\n" " // [\"goodbye\", \" mars\"],\n" " // ])\n" " ```\n" "\n" " > This implementation tries to stick as closely as possible to\n" " > [RFC4180](https://www.ietf.org/rfc/rfc4180.txt), with a couple of notable\n" " > differences:\n" " > - both `\\n` and `\\r\\n` line endings are accepted.\n" " > - empty lines are allowed and just ignored.\n" " > - lines are not forced to all have the same number of fields.\n" " > - a line can start with an empty field `,two,three`.\n" " > - a line can end with an empty field `one,two,`.\n" ). -spec to_lists(binary(), binary()) -> {ok, list(list(binary()))} | {error, error()}. to_lists(Csv, Separator) -> Splitters = {splitters, Separator, splitter:new([<<"\r\n"/utf8>>, <<"\n"/utf8>>]), splitter:new([Separator, <<"\""/utf8>>]), splitter:new([<<"\"\""/utf8>>, <<"\""/utf8>>])}, lines_loop(Csv, Splitters, 0, []). -file("src/gsv.gleam", 318). ?DOC( " Parses a csv string into a list of dicts: the first line of the csv is\n" " interpreted as the headers' row and each of the following lines is turned\n" " into a dict with a value for each of the headers.\n" "\n" " If a field is empty then it won't be added to the dict.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " \"pet,name,cuteness\n" " dog,Fido,100\n" " cat,,1000\n" " \"\n" " |> gsv.to_dicts(separator: \",\")\n" " // Ok([\n" " // dict.from_list([\n" " // #(\"pet\", \"dog\"), #(\"name\", \"Fido\"), #(\"cuteness\", \"100\")\n" " // ]),\n" " // dict.from_list([\n" " // #(\"pet\", \"cat\"), #(\"cuteness\", \"1000\")\n" " // ]),\n" " // ])\n" " ```\n" "\n" " > Just list `to_lists` this implementation tries to stick as closely as\n" " > possible to [RFC4180](https://www.ietf.org/rfc/rfc4180.txt).\n" " > You can look at `to_lists`' documentation to see how it differs from the\n" " > RFC.\n" ). -spec to_dicts(binary(), binary()) -> {ok, list(gleam@dict:dict(binary(), binary()))} | {error, error()}. to_dicts(Input, Field_separator) -> gleam@result:map(to_lists(Input, Field_separator), fun(Rows) -> case Rows of [] -> []; [Headers | Rows@1] -> Headers@1 = erlang:list_to_tuple(Headers), gleam@list:map( Rows@1, fun(Row) -> gleam@list:index_fold( Row, maps:new(), fun(Row@1, Field, Index) -> case Field of <<""/utf8>> -> Row@1; _ -> case glearray_ffi:get( Headers@1, Index ) of {ok, Header} -> gleam@dict:insert( Row@1, Header, Field ); {error, _} -> Row@1 end end end ) end ) end end). -file("src/gsv.gleam", 377). -spec escape_field(binary(), binary()) -> binary(). escape_field(Field, Separator) -> case gleam_stdlib:contains_string(Field, <<"\""/utf8>>) of true -> <<<<"\""/utf8, (gleam@string:replace(Field, <<"\""/utf8>>, <<"\"\""/utf8>>))/binary>>/binary, "\""/utf8>>; false -> case gleam_stdlib:contains_string(Field, Separator) orelse gleam_stdlib:contains_string( Field, <<"\n"/utf8>> ) of true -> <<<<"\""/utf8, Field/binary>>/binary, "\""/utf8>>; false -> Field end end. -file("src/gsv.gleam", 362). ?DOC( " Takes a list of lists of strings and turns it to a csv string, automatically\n" " escaping all fields that contain double quotes or line endings.\n" "\n" " ## Examples\n" "\n" " ```gleam\n" " let rows = [[\"hello\", \"world\"], [\"goodbye\", \"mars\"]]\n" " from_lists(rows, separator: \",\", line_ending: Unix)\n" " // \"hello,world\n" " // goodbye,mars\"\n" " ```\n" "\n" " ```gleam\n" " let rows = [[]]\n" " ```\n" ). -spec from_lists(list(list(binary())), binary(), line_ending()) -> binary(). from_lists(Rows, Separator, Line_ending) -> Line_ending@1 = line_ending_to_string(Line_ending), _pipe@1 = gleam@list:map( Rows, fun(Row) -> _pipe = gleam@list:map( Row, fun(_capture) -> escape_field(_capture, Separator) end ), gleam@string:join(_pipe, Separator) end ), _pipe@2 = gleam@string:join(_pipe@1, Line_ending@1), gleam@string:append(_pipe@2, Line_ending@1). -file("src/gsv.gleam", 414). -spec row_dict_to_list(gleam@dict:dict(binary(), binary()), list(binary())) -> list(binary()). row_dict_to_list(Row, Headers) -> gleam@list:map( Headers, fun(Header) -> case gleam_stdlib:map_get(Row, Header) of {ok, Field} -> Field; {error, nil} -> <<""/utf8>> end end ). -file("src/gsv.gleam", 394). ?DOC( " Takes a list of dicts and writes it to a csv string.\n" " Will automatically escape strings that contain double quotes or\n" " line endings with double quotes (in csv, double quotes get escaped by doing\n" " a double doublequote)\n" " The string `he\"llo\\n` becomes `\"he\"\"llo\\n\"`\n" ). -spec from_dicts( list(gleam@dict:dict(binary(), binary())), binary(), line_ending() ) -> binary(). from_dicts(Rows, Separator, Line_ending) -> case Rows of [] -> <<""/utf8>>; _ -> Headers = begin _pipe = Rows, _pipe@1 = gleam@list:flat_map(_pipe, fun maps:keys/1), _pipe@2 = gleam@list:unique(_pipe@1), gleam@list:sort(_pipe@2, fun gleam@string:compare/2) end, Rows@1 = gleam@list:map( Rows, fun(_capture) -> row_dict_to_list(_capture, Headers) end ), from_lists([Headers | Rows@1], Separator, Line_ending) end.