Current section
Files
Jump to
Current section
Files
src/grid.erl
-module(grid).
% API
-export([format/1]).
-ignore_xref({format, 1}).
-export([format/2]).
-ignore_xref({format, 2}).
-export([cell/2]).
-ignore_xref({cell, 2}).
%--- Macros --------------------------------------------------------------------
-define(cell(Item, Length), {'$cell', Length, Item}).
%--- API -----------------------------------------------------------------------
format(Items) -> format(Items, #{}).
format(Items, Opts) ->
{Rows, Columns} = process(Items, columns(Opts), opts(Opts)),
render(Rows, Columns, Opts).
cell(Item, Length) -> ?cell(Item, Length).
%--- Internal ------------------------------------------------------------------
opts(Opts) -> maps:map(fun opt/2, Opts).
opt(header, true) -> #{format => fun(V) -> V end};
opt(header, Format) when is_atom(Format) -> #{format => Format};
opt(columns, []) -> error(empty_columns);
opt(_K, V) -> V.
columns(Opts) -> columns(maps:get(columns, Opts, []), 1, #{}).
columns([], Index, Acc) ->
{Index, Acc};
columns([Column | Columns], Index, Acc) when is_atom(Column) ->
NewAcc = maps:put(Index, #{key => Column, index => Index}, Acc),
columns(Columns, Index + 1, NewAcc);
columns([I | Columns], Index, Acc) when is_integer(I) ->
NewAcc = maps:put(I, #{index => Index}, Acc),
columns(Columns, I, NewAcc);
columns([#{index := I} = Column | Columns], _Index, Acc) ->
case maps:find(I, Acc) of
{ok, Existing} -> error({duplicate_index, Column, Existing});
error -> columns(Columns, I + 1, maps:put(I, Column, Acc))
end;
columns([Column | Columns], Index, Acc) when is_map(Column) ->
columns(Columns, Index + 1, maps:put(Index, Column#{index => Index}, Acc)).
process(Items, Columns, Opts) ->
{Rows, AllColumns} = process(Items, Columns, [], Opts),
{Rows, filter_columns(Columns, AllColumns)}.
process([], Columns, Rows, Opts) ->
{Header, {_Index, NewColumns}} = process_headers(Columns, Opts),
AllRows = Header ++ lists:reverse(Rows),
IndexCompare = fun(#{index := I1}, #{index := I2}) -> I1 =< I2 end,
SortedColumns = lists:sort(IndexCompare, maps:values(NewColumns)),
{AllRows, SortedColumns};
process([Item | Rest], Columns, Rows, Opts) ->
{Row, NewColumns} = process_row(Item, Columns),
process(Rest, NewColumns, [Row | Rows], Opts).
process_headers({Index, Columns}, #{header := Header}) ->
Process = fun(_, Column, Acc) -> process_header(Header, Column, Acc) end,
Item = lists:reverse(maps:fold(Process, [], Columns)),
{Formatted, NewColumns} = process_row(Item, {Index, Columns}),
{[Formatted], NewColumns};
process_headers(Columns, _Opts) ->
{[], Columns}.
process_header(Header, #{name := Name}, Acc) ->
[format_header(Header, Name) | Acc];
process_header(Header, #{key := Key}, Acc) ->
[format_header(Header, render_cell_value(Key)) | Acc];
process_header(Header, #{index := I}, Acc) ->
[format_header(Header, render_cell_value(I)) | Acc].
process_row(Row, Columns) ->
process_row(next(iter(Row)), Columns, {}).
process_row(none, Columns, Acc) ->
{Acc, Columns};
process_row({Item, Pos, Iter}, Columns, Acc) ->
Cell = process_cell(Item),
{Index, NewColumns} =
update_columns(Pos, #{width => cell_length(Cell)}, Columns),
NewAcc = set_cell(Acc, Index, Cell),
process_row(next(Iter), NewColumns, NewAcc).
cell_length(?cell(_Text, Length)) -> Length.
set_cell(Row, Index, Cell) when Index =< tuple_size(Row) ->
setelement(Index, Row, Cell);
set_cell(Row, Index, Cell) when Index == tuple_size(Row) + 1 ->
erlang:append_element(Row, Cell);
set_cell(Row, Index, Cell) ->
set_cell(erlang:append_element(Row, '_'), Index, Cell).
iter(Item) when is_list(Item) -> {list, 1, Item};
iter(Item) when is_tuple(Item) -> {tuple, 1, Item};
iter(Item) when is_map(Item) -> {map, 1, maps:next(maps:iterator(Item))};
iter(Item) -> error({unknown_row_type, Item}).
next({list, _Index, []}) ->
none;
next({list, Index, [Elem | Rest]}) ->
{Elem, {index, Index}, {list, Index + 1, Rest}};
next({tuple, Index, Tuple}) when Index > tuple_size(Tuple) ->
none;
next({tuple, Index, Tuple}) ->
{element(Index, Tuple), {index, Index}, {tuple, Index + 1, Tuple}};
next({map, _Index, none}) ->
none;
next({map, Index, {Key, Value, NewIter}}) ->
{Value, {key, Key}, {map, Index + 1, NewIter}}.
process_cell(?cell(Term, Length)) ->
?cell(render_cell_value(Term), Length);
process_cell(Term) ->
Rendered = render_cell_value(Term),
?cell(Rendered, string:length(Rendered)).
update_columns({index, Value}, Attrs, {Index, Columns}) ->
NewColumns =
case maps:find(Value, Columns) of
{ok, Column} ->
{Index, maps:put(Value, update_column(Column, Attrs), Columns)};
error ->
{Index + 1, maps:put(Index, Attrs#{index => Index}, Columns)}
end,
{Value, NewColumns};
update_columns({Key, Value}, Attrs, {Index, Columns}) ->
First = maps:next(maps:iterator(Columns)),
update_columns(Key, Value, Attrs, {Index, Columns}, First).
update_columns(Key, Value, Attrs, {Index, Columns}, none) ->
NewColumns = maps:put(Index, Attrs#{Key => Value, index => Index}, Columns),
{Index, {Index + 1, NewColumns}};
update_columns(Key, Value, Attrs, {Index, Columns}, {I, Column, Iter}) ->
case Column of
#{Key := Value, index := I} ->
{I, {Index, maps:put(I, update_column(Column, Attrs), Columns)}};
_Other ->
update_columns(Key, Value, Attrs, {Index, Columns}, maps:next(Iter))
end.
update_column(Column, Updates) ->
Update = fun
(width, W1, W2) -> max(W1, W2);
(_, _, V) -> V
end,
% FIXME: When supporting OTP 24+ only, use maps:merge_with/3 instead
% maps:merge_with(Update, Column, Updates).
maps:fold(
fun(Key, Value, C) ->
case maps:find(Key, C) of
{ok, Old} -> C#{Key := Update(Key, Old, Value)};
error -> C#{Key => Value}
end
end,
Column,
Updates
).
render(Items, Columns, Opts) ->
lists:map(fun(Item) -> render_row(Item, Columns, Opts) end, Items).
render_row({}, _Columns, _Opts) ->
[$\n];
render_row({Cell}, [Column | _], _Opts) ->
[render_cell(Cell, Column, no_padding), $\n];
render_row(Row, [Column], _Opts) ->
[render_cell(element(1, Row), Column, no_padding), $\n];
render_row(Row, [Column | Columns], Opts) ->
Rendered = render_cell(element(1, Row), Column, padding),
[Rendered, render_cells(Row, 2, Columns, Opts), $\n].
render_cells(_Row, _Index, [], _Opts) ->
[];
render_cells(Row, Index, [Column | _], Opts) when Index == tuple_size(Row) ->
[spacer(Opts), render_cell(element(Index, Row), Column, no_padding)];
render_cells(Row, Index, [Column], Opts) ->
[spacer(Opts), render_cell(element(Index, Row), Column, no_padding)];
render_cells(Row, Index, [Column | Columns], Opts) ->
Rendered = render_cell(element(Index, Row), Column, padding),
[spacer(Opts), Rendered | render_cells(Row, Index + 1, Columns, Opts)].
render_cell('_', _Column, no_padding) ->
[];
render_cell('_', #{width := CW}, padding) ->
lists:duplicate(CW, $\s);
render_cell(?cell(Text, W), #{width := CW}, padding) when W =< CW ->
[Text, lists:duplicate(CW - W, $\s)];
render_cell(?cell(Text, _W), _Column, no_padding) ->
[Text].
render_cell_value(Term) when is_binary(Term); is_list(Term) ->
Term;
render_cell_value(Term) when is_integer(Term) ->
integer_to_binary(Term);
render_cell_value(Term) when is_atom(Term) ->
atom_to_binary(Term, utf8).
filter_columns({_, Desired}, Columns) when map_size(Desired) == 0 -> Columns;
filter_columns({_, Desired}, All) -> lists:sublist(All, maps:size(Desired)).
spacer(#{spacer := Spacer}) -> Spacer;
spacer(_Opts) -> <<" ">>.
format_header(#{format := Format}, Header) ->
format_text(Format, Header).
format_text(uppercase, Binary) -> string:uppercase(words(Binary));
format_text(titlecase, Binary) -> string:titlecase(words(Binary));
format_text(lowercase, Binary) -> string:lowercase(words(Binary));
format_text(Fun, Binary) when is_function(Fun, 1) -> Fun(Binary);
format_text(Format, _Binary) -> error({invalid_format, Format}).
words(Binary) -> string:replace(Binary, <<"_">>, <<" ">>, all).