22
22
23
23
% % API
24
24
-export ([print /2 , print /3 ,
25
- create_table /2 , autosize_create_table /2 ]).
25
+ create_table /2 ,
26
+ autosize_create_table /2 , autosize_create_table /3 ]).
26
27
27
28
-include (" clique_status_types.hrl" ).
28
29
29
30
-define (MAX_LINE_LEN , 100 ).
31
+ -define (else , true ).
32
+ -define (MINWIDTH (W ),
33
+ if W =< 0 ->
34
+ 1 ;
35
+ ? else ->
36
+ W
37
+ end ).
30
38
31
39
-spec print (list (), list ()) -> ok .
32
40
print (_Spec , []) ->
@@ -48,13 +56,21 @@ print(Header, Spec, Rows) ->
48
56
49
57
-spec autosize_create_table ([any ()], [[any ()]]) -> iolist ().
50
58
autosize_create_table (Schema , Rows ) ->
59
+ autosize_create_table (Schema , Rows , []).
60
+
61
+ % % Currently the only constraint supported in the proplist is
62
+ % % `fixed_width' with a list of columns that *must not* be shrunk
63
+ % % (e.g., integer values). First column is 0.
64
+ -spec autosize_create_table ([any ()], [[any ()]], [tuple ()]) -> iolist ().
65
+ autosize_create_table (Schema , Rows , Constraints ) ->
51
66
BorderSize = 1 + length (hd (Rows )),
52
67
MaxLineLen = case io :columns () of
53
- % % Leaving an extra space seems to work better
54
- {ok , N } -> N - 1 ;
55
- {error , enotsup } -> ? MAX_LINE_LEN
56
- end ,
57
- Sizes = get_field_widths (MaxLineLen - BorderSize , [Schema | Rows ]),
68
+ % % Leaving an extra space seems to work better
69
+ {ok , N } -> N - 1 ;
70
+ {error , enotsup } -> ? MAX_LINE_LEN
71
+ end ,
72
+ Sizes = get_field_widths (MaxLineLen - BorderSize , [Schema | Rows ],
73
+ proplists :get_value (fixed_width , Constraints , [])),
58
74
Spec = lists :zip (Schema , Sizes ),
59
75
create_table (Spec , Rows , MaxLineLen , []).
60
76
@@ -81,54 +97,114 @@ create_table(Spec, [], _Length, IoList) ->
81
97
create_table (Spec , [Row | Rows ], Length , IoList ) ->
82
98
create_table (Spec , Rows , Length , [row (Spec , Row ) | IoList ]).
83
99
84
- -spec get_field_widths (pos_integer (), [term ()]) -> [pos_integer ()].
85
- get_field_widths (MaxLineLen , Rows ) ->
100
+ % % Measure and shrink table width as necessary to fit the console
101
+ -spec get_field_widths (pos_integer (), [term ()], [non_neg_integer ()]) -> [non_neg_integer ()].
102
+ get_field_widths (MaxLineLen , Rows , Unshrinkable ) ->
86
103
Widths = max_widths (Rows ),
87
- resize_row (MaxLineLen , Widths ).
104
+ fit_widths_to_terminal (MaxLineLen , Widths , Unshrinkable ).
88
105
89
- -spec resize_row (pos_integer (), [pos_integer ()]) -> [pos_integer ()].
90
- resize_row (MaxLength , Widths ) ->
106
+ fit_widths_to_terminal (MaxWidth , Widths , Unshrinkable ) ->
91
107
Sum = lists :sum (Widths ),
92
- case Sum > MaxLength of
93
- true ->
94
- resize_items (Sum , MaxLength , Widths );
95
- false ->
96
- Widths
97
- end .
98
-
99
- -spec resize_items (pos_integer (), pos_integer (), [pos_integer ()]) ->
100
- [pos_integer ()].
101
- resize_items (Sum , MaxLength , Widths ) ->
102
- Diff = Sum - MaxLength ,
103
- NumColumns = length (Widths ),
104
- case NumColumns > Diff of
105
- true ->
106
- Remaining = NumColumns - Diff ,
107
- reduce_widths (1 , Remaining , Widths );
108
- false ->
109
- PerColumn = Diff div NumColumns + 1 ,
110
- Remaining = Diff - NumColumns ,
111
- reduce_widths (PerColumn , Remaining , Widths )
112
- end .
113
- -spec reduce_widths (pos_integer (), pos_integer (), [pos_integer ()]) ->
114
- [pos_integer ()].
115
- reduce_widths (PerColumn , Total , Widths ) ->
116
- % % Just subtract one character from each column until we run out.
108
+ Weights = calculate_field_weights (Sum , Widths , Unshrinkable ),
109
+ MustRemove = Sum - MaxWidth ,
110
+ calculate_new_widths (MaxWidth , MustRemove , Widths , Weights ).
111
+
112
+ % % Determine field weighting as proportion of total width of the
113
+ % % table. Fields which were flagged as unshrinkable will be given a
114
+ % % weight of 0.
115
+ -spec calculate_field_weights (pos_integer (), list (pos_integer ()),
116
+ list (non_neg_integer ())) ->
117
+ list (number ()).
118
+ calculate_field_weights (Sum , Widths , []) ->
119
+ % % If no fields are constrained as unshrinkable, simply divide
120
+ % % each width by the sum of all widths for our proportions
121
+ lists :map (fun (X ) -> X / Sum end , Widths );
122
+ calculate_field_weights (_Sum , Widths , Unshrinkable ) ->
123
+ TaggedWidths = flag_unshrinkable_widths (Widths , Unshrinkable ),
124
+ ShrinkableWidth = lists :sum (lists :filter (fun ({_X , noshrink }) -> false ;
125
+ (_X ) -> true end ,
126
+ TaggedWidths )),
127
+ lists :map (fun ({_X , noshrink }) -> 0 ;
128
+ (X ) -> X / ShrinkableWidth end ,
129
+ TaggedWidths ).
130
+
131
+ % % Takes a list of column widths and a list of (zero-based) index
132
+ % % values of the columns that must not shrink. Returns a mixed list of
133
+ % % widths and `noshrink' tuples.
134
+ flag_unshrinkable_widths (Widths , NoShrink ) ->
117
135
{_ , NewWidths } =
118
- lists :foldl (fun (Width , {Remaining , NewWidths }) ->
119
- case Remaining of
120
- 0 ->
121
- {0 , [Width | NewWidths ]};
122
- _ ->
123
- Rem = Remaining - PerColumn ,
124
- {Rem , [Width - PerColumn | NewWidths ]}
125
- end
126
- end , {Total , []}, Widths ),
136
+ lists :foldl (fun (X , {Idx , Mapped }) ->
137
+ case lists :member (Idx , NoShrink ) of
138
+ true ->
139
+ {Idx + 1 , [{X , noshrink }|Mapped ]};
140
+ false ->
141
+ {Idx + 1 , [X |Mapped ]}
142
+ end
143
+ end , {0 , []}, Widths ),
127
144
lists :reverse (NewWidths ).
128
145
146
+ % % Calculate the proportional weight for each column for shrinking.
147
+ % % Zip the results into a `{Width, Weight, Index}' tuple list.
148
+ column_zip (Widths , Weights , ToNarrow ) ->
149
+ column_zip (Widths , Weights , ToNarrow , 0 , []).
150
+
151
+ column_zip ([], [], _ToNarrow , _Index , Accum ) ->
152
+ lists :reverse (Accum );
153
+ column_zip ([Width |Widths ], [Weight |Weights ], ToNarrow , Index , Accum ) ->
154
+ NewWidth = ? MINWIDTH (Width - round (ToNarrow * Weight )),
155
+ column_zip (Widths , Weights , ToNarrow , Index + 1 ,
156
+ [{NewWidth , Weight , Index }] ++ Accum ).
157
+
158
+ % % Given the widths based on data to be displayed, return widths
159
+ % % necessary to narrow the table to fit the console.
160
+ calculate_new_widths (_Max , ToNarrow , Widths , _Weights ) when ToNarrow =< 0 ->
161
+ % % Console is wide enough, no need to narrow
162
+ Widths ;
163
+ calculate_new_widths (MaxWidth , ToNarrow , Widths , Weights ) ->
164
+ fix_rounding (MaxWidth , column_zip (Widths , Weights , ToNarrow )).
165
+
166
+ % % Rounding may introduce an error. If so, remove the requisite number
167
+ % % of spaces from the widest field
168
+ fix_rounding (Target , Cols ) ->
169
+ Widths = lists :map (fun ({Width , _Weight , _Idx }) -> Width end ,
170
+ Cols ),
171
+ SumWidths = lists :sum (Widths ),
172
+ shrink_widest (Target , SumWidths , Widths , Cols ).
173
+
174
+ % % Determine whether our target table width is wider than the terminal
175
+ % % due to any rounding error and find columns eligible to be shrunk.
176
+ shrink_widest (Target , Current , Widths , _Cols ) when Target =< Current ->
177
+ Widths ;
178
+ shrink_widest (Target , Current , Widths , Cols ) ->
179
+ Gap = Current - Target ,
180
+ NonZeroWeighted = lists :dropwhile (fun ({_Width , 0 , _Idx }) -> true ;
181
+ (_ ) -> false end ,
182
+ Cols ),
183
+ shrink_widest_weighted (Gap , NonZeroWeighted , Widths ).
184
+
185
+ % % Take the widest column with a non-zero weight and reduce it by the
186
+ % % amount necessary to compensate for any rounding error.
187
+ shrink_widest_weighted (_Gap , [], Widths ) ->
188
+ Widths ; % % All columns constrained to fixed widths, nothing we can do
189
+ shrink_widest_weighted (Gap , Cols , Widths ) ->
190
+ SortedCols = lists :sort (
191
+ fun ({WidthA , _WeightA , _IdxA }, {WidthB , _WeightB , _IdxB }) ->
192
+ WidthA > WidthB
193
+ end , Cols ),
194
+ {OldWidth , _Weight , Idx } = hd (SortedCols ),
195
+ NewWidth = ? MINWIDTH (OldWidth - Gap ),
196
+ replace_list_element (Idx , NewWidth , Widths ).
197
+
198
+ % % Replace the item at `Index' in `List' with `Element'.
199
+ % % Zero-based indexing.
200
+ -spec replace_list_element (non_neg_integer (), term (), list ()) -> list ().
201
+ replace_list_element (Index , Element , List ) ->
202
+ {Prefix , Suffix } = lists :split (Index , List ),
203
+ Prefix ++ [Element ] ++ tl (Suffix ).
204
+
129
205
get_row_length (Spec , Rows ) ->
130
206
Res = lists :foldl (fun ({_Name , MinSize }, Total ) ->
131
- Longest = find_longest_field (Rows , length (Total )+ 1 ),
207
+ Longest = find_longest_field (Rows , length (Total )+ 1 ),
132
208
Size = erlang :max (MinSize , Longest ),
133
209
[Size | Total ]
134
210
end , [], Spec ),
@@ -137,19 +213,19 @@ get_row_length(Spec, Rows) ->
137
213
-spec find_longest_field (list (), pos_integer ()) -> non_neg_integer ().
138
214
find_longest_field (Rows , ColumnNo ) ->
139
215
lists :foldl (fun (Row , Longest ) ->
140
- erlang :max (Longest ,
141
- field_length (lists :nth (ColumnNo ,Row )))
142
- end , 0 , Rows ).
216
+ erlang :max (Longest ,
217
+ field_length (lists :nth (ColumnNo ,Row )))
218
+ end , 0 , Rows ).
143
219
144
220
-spec max_widths ([term ()]) -> list (pos_integer ()).
145
221
max_widths ([Row ]) ->
146
222
field_lengths (Row );
147
223
max_widths ([Row1 | Rest ]) ->
148
224
Row1Lengths = field_lengths (Row1 ),
149
225
lists :foldl (fun (Row , Acc ) ->
150
- Lengths = field_lengths (Row ),
151
- [max (A , B ) || {A , B } <- lists :zip (Lengths , Acc )]
152
- end , Row1Lengths , Rest ).
226
+ Lengths = field_lengths (Row ),
227
+ [max (A , B ) || {A , B } <- lists :zip (Lengths , Acc )]
228
+ end , Row1Lengths , Rest ).
153
229
154
230
-spec row (list (), list (string ())) -> iolist ().
155
231
row (Spec , Row0 ) ->
0 commit comments