Current section

Files

Jump to
clique src clique_table.erl
Raw

src/clique_table.erl

%% -------------------------------------------------------------------
%%
%% Copyright (c) 2013, 2014 Basho Technologies, Inc.
%%
%% This file is provided to you under the Apache License,
%% Version 2.0 (the "License"); you may not use this file
%% except in compliance with the License. You may obtain
%% a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing,
%% software distributed under the License is distributed on an
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
%% KIND, either express or implied. See the License for the
%% specific language governing permissions and limitations
%% under the License.
%%
%% -------------------------------------------------------------------
-module(clique_table).
%% API
-export([print/2, print/3,
create_table/2,
autosize_create_table/2, autosize_create_table/3]).
-include("clique_status_types.hrl").
-define(MAX_LINE_LEN, 100).
-define(else, true).
-define(MINWIDTH(W),
if W =< 0 ->
1;
?else ->
W
end).
-spec print(list(), list()) -> ok.
print(_Spec, []) ->
ok;
%% Explict sizes were not given. This is called using the new status types.
print(Schema, Rows) when is_list(hd(Schema)) ->
Table = autosize_create_table(Schema, Rows),
io:format("~n~ts~n", [Table]);
print(Spec, Rows) ->
Table = create_table(Spec, Rows),
io:format("~n~ts~n", [Table]).
-spec print(list(), list(), list()) -> ok.
print(_Hdr, _Spec, []) ->
ok;
print(Header, Spec, Rows) ->
Table = create_table(Spec, Rows),
io:format("~ts~n~n~ts~n", [Header, Table]).
-spec autosize_create_table([any()], [[any()]]) -> iolist().
autosize_create_table(Schema, Rows) ->
autosize_create_table(Schema, Rows, []).
%% Currently the only constraint supported in the proplist is
%% `fixed_width' with a list of columns that *must not* be shrunk
%% (e.g., integer values). First column is 0.
-spec autosize_create_table([any()], [[any()]], [tuple()]) -> iolist().
autosize_create_table(Schema, Rows, Constraints) ->
BorderSize = 1 + length(hd(Rows)),
MaxLineLen = case io:columns() of
%% Leaving an extra space seems to work better
{ok, N} -> N - 1;
{error, enotsup} -> ?MAX_LINE_LEN
end,
Sizes = get_field_widths(MaxLineLen - BorderSize, [Schema | Rows],
proplists:get_value(fixed_width, Constraints, [])),
Spec = lists:zip(Schema, Sizes),
create_table(Spec, Rows, MaxLineLen, []).
-spec create_table(list(), list()) -> iolist().
create_table(Spec, Rows) ->
Lengths = get_row_length(Spec, Rows),
Length = lists:sum(Lengths)+2,
AdjustedSpec = [{Field, NewLength} || {{Field, _DefaultLength}, NewLength}
<- lists:zip(Spec, Lengths)],
create_table(AdjustedSpec, Rows, Length, []).
-spec create_table(list(), list(), non_neg_integer(), iolist()) -> iolist().
create_table(Spec, Rows, Length, []) ->
FirstThreeRows = [vertical_border(Spec), titles(Spec),
vertical_border(Spec)],
create_table(Spec, Rows, Length, FirstThreeRows);
create_table(_Spec, [], _Length, IoList) when length(IoList) == 3 ->
%% table had no rows, no final row needed
lists:reverse(IoList);
create_table(Spec, [], _Length, IoList) ->
BottomBorder = vertical_border(Spec),
%% There are no more rows to print so return the table
lists:reverse([BottomBorder | IoList]);
create_table(Spec, [Row | Rows], Length, IoList) ->
create_table(Spec, Rows, Length, [row(Spec, Row) | IoList]).
%% Measure and shrink table width as necessary to fit the console
-spec get_field_widths(pos_integer(), [term()], [non_neg_integer()]) -> [non_neg_integer()].
get_field_widths(MaxLineLen, Rows, Unshrinkable) ->
Widths = max_widths(Rows),
fit_widths_to_terminal(MaxLineLen, Widths, Unshrinkable).
fit_widths_to_terminal(MaxWidth, Widths, Unshrinkable) ->
Sum = lists:sum(Widths),
Weights = calculate_field_weights(Sum, Widths, Unshrinkable),
MustRemove = Sum - MaxWidth,
calculate_new_widths(MaxWidth, MustRemove, Widths, Weights).
%% Determine field weighting as proportion of total width of the
%% table. Fields which were flagged as unshrinkable will be given a
%% weight of 0.
-spec calculate_field_weights(pos_integer(), list(pos_integer()),
list(non_neg_integer())) ->
list(number()).
calculate_field_weights(Sum, Widths, []) ->
%% If no fields are constrained as unshrinkable, simply divide
%% each width by the sum of all widths for our proportions
lists:map(fun(X) -> X / Sum end, Widths);
calculate_field_weights(_Sum, Widths, Unshrinkable) ->
TaggedWidths = flag_unshrinkable_widths(Widths, Unshrinkable),
ShrinkableWidth = lists:sum(lists:filter(fun({_X, noshrink}) -> false;
(_X) -> true end,
TaggedWidths)),
lists:map(fun({_X, noshrink}) -> 0;
(X) -> X / ShrinkableWidth end,
TaggedWidths).
%% Takes a list of column widths and a list of (zero-based) index
%% values of the columns that must not shrink. Returns a mixed list of
%% widths and `noshrink' tuples.
flag_unshrinkable_widths(Widths, NoShrink) ->
{_, NewWidths} =
lists:foldl(fun(X, {Idx, Mapped}) ->
case lists:member(Idx, NoShrink) of
true ->
{Idx + 1, [{X, noshrink}|Mapped]};
false ->
{Idx + 1, [X|Mapped]}
end
end, {0, []}, Widths),
lists:reverse(NewWidths).
%% Calculate the proportional weight for each column for shrinking.
%% Zip the results into a `{Width, Weight, Index}' tuple list.
column_zip(Widths, Weights, ToNarrow) ->
column_zip(Widths, Weights, ToNarrow, 0, []).
column_zip([], [], _ToNarrow, _Index, Accum) ->
lists:reverse(Accum);
column_zip([Width|Widths], [Weight|Weights], ToNarrow, Index, Accum) ->
NewWidth = ?MINWIDTH(Width - round(ToNarrow * Weight)),
column_zip(Widths, Weights, ToNarrow, Index+1,
[{NewWidth, Weight, Index}] ++ Accum).
%% Given the widths based on data to be displayed, return widths
%% necessary to narrow the table to fit the console.
calculate_new_widths(_Max, ToNarrow, Widths, _Weights) when ToNarrow =< 0 ->
%% Console is wide enough, no need to narrow
Widths;
calculate_new_widths(MaxWidth, ToNarrow, Widths, Weights) ->
fix_rounding(MaxWidth, column_zip(Widths, Weights, ToNarrow)).
%% Rounding may introduce an error. If so, remove the requisite number
%% of spaces from the widest field
fix_rounding(Target, Cols) ->
Widths = lists:map(fun({Width, _Weight, _Idx}) -> Width end,
Cols),
SumWidths = lists:sum(Widths),
shrink_widest(Target, SumWidths, Widths, Cols).
%% Determine whether our target table width is wider than the terminal
%% due to any rounding error and find columns eligible to be shrunk.
shrink_widest(Target, Current, Widths, _Cols) when Target =< Current ->
Widths;
shrink_widest(Target, Current, Widths, Cols) ->
Gap = Current - Target,
NonZeroWeighted = lists:dropwhile(fun({_Width, 0, _Idx}) -> true;
(_) -> false end,
Cols),
shrink_widest_weighted(Gap, NonZeroWeighted, Widths).
%% Take the widest column with a non-zero weight and reduce it by the
%% amount necessary to compensate for any rounding error.
shrink_widest_weighted(_Gap, [], Widths) ->
Widths; %% All columns constrained to fixed widths, nothing we can do
shrink_widest_weighted(Gap, Cols, Widths) ->
SortedCols = lists:sort(
fun({WidthA, _WeightA, _IdxA}, {WidthB, _WeightB, _IdxB}) ->
WidthA > WidthB
end, Cols),
{OldWidth, _Weight, Idx} = hd(SortedCols),
NewWidth = ?MINWIDTH(OldWidth - Gap),
replace_list_element(Idx, NewWidth, Widths).
%% Replace the item at `Index' in `List' with `Element'.
%% Zero-based indexing.
-spec replace_list_element(non_neg_integer(), term(), list()) -> list().
replace_list_element(Index, Element, List) ->
{Prefix, Suffix} = lists:split(Index, List),
Prefix ++ [Element] ++ tl(Suffix).
get_row_length(Spec, Rows) ->
Res = lists:foldl(fun({_Name, MinSize}, Total) ->
Longest = find_longest_field(Rows, length(Total)+1),
Size = erlang:max(MinSize, Longest),
[Size | Total]
end, [], Spec),
lists:reverse(Res).
-spec find_longest_field(list(), pos_integer()) -> non_neg_integer().
find_longest_field(Rows, ColumnNo) ->
lists:foldl(fun(Row, Longest) ->
erlang:max(Longest,
field_length(lists:nth(ColumnNo,Row)))
end, 0, Rows).
-spec max_widths([term()]) -> list(pos_integer()).
max_widths([Row]) ->
field_lengths(Row);
max_widths([Row1 | Rest]) ->
Row1Lengths = field_lengths(Row1),
lists:foldl(fun(Row, Acc) ->
Lengths = field_lengths(Row),
[max(A, B) || {A, B} <- lists:zip(Lengths, Acc)]
end, Row1Lengths, Rest).
-spec row(list(), list(string())) -> iolist().
row(Spec, Row0) ->
%% handle multiline fields
Rows = expand_row(Row0),
[
[ $| | lists:reverse(
["\n" | lists:foldl(fun({{_, Size}, Str}, Acc) ->
[align(Str, Size) | Acc]
end, [], lists:zip(Spec, Row))])] || Row <- Rows].
-spec titles(list()) -> iolist().
titles(Spec) ->
[ $| | lists:reverse(
["\n" | lists:foldl(fun({Title, Size}, TitleRow) ->
[align(Title, Size) | TitleRow]
end, [], Spec)])].
-spec align(string(), non_neg_integer()) -> iolist().
align(undefined, Size) ->
align("", Size);
align(Str, Size) when is_integer(Str) ->
align(integer_to_list(Str), Size);
align(Str, Size) when is_binary(Str) ->
align(unicode:characters_to_list(Str, utf8), Size);
align(Str, Size) when is_atom(Str) ->
align(atom_to_list(Str), Size);
align(Str, Size) when is_list(Str), length(Str) >= Size ->
Truncated = lists:sublist(Str, Size),
Truncated ++ "|";
align(Str, Size) when is_list(Str) ->
string:centre(Str, Size) ++ "|";
align(Term, Size) ->
Str = lists:flatten(io_lib:format("~p", [Term])),
align(Str, Size).
-spec vertical_border(list(tuple())) -> string().
vertical_border(Spec) ->
lists:reverse([$\n, [[char_seq(Length, $-), $+] ||
{_Name, Length} <- Spec], $+]).
-spec char_seq(non_neg_integer(), char()) -> string().
char_seq(Length, Char) ->
[Char || _ <- lists:seq(1, Length)].
field_lengths(Row) ->
[field_length(Field) || Field <- Row].
field_length(Field) when is_atom(Field) ->
field_length(atom_to_list(Field));
field_length(Field) when is_binary(Field) ->
field_length(unicode:characters_to_list(Field, utf8));
field_length(Field) when is_list(Field) ->
Lines = string:tokens(lists:flatten(Field), "\n"),
lists:foldl(fun(Line, Longest) ->
erlang:max(Longest,
length(Line))
end, 0, Lines);
field_length(Field) ->
field_length(io_lib:format("~p", [Field])).
expand_field(Field) when is_atom(Field) ->
expand_field(atom_to_list(Field));
expand_field(Field) when is_binary(Field) ->
expand_field(unicode:characters_to_list(Field, utf8));
expand_field(Field) when is_list(Field) ->
string:tokens(lists:flatten(Field), "\n");
expand_field(Field) ->
expand_field(io_lib:format("~p", [Field])).
expand_row(Row) ->
{ExpandedRow, MaxHeight} = lists:foldl(fun(Field, {Fields, Max}) ->
EF = expand_field(Field),
{[EF|Fields], erlang:max(Max, length(EF))}
end, {[], 0}, lists:reverse(Row)),
PaddedRow = [pad_field(Field, MaxHeight) || Field <- ExpandedRow],
[ [ lists:nth(N, Field) || Field <- PaddedRow]
|| N <- lists:seq(1, MaxHeight)].
pad_field(Field, MaxHeight) when length(Field) < MaxHeight ->
Field ++ ["" || _ <- lists:seq(1, MaxHeight - length(Field))];
pad_field(Field, _MaxHeight) ->
Field.