Current section
Files
Jump to
Current section
Files
src/gsv.erl
-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 = <<<<Line_piece/binary, Newline/binary>>/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 = <<<<Field/binary, Field_piece/binary>>/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;
_ ->
<<Field/binary, Field_piece@1/binary>>
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;
_ ->
<<Field/binary, Field_piece@2/binary>>
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.