fat_core 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/TODO.org +0 -19
- data/lib/fat_core/string.rb +23 -11
- data/lib/fat_core/version.rb +3 -3
- data/lib/fat_core.rb +0 -4
- data/spec/lib/string_spec.rb +41 -37
- metadata +2 -33
- data/lib/fat_core/column.rb +0 -345
- data/lib/fat_core/evaluator.rb +0 -43
- data/lib/fat_core/formatters/aoa_formatter.rb +0 -84
- data/lib/fat_core/formatters/aoh_formatter.rb +0 -82
- data/lib/fat_core/formatters/formatter.rb +0 -973
- data/lib/fat_core/formatters/org_formatter.rb +0 -72
- data/lib/fat_core/formatters/text_formatter.rb +0 -91
- data/lib/fat_core/formatters.rb +0 -5
- data/lib/fat_core/table.rb +0 -988
- data/spec/example_files/datawatch.org +0 -471
- data/spec/example_files/goldberg.org +0 -199
- data/spec/example_files/wpcs.csv +0 -92
- data/spec/lib/column_spec.rb +0 -224
- data/spec/lib/evaluator_spec.rb +0 -34
- data/spec/lib/formatters/aoa_formatter_spec.rb +0 -62
- data/spec/lib/formatters/aoh_formatter_spec.rb +0 -61
- data/spec/lib/formatters/formatter_spec.rb +0 -371
- data/spec/lib/formatters/org_formatter_spec.rb +0 -60
- data/spec/lib/formatters/text_formatter_spec.rb +0 -60
- data/spec/lib/table_spec.rb +0 -990
@@ -1,973 +0,0 @@
|
|
1
|
-
module FatCore
|
2
|
-
## A formatter is for use in Table output routines, and provides instructions
|
3
|
-
## for how the table ought to be formatted. The goal is to make subclasses of
|
4
|
-
## this class to handle different output targets, such as aoa for org tables,
|
5
|
-
## ansi terminals, LaTeX, html, plain text, org mode table text, and so forth.
|
6
|
-
## Many of the formatting options, such as color, will be no-ops for some
|
7
|
-
## output targets, such as text, but will be valid nonetheless. Thus, the
|
8
|
-
## Formatter subclass should provide the best implementation for each
|
9
|
-
## formatting request available for the target. This base class will consist
|
10
|
-
## largely of stub methods with implementations provided by the subclass.
|
11
|
-
class Formatter
|
12
|
-
LOCATIONS = [:header, :body, :bfirst, :gfirst, :gfooter, :footer].freeze
|
13
|
-
|
14
|
-
attr_reader :table, :format_at, :footers, :gfooters
|
15
|
-
|
16
|
-
class_attribute :default_format
|
17
|
-
self.default_format = {
|
18
|
-
nil_text: '',
|
19
|
-
case: :none,
|
20
|
-
alignment: :left,
|
21
|
-
bold: false,
|
22
|
-
italic: false,
|
23
|
-
color: 'black',
|
24
|
-
hms: false,
|
25
|
-
pre_digits: -1,
|
26
|
-
post_digits: -1,
|
27
|
-
commas: false,
|
28
|
-
currency: false,
|
29
|
-
datetime_fmt: '%F %H:%M:%S',
|
30
|
-
date_fmt: '%F',
|
31
|
-
true_text: 'T',
|
32
|
-
false_text: 'F',
|
33
|
-
true_color: 'black',
|
34
|
-
false_color: 'black'
|
35
|
-
}
|
36
|
-
|
37
|
-
class_attribute :currency_symbol
|
38
|
-
self.currency_symbol = '$'
|
39
|
-
|
40
|
-
# def self.currency_symbol=(char)
|
41
|
-
# @@currency_symbol = char.to_s
|
42
|
-
# end
|
43
|
-
|
44
|
-
# def self.currency_symbol
|
45
|
-
# @@currency_symbol
|
46
|
-
# end
|
47
|
-
|
48
|
-
# A Formatter can specify a hash to hold the formatting instructions for
|
49
|
-
# columns by using the column head as a key and the value as the format
|
50
|
-
# instructions. In addition, the keys, :numeric, :string, :datetime,
|
51
|
-
# :boolean, and :nil, can be used to specify the default format instructions
|
52
|
-
# for columns of the given type is no other instructions have been given.
|
53
|
-
#
|
54
|
-
# Formatting instructions are strings, and what are valid strings depend on
|
55
|
-
# the type of the column:
|
56
|
-
#
|
57
|
-
# - string :: for string columns, the following instructions are valid:
|
58
|
-
# + u :: convert the element to all lowercase,
|
59
|
-
# + U :: convert the element to all uppercase,
|
60
|
-
# + t :: title case the element, that is, upcase the initial letter in
|
61
|
-
# each word and lower case the other letters
|
62
|
-
# + B :: make the element bold
|
63
|
-
# + I :: make the element italic
|
64
|
-
# + R :: align the element on the right of the column
|
65
|
-
# + L :: align the element on the left of the column
|
66
|
-
# + C :: align the element in the center of the column
|
67
|
-
# + c[color] :: render the element in the given color
|
68
|
-
# - numeric :: for a numeric, all the instructions valid for string are
|
69
|
-
# available, in addition to the following:
|
70
|
-
# + , :: insert grouping commas,
|
71
|
-
# + $ :: format the number as currency according to the locale,
|
72
|
-
# + m.n :: include at least m digits before the decimal point, padding on
|
73
|
-
# the left with zeroes as needed, and round the number to the n
|
74
|
-
# decimal places and include n digits after the decimal point,
|
75
|
-
# padding on the right with zeroes as needed,
|
76
|
-
# + H :: convert the number (assumed to be in units of seconds) to
|
77
|
-
# HH:MM:SS.ss form. So a column that is the result of subtracting
|
78
|
-
# two :datetime forms will result in a :numeric expressed as seconds
|
79
|
-
# and can be displayed in hours, minutes, and seconds with this
|
80
|
-
# formatting instruction.
|
81
|
-
# - datetime :: for a datetime, all the instructions valid for string are
|
82
|
-
# available, in addition to the following:
|
83
|
-
# + d[fmt] :: apply the format to a datetime that has no or zero hour,
|
84
|
-
# minute, second components, where fmt is a valid format string for
|
85
|
-
# Date#strftime, otherwise, the datetime will be formatted as an ISO
|
86
|
-
# 8601 string, YYYY-MM-DD.
|
87
|
-
# + D[fmt] :: apply the format to a datetime that has at least a non-zero
|
88
|
-
# hour component where fmt is a valid format string for
|
89
|
-
# Date#strftime, otherwise, the datetime will be formatted as an ISO
|
90
|
-
# 8601 string, YYYY-MM-DD.
|
91
|
-
# - boolean :: all the instructions valid for string are available, in
|
92
|
-
# addition to the following:
|
93
|
-
# + Y :: print true as 'Y' and false as 'N',
|
94
|
-
# + T :: print true as 'T' and false as 'F',
|
95
|
-
# + X :: print true as 'X' and false as '',
|
96
|
-
# + b[xxx,yyy] :: print true as the string given as xxx and false as the
|
97
|
-
# string given as yyy,
|
98
|
-
# + c[tcolor,fcolor] :: color a true element with tcolor and a false
|
99
|
-
# element with fcolor.
|
100
|
-
# - nil :: by default, nil elements are rendered as blank cells, but you can
|
101
|
-
# make them visible with the following, and in that case, all the
|
102
|
-
# formatting instructions valid for strings are available:
|
103
|
-
# + n[niltext] :: render a nil item with the given text.
|
104
|
-
#
|
105
|
-
# In the foregoing, the earlier elements in each list will be available for
|
106
|
-
# all formatter subclasses, while the later elements may or may not have any
|
107
|
-
# effect on the output.
|
108
|
-
#
|
109
|
-
# The hashes that can be specified to the formatter determine the formatting
|
110
|
-
# instructions for different parts of the output table:
|
111
|
-
#
|
112
|
-
# - header: :: instructions for the headers of the table,
|
113
|
-
# - bfirst :: instructions for the first row in the body of the table,
|
114
|
-
# - gfirst :: instructions for the cells in the first row of a group,
|
115
|
-
# - body :: instructions for the cells in the body of the table, to the
|
116
|
-
# extent they are not governed by bfirst or gfirst.
|
117
|
-
# - gfooter :: instructions for the cells of a group footer, and
|
118
|
-
# - footer :: instructions for the cells of a footer.
|
119
|
-
#
|
120
|
-
def initialize(table = Table.new)
|
121
|
-
unless table && table.is_a?(Table)
|
122
|
-
raise ArgumentError, 'must initialize Formatter with a Table'
|
123
|
-
end
|
124
|
-
@table = table
|
125
|
-
@footers = {}
|
126
|
-
@gfooters = {}
|
127
|
-
# Formatting instructions for various "locations" within the Table, as
|
128
|
-
# a hash of hashes. The outer hash is keyed on the location, and each
|
129
|
-
# inner hash is keyed on either a column sym or a type sym, :string, :numeric,
|
130
|
-
# :datetime, :boolean, or :nil. The value of the inner hashes are
|
131
|
-
# OpenStruct structs.
|
132
|
-
@format_at = {}
|
133
|
-
[:header, :bfirst, :gfirst, :body, :footer, :gfooter].each do |loc|
|
134
|
-
@format_at[loc] = {}
|
135
|
-
table.headers.each do |h|
|
136
|
-
format_at[loc][h] = OpenStruct.new(self.class.default_format)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
yield self if block_given?
|
140
|
-
end
|
141
|
-
|
142
|
-
############################################################################
|
143
|
-
# Footer methods
|
144
|
-
#
|
145
|
-
#
|
146
|
-
# A Table may have any number of footers and any number of group footers.
|
147
|
-
# Footers are not part of the table's data and never participate in any of
|
148
|
-
# the transformation methods on tables. They are never inherited by output
|
149
|
-
# tables from input tables in any of the transformation methods.
|
150
|
-
#
|
151
|
-
# When output, a table footer will appear at the bottom of the table, and a
|
152
|
-
# group footer will appear at the bottom of each group.
|
153
|
-
#
|
154
|
-
# Each footer must have a label, usually a string such as 'Total', to
|
155
|
-
# identify the purpose of the footer, and the label must be distinct among
|
156
|
-
# all footers of the same type. That is you may have a table footer labeled
|
157
|
-
# 'Total' and a group footer labeled 'Total', but you may not have two table
|
158
|
-
# footers with that label. If the first column of the table is not included
|
159
|
-
# in the footer, the footer's label will be placed there, otherwise, there
|
160
|
-
# will be no label output. The footers are accessible with the #footers
|
161
|
-
# method, which returns a hash indexed by the label converted to a symbol.
|
162
|
-
# The symbol is reconverted to a title-cased string on output.
|
163
|
-
#
|
164
|
-
# Note that by adding footers or gfooters to the table, you are only stating
|
165
|
-
# what footers you want on output of the table. No actual calculation is
|
166
|
-
# performed until the table is output.
|
167
|
-
#
|
168
|
-
############################################################################
|
169
|
-
|
170
|
-
public
|
171
|
-
|
172
|
-
# Add a table footer to the table with a label given in the first parameter,
|
173
|
-
# defaulting to 'Total'. After the label, you can given any number of
|
174
|
-
# headers (as symbols) for columns to be summed, and then any number of hash
|
175
|
-
# parameters for columns for with to apply an aggregate other than :sum.
|
176
|
-
# For example, these are valid footer definitions.
|
177
|
-
#
|
178
|
-
# # Just sum the shares column with a label of 'Total'
|
179
|
-
# tab.footer(:shares)
|
180
|
-
#
|
181
|
-
# # Change the label and sum the :price column as well
|
182
|
-
# tab.footer('Grand Total', :shares, :price)
|
183
|
-
#
|
184
|
-
# # Average then show standard deviation of several columns
|
185
|
-
# tab.footer.('Average', date: avg, shares: :avg, price: avg)
|
186
|
-
# tab.footer.('Sigma', date: dev, shares: :dev, price: :dev)
|
187
|
-
#
|
188
|
-
# # Do some sums and some other aggregates: sum shares, average date and
|
189
|
-
# # price.
|
190
|
-
# tab.footer.('Summary', :shares, date: avg, price: avg)
|
191
|
-
def footer(label, *sum_cols, **agg_cols)
|
192
|
-
label = label.to_s
|
193
|
-
foot = {}
|
194
|
-
sum_cols.each do |h|
|
195
|
-
unless table.headers.include?(h)
|
196
|
-
raise "No '#{h}' column in table to sum in the footer"
|
197
|
-
end
|
198
|
-
foot[h] = :sum
|
199
|
-
end
|
200
|
-
agg_cols.each do |h, agg|
|
201
|
-
unless table.headers.include?(h)
|
202
|
-
raise "No '#{h}' column in table to #{aggregate} in the footer"
|
203
|
-
end
|
204
|
-
foot[h] = agg
|
205
|
-
end
|
206
|
-
@footers[label] = foot
|
207
|
-
self
|
208
|
-
end
|
209
|
-
|
210
|
-
def gfooter(label, *sum_cols, **agg_cols)
|
211
|
-
label = label.to_s
|
212
|
-
foot = {}
|
213
|
-
sum_cols.each do |h|
|
214
|
-
unless table.headers.include?(h)
|
215
|
-
raise "No '#{h}' column in table to sum in the group footer"
|
216
|
-
end
|
217
|
-
foot[h] = :sum
|
218
|
-
end
|
219
|
-
agg_cols.each do |h, agg|
|
220
|
-
unless table.headers.include?(h)
|
221
|
-
raise "No '#{h}' column in table to #{aggregate} in the group footer"
|
222
|
-
end
|
223
|
-
foot[h] = agg
|
224
|
-
end
|
225
|
-
@gfooters[label] = foot
|
226
|
-
self
|
227
|
-
end
|
228
|
-
|
229
|
-
def sum_footer(*cols)
|
230
|
-
footer('Total', *cols)
|
231
|
-
end
|
232
|
-
|
233
|
-
def sum_gfooter(*cols)
|
234
|
-
gfooter('Group Total', *cols)
|
235
|
-
end
|
236
|
-
|
237
|
-
def avg_footer(*cols)
|
238
|
-
hsh = {}
|
239
|
-
cols.each do |c|
|
240
|
-
hsh[c] = :avg
|
241
|
-
end
|
242
|
-
footer('Average', hsh)
|
243
|
-
end
|
244
|
-
|
245
|
-
def avg_gfooter(*cols)
|
246
|
-
hsh = {}
|
247
|
-
cols.each do |c|
|
248
|
-
hsh[c] = :avg
|
249
|
-
end
|
250
|
-
gfooter('Group Average', hsh)
|
251
|
-
end
|
252
|
-
|
253
|
-
def min_footer(*cols)
|
254
|
-
hsh = {}
|
255
|
-
cols.each do |c|
|
256
|
-
hsh[c] = :min
|
257
|
-
end
|
258
|
-
footer('Minimum', hsh)
|
259
|
-
end
|
260
|
-
|
261
|
-
def min_gfooter(*cols)
|
262
|
-
hsh = {}
|
263
|
-
cols.each do |c|
|
264
|
-
hsh[c] = :min
|
265
|
-
end
|
266
|
-
gfooter('Group Minimum', hsh)
|
267
|
-
end
|
268
|
-
|
269
|
-
def max_footer(*cols)
|
270
|
-
hsh = {}
|
271
|
-
cols.each do |c|
|
272
|
-
hsh[c] = :max
|
273
|
-
end
|
274
|
-
footer('Maximum', hsh)
|
275
|
-
end
|
276
|
-
|
277
|
-
def max_gfooter(*cols)
|
278
|
-
hsh = {}
|
279
|
-
cols.each do |c|
|
280
|
-
hsh[c] = :max
|
281
|
-
end
|
282
|
-
gfooter('Group Maximum', hsh)
|
283
|
-
end
|
284
|
-
|
285
|
-
############################################################################
|
286
|
-
# Formatting methods
|
287
|
-
############################################################################
|
288
|
-
|
289
|
-
# Define formats for all locations
|
290
|
-
def format(**fmts)
|
291
|
-
[:header, :bfirst, :gfirst, :body, :footer, :gfooter].each do |loc|
|
292
|
-
format_for(loc, fmts)
|
293
|
-
end
|
294
|
-
end
|
295
|
-
|
296
|
-
# Define a format for the given location, :header, :body, :footer, :gfooter
|
297
|
-
# (the group footers), :bfirst (the first row in the table body), or :gfirst
|
298
|
-
# (the first rows in group bodies). Formats are specified with hash
|
299
|
-
# arguments where the keys are either (1) the name of a table column in
|
300
|
-
# symbol form, or (2) the name of a column type, i.e., :string, :numeric, or
|
301
|
-
# :datetime, :boolean, or :nil (for empty cells or untyped columns). The
|
302
|
-
# value given for the hash arguments should be strings that contain
|
303
|
-
# "instructions" on how elements of that column, or that type are to be
|
304
|
-
# formatted on output. Formatting instructions for a column name take
|
305
|
-
# precedence over those specified by type. And more specific locations take
|
306
|
-
# precedence over less specific ones. For example, the first line of a table
|
307
|
-
# is part of :body, :gfirst, and :bfirst, but since its identity as the
|
308
|
-
# first row of the table is the most specific (there is only one of those,
|
309
|
-
# there may be many rows that qualify as :gfirst, and even more that qualify
|
310
|
-
# as :body rows). For purposes of formatting, all headers are considered of
|
311
|
-
# the :string type. All empty cells are considered to be of the :nilclass
|
312
|
-
# type. All other cells have the type of the column to which they belong,
|
313
|
-
# including all cells in group or table footers.
|
314
|
-
def format_for(location, **fmts)
|
315
|
-
unless LOCATIONS.include?(location)
|
316
|
-
raise ArgumentError, "unknown format location '#{location}'"
|
317
|
-
end
|
318
|
-
valid_keys = table.headers + [:string, :numeric, :datetime, :boolean, :nil]
|
319
|
-
invalid_keys = (fmts.keys - valid_keys).uniq
|
320
|
-
unless invalid_keys.empty?
|
321
|
-
msg = "invalid #{location} column or type: #{invalid_keys.join(',')}"
|
322
|
-
raise ArgumentError, msg
|
323
|
-
end
|
324
|
-
@format_at[location] ||= {}
|
325
|
-
table.headers.each do |h|
|
326
|
-
# Default formatting hash
|
327
|
-
format_h = default_format.dup
|
328
|
-
|
329
|
-
# Merge in type-based formatting
|
330
|
-
typ = table.type(h).as_sym
|
331
|
-
parse_typ_method_name = 'parse_' + typ.to_s + '_fmt'
|
332
|
-
if location == :header
|
333
|
-
# Treat header as string type
|
334
|
-
if fmts.keys.include?(:string)
|
335
|
-
str_fmt = parse_string_fmt(fmts[:string])
|
336
|
-
format_h = format_h.merge(str_fmt)
|
337
|
-
end
|
338
|
-
else
|
339
|
-
# Use column type for other locations
|
340
|
-
if fmts.keys.include?(typ)
|
341
|
-
typ_fmt = send(parse_typ_method_name, fmts[typ])
|
342
|
-
format_h = format_h.merge(typ_fmt)
|
343
|
-
end
|
344
|
-
if fmts.keys.include?(:string)
|
345
|
-
typ_fmt = parse_string_fmt(fmts[:string])
|
346
|
-
format_h = format_h.merge(typ_fmt)
|
347
|
-
end
|
348
|
-
if fmts.keys.include?(:nil)
|
349
|
-
typ_fmt = parse_nil_fmt(fmts[:nil]).first
|
350
|
-
format_h = format_h.merge(typ_fmt)
|
351
|
-
end
|
352
|
-
end
|
353
|
-
|
354
|
-
# Merge in column-based formatting
|
355
|
-
if fmts[h]
|
356
|
-
col_fmt = send(parse_typ_method_name, fmts[h])
|
357
|
-
format_h = format_h.merge(col_fmt)
|
358
|
-
end
|
359
|
-
|
360
|
-
# Convert to struct
|
361
|
-
format_at[location][h] = OpenStruct.new(format_h)
|
362
|
-
end
|
363
|
-
self
|
364
|
-
end
|
365
|
-
|
366
|
-
###############################################################################
|
367
|
-
# Parsing and validation routines
|
368
|
-
###############################################################################
|
369
|
-
|
370
|
-
private
|
371
|
-
|
372
|
-
# Return a hash that reflects the formatting instructions given in the
|
373
|
-
# string fmt. Raise an error if it contains invalid formatting instructions.
|
374
|
-
# If fmt contains conflicting instructions, say C and L, there is no
|
375
|
-
# guarantee which will win, but it will not be considered an error to do so.
|
376
|
-
def parse_string_fmt(fmt)
|
377
|
-
format, fmt = parse_str_fmt(fmt)
|
378
|
-
unless fmt.blank?
|
379
|
-
raise ArgumentError, "unrecognized string formatting instructions '#{fmt}'"
|
380
|
-
end
|
381
|
-
format
|
382
|
-
end
|
383
|
-
|
384
|
-
# Utility method that extracts string instructions and returns a hash for
|
385
|
-
# of the instructions and the unconsumed part of the instruction string.
|
386
|
-
# This is called to cull string-based instructions from a formatting string
|
387
|
-
# intended for other types, such as numeric, etc.
|
388
|
-
def parse_str_fmt(fmt)
|
389
|
-
# We parse the more complex formatting constructs first, and after each
|
390
|
-
# parse, we remove the matched construct from fmt. At the end, any
|
391
|
-
# remaining characters in fmt should be invalid.
|
392
|
-
fmt_hash = {}
|
393
|
-
if fmt =~ /c\[([-_a-zA-Z]+)\]/
|
394
|
-
fmt_hash[:color] = $1
|
395
|
-
fmt = fmt.sub($&, '')
|
396
|
-
end
|
397
|
-
if fmt =~ /u/
|
398
|
-
fmt_hash[:case] = :lower
|
399
|
-
fmt = fmt.sub($&, '')
|
400
|
-
end
|
401
|
-
if fmt =~ /U/
|
402
|
-
fmt_hash[:case] = :upper
|
403
|
-
fmt = fmt.sub($&, '')
|
404
|
-
end
|
405
|
-
if fmt =~ /t/
|
406
|
-
fmt_hash[:case] = :title
|
407
|
-
fmt = fmt.sub($&, '')
|
408
|
-
end
|
409
|
-
if fmt =~ /B/
|
410
|
-
fmt_hash[:bold] = true
|
411
|
-
fmt = fmt.sub($&, '')
|
412
|
-
end
|
413
|
-
if fmt =~ /I/
|
414
|
-
fmt_hash[:italic] = true
|
415
|
-
fmt = fmt.sub($&, '')
|
416
|
-
end
|
417
|
-
if fmt =~ /R/
|
418
|
-
fmt_hash[:alignment] = :right
|
419
|
-
fmt = fmt.sub($&, '')
|
420
|
-
end
|
421
|
-
if fmt =~ /C/
|
422
|
-
fmt_hash[:alignment] = :center
|
423
|
-
fmt = fmt.sub($&, '')
|
424
|
-
end
|
425
|
-
if fmt =~ /L/
|
426
|
-
fmt_hash[:alignment] = :left
|
427
|
-
fmt = fmt.sub($&, '')
|
428
|
-
end
|
429
|
-
[fmt_hash, fmt]
|
430
|
-
end
|
431
|
-
|
432
|
-
# Utility method that extracts nil instructions and returns a hash of the
|
433
|
-
# instructions and the unconsumed part of the instruction string. This is
|
434
|
-
# called to cull nil-based instructions from a formatting string intended
|
435
|
-
# for other types, such as numeric, etc.
|
436
|
-
def parse_nil_fmt(fmt)
|
437
|
-
# We parse the more complex formatting constructs first, and after each
|
438
|
-
# parse, we remove the matched construct from fmt. At the end, any
|
439
|
-
# remaining characters in fmt should be invalid.
|
440
|
-
fmt_hash = {}
|
441
|
-
if fmt =~ /n\[\s*([^\]]*)\s*\]/
|
442
|
-
fmt_hash[:nil_text] = $1.clean
|
443
|
-
fmt = fmt.sub($&, '')
|
444
|
-
end
|
445
|
-
[fmt_hash, fmt]
|
446
|
-
end
|
447
|
-
|
448
|
-
# Return a hash that reflects the numeric or string formatting instructions
|
449
|
-
# given in the string fmt. Raise an error if it contains invalid formatting
|
450
|
-
# instructions. If fmt contains conflicting instructions, there is no
|
451
|
-
# guarantee which will win, but it will not be considered an error to do so.
|
452
|
-
def parse_numeric_fmt(fmt)
|
453
|
-
# We parse the more complex formatting constructs first, and after each
|
454
|
-
# parse, we remove the matched construct from fmt. At the end, any
|
455
|
-
# remaining characters in fmt should be invalid.
|
456
|
-
fmt_hash, fmt = parse_str_fmt(fmt)
|
457
|
-
fmt = fmt.gsub(/\s+/, '')
|
458
|
-
if fmt =~ /(\d+).(\d+)/
|
459
|
-
fmt_hash[:pre_digits] = $1.to_i
|
460
|
-
fmt_hash[:post_digits] = $2.to_i
|
461
|
-
fmt = fmt.sub($&, '')
|
462
|
-
end
|
463
|
-
if fmt =~ /,/
|
464
|
-
fmt_hash[:commas] = true
|
465
|
-
fmt = fmt.sub($&, '')
|
466
|
-
end
|
467
|
-
if fmt =~ /\$/
|
468
|
-
fmt_hash[:currency] = true
|
469
|
-
fmt = fmt.sub($&, '')
|
470
|
-
end
|
471
|
-
if fmt =~ /H/
|
472
|
-
fmt_hash[:hms] = true
|
473
|
-
fmt = fmt.sub($&, '')
|
474
|
-
end
|
475
|
-
unless fmt.blank?
|
476
|
-
raise ArgumentError, "unrecognized numeric formatting instructions '#{fmt}'"
|
477
|
-
end
|
478
|
-
fmt_hash
|
479
|
-
end
|
480
|
-
|
481
|
-
# Return a hash that reflects the datetime or string formatting instructions
|
482
|
-
# given in the string fmt. Raise an error if it contains invalid formatting
|
483
|
-
# instructions. If fmt contains conflicting instructions, there is no
|
484
|
-
# guarantee which will win, but it will not be considered an error to do so.
|
485
|
-
def parse_datetime_fmt(fmt)
|
486
|
-
# We parse the more complex formatting constructs first, and after each
|
487
|
-
# parse, we remove the matched construct from fmt. At the end, any
|
488
|
-
# remaining characters in fmt should be invalid.
|
489
|
-
fmt_hash, fmt = parse_str_fmt(fmt)
|
490
|
-
fmt = fmt.gsub(/\s+/, '')
|
491
|
-
if fmt =~ /d\[([^\]]*)\]/
|
492
|
-
fmt_hash[:date_fmt] = $1
|
493
|
-
fmt = fmt.sub($&, '')
|
494
|
-
end
|
495
|
-
if fmt =~ /D\[([^\]]*)\]/
|
496
|
-
fmt_hash[:date_fmt] = $1
|
497
|
-
fmt = fmt.sub($&, '')
|
498
|
-
end
|
499
|
-
unless fmt.blank?
|
500
|
-
raise ArgumentError, "unrecognized datetime formatting instructions '#{fmt}'"
|
501
|
-
end
|
502
|
-
fmt_hash
|
503
|
-
end
|
504
|
-
|
505
|
-
# Return a hash that reflects the boolean or string formatting instructions
|
506
|
-
# given in the string fmt. Raise an error if it contains invalid formatting
|
507
|
-
# instructions. If fmt contains conflicting instructions, there is no
|
508
|
-
# guarantee which will win, but it will not be considered an error to do so.
|
509
|
-
def parse_boolean_fmt(fmt)
|
510
|
-
# We parse the more complex formatting constructs first, and after each
|
511
|
-
# parse, we remove the matched construct from fmt. At the end, any
|
512
|
-
# remaining characters in fmt should be invalid.
|
513
|
-
fmt_hash, fmt = parse_str_fmt(fmt)
|
514
|
-
if fmt =~ /b\[\s*([^\],]*),([^\]]*)\s*\]/
|
515
|
-
fmt_hash[:true_text] = $1.clean
|
516
|
-
fmt_hash[:false_text] = $2.clean
|
517
|
-
fmt = fmt.sub($&, '')
|
518
|
-
end
|
519
|
-
# Since true_text, false_text and nil_text may want to have internal
|
520
|
-
# spaces, defer removing extraneous spaces until after they are parsed.
|
521
|
-
fmt = fmt.gsub(/\s+/, '')
|
522
|
-
if fmt =~ /c\[([-_a-zA-Z]+),([-_a-zA-Z]+)\]/
|
523
|
-
fmt_hash[:true_color] = $1
|
524
|
-
fmt_hash[:false_color] = $2
|
525
|
-
fmt = fmt.sub($&, '')
|
526
|
-
end
|
527
|
-
if fmt =~ /Y/
|
528
|
-
fmt_hash[:true_text] = 'Y'
|
529
|
-
fmt_hash[:false_text] = 'N'
|
530
|
-
fmt = fmt.sub($&, '')
|
531
|
-
end
|
532
|
-
if fmt =~ /T/
|
533
|
-
fmt_hash[:true_text] = 'T'
|
534
|
-
fmt_hash[:false_text] = 'F'
|
535
|
-
fmt = fmt.sub($&, '')
|
536
|
-
end
|
537
|
-
if fmt =~ /X/
|
538
|
-
fmt_hash[:true_text] = 'X'
|
539
|
-
fmt_hash[:false_text] = ''
|
540
|
-
fmt = fmt.sub($&, '')
|
541
|
-
end
|
542
|
-
unless fmt.blank?
|
543
|
-
raise ArgumentError, "unrecognized boolean formatting instructions '#{fmt}'"
|
544
|
-
end
|
545
|
-
fmt_hash
|
546
|
-
end
|
547
|
-
|
548
|
-
###############################################################################
|
549
|
-
# Applying formatting
|
550
|
-
###############################################################################
|
551
|
-
|
552
|
-
public
|
553
|
-
|
554
|
-
# Convert a value to a string based on the instructions in istruct,
|
555
|
-
# depending on the type of val.
|
556
|
-
def format_cell(val, istruct, width = nil)
|
557
|
-
case val
|
558
|
-
when Numeric
|
559
|
-
str = format_numeric(val, istruct)
|
560
|
-
format_string(str, istruct, width)
|
561
|
-
when DateTime, Date
|
562
|
-
str = format_datetime(val, istruct)
|
563
|
-
format_string(str, istruct, width)
|
564
|
-
when TrueClass, FalseClass
|
565
|
-
str = format_boolean(val, istruct)
|
566
|
-
format_string(str, istruct, width)
|
567
|
-
when NilClass
|
568
|
-
str = istruct.nil_text
|
569
|
-
format_string(str, istruct, width)
|
570
|
-
when String
|
571
|
-
format_string(val, istruct, width)
|
572
|
-
else
|
573
|
-
raise ArgumentError,
|
574
|
-
"cannot format value '#{val}' of class #{val.class}"
|
575
|
-
end
|
576
|
-
end
|
577
|
-
|
578
|
-
private
|
579
|
-
|
580
|
-
# Convert a boolean to a string according to instructions in istruct, which
|
581
|
-
# is assumed to be the result of parsing a formatting instruction string as
|
582
|
-
# above. Only device-independent formatting is done here. Device dependent
|
583
|
-
# formatting (e.g., color) can be done in a subclass of Formatter by
|
584
|
-
# specializing this method.
|
585
|
-
def format_boolean(val, istruct)
|
586
|
-
return istruct.nil_text if val.nil?
|
587
|
-
val ? istruct.true_text : istruct.false_text
|
588
|
-
end
|
589
|
-
|
590
|
-
# Convert a datetime to a string according to instructions in istruct, which
|
591
|
-
# is assumed to be the result of parsing a formatting instruction string as
|
592
|
-
# above. Only device-independent formatting is done here. Device dependent
|
593
|
-
# formatting (e.g., color) can be done in a subclass of Formatter by
|
594
|
-
# specializing this method.
|
595
|
-
def format_datetime(val, istruct)
|
596
|
-
return istruct.nil_text if val.nil?
|
597
|
-
if val.to_date == val
|
598
|
-
# It is a Date, with no time component.
|
599
|
-
val.strftime(istruct.date_fmt)
|
600
|
-
else
|
601
|
-
val.strftime(istruct.datetime_fmt)
|
602
|
-
end
|
603
|
-
end
|
604
|
-
|
605
|
-
# Convert a numeric to a string according to instructions in istruct, which
|
606
|
-
# is assumed to be the result of parsing a formatting instruction string as
|
607
|
-
# above. Only device-independent formatting is done here. Device dependent
|
608
|
-
# formatting (e.g., color) can be done in a subclass of Formatter by
|
609
|
-
# specializing this method.
|
610
|
-
def format_numeric(val, istruct)
|
611
|
-
return istruct.nil_text if val.nil?
|
612
|
-
val = val.round(istruct.post_digits) if istruct.post_digits >= 0
|
613
|
-
if istruct.hms
|
614
|
-
result = val.secs_to_hms
|
615
|
-
istruct.commas = false
|
616
|
-
elsif istruct.currency
|
617
|
-
prec = istruct.post_digits == -1 ? 2 : istruct.post_digits
|
618
|
-
delim = istruct.commas ? ',' : ''
|
619
|
-
result = val.to_s(:currency, precision: prec, delimiter: delim,
|
620
|
-
unit: currency_symbol)
|
621
|
-
istruct.commas = false
|
622
|
-
elsif istruct.pre_digits.positive?
|
623
|
-
if val.whole?
|
624
|
-
# No fractional part, ignore post_digits
|
625
|
-
result = sprintf("%0#{istruct.pre_digits}d", val)
|
626
|
-
elsif istruct.post_digits >= 0
|
627
|
-
# There's a fractional part and pre_digits. sprintf width includes
|
628
|
-
# space for fractional part and decimal point, so add pre, post, and 1
|
629
|
-
# to get the proper sprintf width.
|
630
|
-
wid = istruct.pre_digits + 1 + istruct.post_digits
|
631
|
-
result = sprintf("%0#{wid}.#{istruct.post_digits}f", val)
|
632
|
-
else
|
633
|
-
val = val.round(0)
|
634
|
-
result = sprintf("%0#{istruct.pre_digits}d", val)
|
635
|
-
end
|
636
|
-
elsif istruct.post_digits >= 0
|
637
|
-
# Round to post_digits but no padding of whole number, pad fraction with
|
638
|
-
# trailing zeroes.
|
639
|
-
result = sprintf("%.#{istruct.post_digits}f", val)
|
640
|
-
else
|
641
|
-
result = val.to_s
|
642
|
-
end
|
643
|
-
if istruct.commas
|
644
|
-
# Commify the whole number part if not done already.
|
645
|
-
result = result.commify
|
646
|
-
end
|
647
|
-
result
|
648
|
-
end
|
649
|
-
|
650
|
-
# Apply non-device-dependent string formatting instructions.
|
651
|
-
def format_string(val, istruct, width = nil)
|
652
|
-
val = istruct.nil_text if val.nil?
|
653
|
-
val =
|
654
|
-
case istruct.case
|
655
|
-
when :lower
|
656
|
-
val.downcase
|
657
|
-
when :upper
|
658
|
-
val.upcase
|
659
|
-
when :title
|
660
|
-
val.entitle
|
661
|
-
when :none
|
662
|
-
val
|
663
|
-
end
|
664
|
-
if width && aligned?
|
665
|
-
pad = width - width(val)
|
666
|
-
case istruct.alignment
|
667
|
-
when :left
|
668
|
-
val += ' ' * pad
|
669
|
-
when :right
|
670
|
-
val = ' ' * pad + val
|
671
|
-
when :center
|
672
|
-
lpad = pad / 2 + (pad.odd? ? 1 : 0)
|
673
|
-
rpad = pad / 2
|
674
|
-
val = ' ' * lpad + val + ' ' * rpad
|
675
|
-
else
|
676
|
-
val = val
|
677
|
-
end
|
678
|
-
end
|
679
|
-
val
|
680
|
-
end
|
681
|
-
|
682
|
-
###############################################################################
|
683
|
-
# Output routines
|
684
|
-
###############################################################################
|
685
|
-
|
686
|
-
public
|
687
|
-
|
688
|
-
def output
|
689
|
-
# Build the output as an array of row hashes, with the hashes keyed on the
|
690
|
-
# new header string and the values the string-formatted cells. Each group
|
691
|
-
# boundary, gfooter, and footer is marked by inserting a nil in the result
|
692
|
-
# array at that point.
|
693
|
-
formatted_headers = build_formatted_headers
|
694
|
-
new_rows = build_formatted_body
|
695
|
-
new_rows += build_formatted_footers
|
696
|
-
|
697
|
-
# Make a second pass over the formatted cells to add spacing according to
|
698
|
-
# the alignment instruction if this is a Formatter that performs its own
|
699
|
-
# alignment.
|
700
|
-
if aligned?
|
701
|
-
widths = width_map(formatted_headers, new_rows)
|
702
|
-
table.headers.each do |h|
|
703
|
-
formatted_headers[h] =
|
704
|
-
format_string(formatted_headers[h], format_at[:header][h], widths[h])
|
705
|
-
end
|
706
|
-
aligned_rows = []
|
707
|
-
new_rows.each do |loc_row|
|
708
|
-
if loc_row.nil?
|
709
|
-
aligned_rows << nil
|
710
|
-
next
|
711
|
-
end
|
712
|
-
loc, row = *loc_row
|
713
|
-
aligned_row = {}
|
714
|
-
row.each_pair do |h, val|
|
715
|
-
aligned_row[h] = format_string(val, format_at[loc][h], widths[h])
|
716
|
-
end
|
717
|
-
aligned_rows << [loc, aligned_row]
|
718
|
-
end
|
719
|
-
new_rows = aligned_rows
|
720
|
-
end
|
721
|
-
|
722
|
-
# Now that the contents of the output table cells have been computed and
|
723
|
-
# alignment applied, we can actually construct the table using the methods
|
724
|
-
# for constructing table parts, pre_table, etc. We expect that these will
|
725
|
-
# be overridden by subclasses of Formatter for specific output targets. In
|
726
|
-
# any event, the result is a single string representing the table in the
|
727
|
-
# syntax of the output target.
|
728
|
-
result = ''
|
729
|
-
result += pre_table
|
730
|
-
if include_header_row?
|
731
|
-
result += pre_header(widths)
|
732
|
-
result += pre_row
|
733
|
-
cells = []
|
734
|
-
formatted_headers.each_pair do |h, v|
|
735
|
-
cells << pre_cell(h) + quote_cell(v) + post_cell
|
736
|
-
end
|
737
|
-
result += cells.join(inter_cell)
|
738
|
-
result += post_row
|
739
|
-
result += post_header(widths)
|
740
|
-
end
|
741
|
-
new_rows.each do |loc_row|
|
742
|
-
result += hline(widths) if loc_row.nil?
|
743
|
-
next if loc_row.nil?
|
744
|
-
_loc, row = *loc_row
|
745
|
-
result += pre_row
|
746
|
-
cells = []
|
747
|
-
row.each_pair do |h, v|
|
748
|
-
cells << pre_cell(h) + quote_cell(v) + post_cell
|
749
|
-
end
|
750
|
-
result += cells.join(inter_cell)
|
751
|
-
result += post_row
|
752
|
-
end
|
753
|
-
result += post_footers(widths)
|
754
|
-
result += post_table
|
755
|
-
evaluate? ? eval(result) : result
|
756
|
-
end
|
757
|
-
|
758
|
-
private
|
759
|
-
|
760
|
-
# Return a hash mapping the table's headers to their formatted versions. If
|
761
|
-
# a hash of column widths is given, perform alignment within the given field
|
762
|
-
# widths.
|
763
|
-
def build_formatted_headers(widths = {})
|
764
|
-
map = {}
|
765
|
-
table.headers.each do |h|
|
766
|
-
map[h] = format_string(h.as_string, format_at[:header][h], widths[h])
|
767
|
-
end
|
768
|
-
map
|
769
|
-
end
|
770
|
-
|
771
|
-
# Return an array of two-element arrays, with the first element of the
|
772
|
-
# inner array being the location of the row and the second element being a
|
773
|
-
# hash, using the table's headers as keys and the formatted cells as the
|
774
|
-
# values. Add formatted group footers along the way.
|
775
|
-
def build_formatted_body
|
776
|
-
new_rows = []
|
777
|
-
tbl_row_k = 0
|
778
|
-
table.groups.each_with_index do |grp, grp_k|
|
779
|
-
# Mark the beginning of a group if this is the first group after the
|
780
|
-
# header or the second or later group.
|
781
|
-
new_rows << nil if include_header_row? || grp_k.positive?
|
782
|
-
# Compute group body
|
783
|
-
grp_col = {}
|
784
|
-
grp.each_with_index do |row, grp_row_k|
|
785
|
-
new_row = {}
|
786
|
-
location =
|
787
|
-
if tbl_row_k.zero?
|
788
|
-
:bfirst
|
789
|
-
elsif grp_row_k.zero?
|
790
|
-
:gfirst
|
791
|
-
else
|
792
|
-
:body
|
793
|
-
end
|
794
|
-
table.headers.each do |h|
|
795
|
-
grp_col[h] ||= Column.new(header: h)
|
796
|
-
grp_col[h] << row[h]
|
797
|
-
new_row[h] = format_cell(row[h], format_at[location][h])
|
798
|
-
end
|
799
|
-
new_rows << [location, new_row]
|
800
|
-
tbl_row_k += 1
|
801
|
-
end
|
802
|
-
# Compute group footers
|
803
|
-
gfooters.each_pair do |label, gfooter|
|
804
|
-
# Mark the beginning of a group footer
|
805
|
-
new_rows << nil
|
806
|
-
gfoot_row = {}
|
807
|
-
first_h = nil
|
808
|
-
grp_col.each_pair do |h, col|
|
809
|
-
first_h ||= h
|
810
|
-
gfoot_row[h] =
|
811
|
-
if gfooter[h]
|
812
|
-
format_cell(col.send(gfooter[h]), format_at[:gfooter][h])
|
813
|
-
else
|
814
|
-
''
|
815
|
-
end
|
816
|
-
end
|
817
|
-
if gfoot_row[first_h].blank?
|
818
|
-
gfoot_row[first_h] = format_string(label, format_at[:gfooter][first_h])
|
819
|
-
end
|
820
|
-
new_rows << [:gfooter, gfoot_row]
|
821
|
-
end
|
822
|
-
end
|
823
|
-
new_rows
|
824
|
-
end
|
825
|
-
|
826
|
-
def build_formatted_footers
|
827
|
-
new_rows = []
|
828
|
-
# Done with body, compute the table footers.
|
829
|
-
footers.each_pair do |label, footer|
|
830
|
-
# Mark the beginning of a footer
|
831
|
-
new_rows << nil
|
832
|
-
foot_row = {}
|
833
|
-
first_h = nil
|
834
|
-
table.columns.each do |col|
|
835
|
-
h = col.header
|
836
|
-
first_h ||= h
|
837
|
-
foot_row[h] =
|
838
|
-
if footer[h]
|
839
|
-
format_cell(col.send(footer[h]), format_at[:footer][h])
|
840
|
-
else
|
841
|
-
''
|
842
|
-
end
|
843
|
-
end
|
844
|
-
# Put the label in the first column of footer unless it has been
|
845
|
-
# formatted as part of footer.
|
846
|
-
if foot_row[first_h].blank?
|
847
|
-
foot_row[first_h] = format_string(label, format_at[:footer][first_h])
|
848
|
-
end
|
849
|
-
new_rows << [:footer, foot_row]
|
850
|
-
end
|
851
|
-
new_rows
|
852
|
-
end
|
853
|
-
|
854
|
-
# Return a hash of the maximum widths of all the given headers and rows.
|
855
|
-
def width_map(formatted_headers, rows)
|
856
|
-
widths = {}
|
857
|
-
formatted_headers.each_pair do |h, v|
|
858
|
-
widths[h] ||= 0
|
859
|
-
widths[h] = [widths[h], width(v)].max
|
860
|
-
end
|
861
|
-
rows.each do |loc_row|
|
862
|
-
next if loc_row.nil?
|
863
|
-
_loc, row = *loc_row
|
864
|
-
row.each_pair do |h, v|
|
865
|
-
widths[h] ||= 0
|
866
|
-
widths[h] = [widths[h], width(v)].max
|
867
|
-
end
|
868
|
-
end
|
869
|
-
widths
|
870
|
-
end
|
871
|
-
|
872
|
-
# Does this Formatter require a second pass over the cells to align the
|
873
|
-
# columns according to the alignment formatting instruction to the width of
|
874
|
-
# the widest cell in each column?
|
875
|
-
def aligned?
|
876
|
-
false
|
877
|
-
end
|
878
|
-
|
879
|
-
# Should the string result of output be evaluated to form a ruby data
|
880
|
-
# structure? For example, AoaFormatter wants to return an array of arrays of
|
881
|
-
# strings, so it should build a ruby expression to do that, then have it
|
882
|
-
# eval'ed.
|
883
|
-
def evaluate?
|
884
|
-
false
|
885
|
-
end
|
886
|
-
|
887
|
-
# Compute the width of the string as displayed, taking into account the
|
888
|
-
# characteristics of the target device. For example, a colored string
|
889
|
-
# should not include in the width terminal control characters that simply
|
890
|
-
# change the color without occupying any space. Thus, this method must be
|
891
|
-
# overridden in a subclass if a simple character count does not reflect the
|
892
|
-
# width as displayed.
|
893
|
-
def width(str)
|
894
|
-
str.length
|
895
|
-
end
|
896
|
-
|
897
|
-
def pre_table
|
898
|
-
''
|
899
|
-
end
|
900
|
-
|
901
|
-
def post_table
|
902
|
-
''
|
903
|
-
end
|
904
|
-
|
905
|
-
def include_header_row?
|
906
|
-
true
|
907
|
-
end
|
908
|
-
|
909
|
-
def pre_header(_widths)
|
910
|
-
''
|
911
|
-
end
|
912
|
-
|
913
|
-
def post_header(_widths)
|
914
|
-
''
|
915
|
-
end
|
916
|
-
|
917
|
-
def pre_row
|
918
|
-
''
|
919
|
-
end
|
920
|
-
|
921
|
-
def pre_cell(_h)
|
922
|
-
''
|
923
|
-
end
|
924
|
-
|
925
|
-
def quote_cell(v)
|
926
|
-
v
|
927
|
-
end
|
928
|
-
|
929
|
-
def post_cell
|
930
|
-
''
|
931
|
-
end
|
932
|
-
|
933
|
-
def inter_cell
|
934
|
-
'|'
|
935
|
-
end
|
936
|
-
|
937
|
-
def post_row
|
938
|
-
"\n"
|
939
|
-
end
|
940
|
-
|
941
|
-
def hline(_widths)
|
942
|
-
''
|
943
|
-
end
|
944
|
-
|
945
|
-
def pre_group
|
946
|
-
''
|
947
|
-
end
|
948
|
-
|
949
|
-
def post_group
|
950
|
-
''
|
951
|
-
end
|
952
|
-
|
953
|
-
def pre_gfoot
|
954
|
-
''
|
955
|
-
end
|
956
|
-
|
957
|
-
def post_gfoot
|
958
|
-
''
|
959
|
-
end
|
960
|
-
|
961
|
-
def pre_foot
|
962
|
-
''
|
963
|
-
end
|
964
|
-
|
965
|
-
def post_foot
|
966
|
-
''
|
967
|
-
end
|
968
|
-
|
969
|
-
def post_footers(_widths)
|
970
|
-
''
|
971
|
-
end
|
972
|
-
end
|
973
|
-
end
|