fat_table 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +2 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.org +2106 -0
- data/README.rdoc +1965 -0
- data/Rakefile +12 -0
- data/TODO.org +31 -0
- data/bin/ft_console +119 -0
- data/bin/setup +8 -0
- data/fat_table.gemspec +80 -0
- data/lib/fat_table.rb +225 -0
- data/lib/fat_table/column.rb +522 -0
- data/lib/fat_table/db_handle.rb +81 -0
- data/lib/fat_table/errors.rb +13 -0
- data/lib/fat_table/evaluator.rb +55 -0
- data/lib/fat_table/formatters.rb +7 -0
- data/lib/fat_table/formatters/aoa_formatter.rb +91 -0
- data/lib/fat_table/formatters/aoh_formatter.rb +91 -0
- data/lib/fat_table/formatters/formatter.rb +1248 -0
- data/lib/fat_table/formatters/latex_formatter.rb +208 -0
- data/lib/fat_table/formatters/org_formatter.rb +72 -0
- data/lib/fat_table/formatters/term_formatter.rb +297 -0
- data/lib/fat_table/formatters/text_formatter.rb +92 -0
- data/lib/fat_table/table.rb +1322 -0
- data/lib/fat_table/version.rb +4 -0
- metadata +331 -0
@@ -0,0 +1,81 @@
|
|
1
|
+
module FatTable
|
2
|
+
class << self;
|
3
|
+
# The +DBI+ database handle returned by the last successful call to
|
4
|
+
# FatTable.set_db.
|
5
|
+
attr_accessor :handle
|
6
|
+
end
|
7
|
+
|
8
|
+
# This method must be called before calling FatTable.from_sql or
|
9
|
+
# FatTable::Table.from_sql in order to specify the database to use. All of
|
10
|
+
# the keyword parameters have a default except +database:+, which must contain
|
11
|
+
# the name of the database to query.
|
12
|
+
#
|
13
|
+
# +driver+::
|
14
|
+
# One of 'Pg' (for Postgresql), 'Mysql' (for Mysql), or 'SQLite3' (for
|
15
|
+
# SQLite3) to specify the +DBI+ driver to use. You may have to install the
|
16
|
+
# driver to make this work. By default use 'Pg'.
|
17
|
+
#
|
18
|
+
# +database+::
|
19
|
+
# The name of the database to access. There is no default for this.
|
20
|
+
#
|
21
|
+
# +user+::
|
22
|
+
# The user name to use for accessing the database. It defaults to nil,
|
23
|
+
# which may be interpreted as a default user by the DBI driver being used.
|
24
|
+
#
|
25
|
+
# +password+::
|
26
|
+
# The password to use for accessing the database. It defaults to nil,
|
27
|
+
# which may be interpreted as a default password by the DBI driver being used.
|
28
|
+
#
|
29
|
+
# +host+::
|
30
|
+
# The name of the host on which to look for the database connection,
|
31
|
+
# defaulting to 'localhost'.
|
32
|
+
#
|
33
|
+
# +port+::
|
34
|
+
# The port number as a string or integer on which to access the database on
|
35
|
+
# the given host. Defaults to '5432'. Only used if host is not 'localhost'.
|
36
|
+
#
|
37
|
+
# +socket+::
|
38
|
+
# The socket to use to access the database if the host is 'localhost'.
|
39
|
+
# Defaults to the standard socket for the Pg driver, '/tmp/.s.PGSQL.5432'.
|
40
|
+
#
|
41
|
+
# If successful the database handle for DBI is return.
|
42
|
+
# Once called successfully, this establishes the database handle to use for
|
43
|
+
# all subsequent calls to FatTable.from_sql or FatTable::Table.from_sql. You
|
44
|
+
# can then access the handle if needed with FatTable.db.
|
45
|
+
def self.set_db(driver: 'Pg',
|
46
|
+
database:,
|
47
|
+
user: nil,
|
48
|
+
password: nil,
|
49
|
+
host: 'localhost',
|
50
|
+
port: '5432',
|
51
|
+
socket: '/tmp/.s.PGSQL.5432')
|
52
|
+
raise UserError, 'must supply database name to set_db' unless database
|
53
|
+
|
54
|
+
valid_drivers = ['Pg', 'Mysql', 'SQLite3']
|
55
|
+
unless valid_drivers.include?(driver)
|
56
|
+
raise UserError, "set_db driver must be one of #{valid_drivers.join(' or ')}"
|
57
|
+
end
|
58
|
+
# In case port is given as an integer
|
59
|
+
port = port.to_s if port
|
60
|
+
|
61
|
+
# Set the dsn for DBI
|
62
|
+
dsn =
|
63
|
+
if host == 'localhost'
|
64
|
+
"DBI:Pg:database=#{database};host=#{host};socket=#{socket}"
|
65
|
+
else
|
66
|
+
"DBI:Pg:database=#{database};host=#{host};port=#{port}"
|
67
|
+
end
|
68
|
+
begin
|
69
|
+
self.handle = ::DBI.connect(dsn, user, password)
|
70
|
+
rescue DBI::OperationalError => ex
|
71
|
+
raise TransientError, "#{dsn}: #{ex}"
|
72
|
+
end
|
73
|
+
handle
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return the +DBI+ database handle as returned by the last call to
|
77
|
+
# FatTable.set_db.
|
78
|
+
def self.db
|
79
|
+
handle
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module FatTable
|
2
|
+
# Raised when the caller of the code made an error that the caller can
|
3
|
+
# correct.
|
4
|
+
class UserError < StandardError; end
|
5
|
+
|
6
|
+
# Raised when the programmer made an error that the caller of the code
|
7
|
+
# cannot correct.
|
8
|
+
class LogicError < StandardError; end
|
9
|
+
|
10
|
+
# Raised when an external resource is not available due to caller or
|
11
|
+
# programmer error or some failure of the external resource to be available.
|
12
|
+
class TransientError < StandardError; end
|
13
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module FatTable
|
2
|
+
# The Evaluator class provides a class for evaluating Ruby expressions based
|
3
|
+
# on variable settings provided at runtime. If the same Evaluator object is
|
4
|
+
# used for several successive calls, it can maintain state between calls with
|
5
|
+
# instance variables. The call to Evaluator.new can be given a hash of
|
6
|
+
# instance variable names and values that will be maintained across all calls
|
7
|
+
# to the #evaluate method. In addition, on each evaluate call, a set of
|
8
|
+
# /local/ variables can be supplied to provide variables that exist only for
|
9
|
+
# the duration of that evaluate call. An optional before and after string can
|
10
|
+
# be given to the constructor that will evaluate the given expression before
|
11
|
+
# and, respectively, after each call to #evaluate. This provides a way to
|
12
|
+
# update values of instance variables for use in subsequent calls to
|
13
|
+
# #evaluate.
|
14
|
+
class Evaluator
|
15
|
+
# Return a new Evaluator object in which the Hash +vars+ defines the
|
16
|
+
# bindings for instance variables to be available and maintained across all
|
17
|
+
# subsequent calls to Evaluator.evaluate. The strings +before+ and +after+
|
18
|
+
# are string expressions that will be evaluated before and after each
|
19
|
+
# subsequent call to Evaluator.evaluate.
|
20
|
+
def initialize(vars: {}, before: nil, after: nil)
|
21
|
+
@before = before
|
22
|
+
@after = after
|
23
|
+
set_instance_vars(vars)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Return the result of evaluating +expr+ as a Ruby expression in which the
|
27
|
+
# instance variables set in Evaluator.new and any local variables set in the
|
28
|
+
# Hash parameter +vars+ are available to the expression. Call any +before+
|
29
|
+
# hook defined in Evaluator.new before evaluating +expr+ and any +after+
|
30
|
+
# hook defined in Evaluator.new after evaluating +expr+.
|
31
|
+
def evaluate(expr = '', vars: {})
|
32
|
+
bdg = binding
|
33
|
+
set_local_vars(vars, bdg)
|
34
|
+
eval(@before, bdg) if @before
|
35
|
+
result = eval(expr, bdg)
|
36
|
+
eval(@after, bdg) if @after
|
37
|
+
result
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def set_instance_vars(vars = {})
|
43
|
+
vars.each_pair do |name, val|
|
44
|
+
name = "@#{name}" unless name.to_s.start_with?('@')
|
45
|
+
instance_variable_set(name, val)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def set_local_vars(vars = {}, bnd)
|
50
|
+
vars.each_pair do |name, val|
|
51
|
+
bnd.local_variable_set(name, val)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'fat_table/formatters/formatter'
|
2
|
+
require 'fat_table/formatters/aoa_formatter'
|
3
|
+
require 'fat_table/formatters/aoh_formatter'
|
4
|
+
require 'fat_table/formatters/org_formatter'
|
5
|
+
require 'fat_table/formatters/text_formatter'
|
6
|
+
require 'fat_table/formatters/term_formatter'
|
7
|
+
require 'fat_table/formatters/latex_formatter'
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module FatTable
|
2
|
+
# A subclass of Formatter for rendering the table as a Ruby Array of Arrays.
|
3
|
+
# Each cell is an element of the inner Array is formatted as a string in
|
4
|
+
# accordance with the formatting directives. All footers are included as extra
|
5
|
+
# Arrays of the output. AoaFormatter supports no +options+
|
6
|
+
class AoaFormatter < Formatter
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def evaluate?
|
11
|
+
true
|
12
|
+
end
|
13
|
+
|
14
|
+
def pre_table
|
15
|
+
'['
|
16
|
+
end
|
17
|
+
|
18
|
+
def post_table
|
19
|
+
']'
|
20
|
+
end
|
21
|
+
|
22
|
+
def pre_header(_widths)
|
23
|
+
''
|
24
|
+
end
|
25
|
+
|
26
|
+
def post_header(_widths)
|
27
|
+
''
|
28
|
+
end
|
29
|
+
|
30
|
+
def pre_row
|
31
|
+
'['
|
32
|
+
end
|
33
|
+
|
34
|
+
def pre_cell(_h)
|
35
|
+
"'"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Because the cell, after conversion to a single-quoted string will be
|
39
|
+
# eval'ed, we need to escape any single-quotes (') that appear in the
|
40
|
+
# string.
|
41
|
+
def quote_cell(v)
|
42
|
+
if v =~ /'/
|
43
|
+
# Use a negative look-behind to only quote single-quotes that are not
|
44
|
+
# already preceded by a backslash
|
45
|
+
v.gsub(/(?<!\\)'/, "'" => "\\'")
|
46
|
+
else
|
47
|
+
v
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def post_cell
|
52
|
+
"'"
|
53
|
+
end
|
54
|
+
|
55
|
+
def inter_cell
|
56
|
+
','
|
57
|
+
end
|
58
|
+
|
59
|
+
def post_row
|
60
|
+
"],\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
def hline(_widths)
|
64
|
+
"nil,\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
def pre_group
|
68
|
+
''
|
69
|
+
end
|
70
|
+
|
71
|
+
def post_group
|
72
|
+
''
|
73
|
+
end
|
74
|
+
|
75
|
+
def pre_gfoot
|
76
|
+
''
|
77
|
+
end
|
78
|
+
|
79
|
+
def post_gfoot
|
80
|
+
''
|
81
|
+
end
|
82
|
+
|
83
|
+
def pre_foot
|
84
|
+
''
|
85
|
+
end
|
86
|
+
|
87
|
+
def post_foot
|
88
|
+
''
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
module FatTable
|
2
|
+
# A subclass of Formatter for rendering the table as a Ruby Array of Hashes.
|
3
|
+
# Each row of the Array is a Hash representing one row of the table with the
|
4
|
+
# keys being the symbolic form of the headers. Each cell is a value in a row
|
5
|
+
# Hash formatted as a string in accordance with the formatting directives. All
|
6
|
+
# footers are included as extra Hashes of the output. AoaFormatter supports no
|
7
|
+
# +options+
|
8
|
+
class AohFormatter < Formatter
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def evaluate?
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
def pre_table
|
17
|
+
'['
|
18
|
+
end
|
19
|
+
|
20
|
+
def post_table
|
21
|
+
']'
|
22
|
+
end
|
23
|
+
|
24
|
+
# We include no row for the header because the keys of each hash serve as
|
25
|
+
# the headers.
|
26
|
+
def include_header_row?
|
27
|
+
false
|
28
|
+
end
|
29
|
+
|
30
|
+
def pre_row
|
31
|
+
'{'
|
32
|
+
end
|
33
|
+
|
34
|
+
def pre_cell(h)
|
35
|
+
":#{h.as_sym} => '"
|
36
|
+
end
|
37
|
+
|
38
|
+
# Because the cell, after conversion to a single-quoted string will be
|
39
|
+
# eval'ed, we need to escape any single-quotes (') that appear in the
|
40
|
+
# string.
|
41
|
+
def quote_cell(v)
|
42
|
+
if v =~ /'/
|
43
|
+
# Use a negative look-behind to only quote single-quotes that are not
|
44
|
+
# already preceded by a backslash
|
45
|
+
v.gsub(/(?<!\\)'/, "'" => "\\'")
|
46
|
+
else
|
47
|
+
v
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def post_cell
|
52
|
+
"'"
|
53
|
+
end
|
54
|
+
|
55
|
+
def inter_cell
|
56
|
+
','
|
57
|
+
end
|
58
|
+
|
59
|
+
def post_row
|
60
|
+
"},\n"
|
61
|
+
end
|
62
|
+
|
63
|
+
def hline(_widths)
|
64
|
+
"nil,\n"
|
65
|
+
end
|
66
|
+
|
67
|
+
def pre_group
|
68
|
+
''
|
69
|
+
end
|
70
|
+
|
71
|
+
def post_group
|
72
|
+
''
|
73
|
+
end
|
74
|
+
|
75
|
+
def pre_gfoot
|
76
|
+
''
|
77
|
+
end
|
78
|
+
|
79
|
+
def post_gfoot
|
80
|
+
''
|
81
|
+
end
|
82
|
+
|
83
|
+
def pre_foot
|
84
|
+
''
|
85
|
+
end
|
86
|
+
|
87
|
+
def post_foot
|
88
|
+
''
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,1248 @@
|
|
1
|
+
module FatTable
|
2
|
+
# A Formatter is for use in Table output routines, and provides methods for
|
3
|
+
# adding group and table footers to the output and instructions for how the
|
4
|
+
# table's cells ought to be formatted. The goal is to make subclasses of this
|
5
|
+
# class handle different output targets, such as aoa for org tables, ANSI
|
6
|
+
# terminals, LaTeX, plain text, org mode table text, and so forth. Many of
|
7
|
+
# the formatting options, such as color, will be no-ops for some output
|
8
|
+
# targets, such as text, but will be valid nonetheless. Thus, the Formatter
|
9
|
+
# subclass should provide the best implementation for each formatting request
|
10
|
+
# available for the target. This base class will consist largely of methods
|
11
|
+
# that format output as pipe-separated values, but implementations provided
|
12
|
+
# by subclasses will override these for different output targets.
|
13
|
+
class Formatter
|
14
|
+
# Valid locations in a Table as an array of symbols.
|
15
|
+
LOCATIONS = [:header, :body, :bfirst, :gfirst, :gfooter, :footer].freeze
|
16
|
+
|
17
|
+
# The table that is the subject of the Formatter.
|
18
|
+
attr_reader :table
|
19
|
+
|
20
|
+
# Options given to the Formatter constructor that allow variants for
|
21
|
+
# specific Formatters.
|
22
|
+
attr_reader :options
|
23
|
+
|
24
|
+
# A Hash of Hashes with the outer Hash keyed on location. The value for the
|
25
|
+
# outer Hash is another Hash keyed on column names. The values of the inner
|
26
|
+
# Hash are OpenStruct objects that contain the formatting instructions for
|
27
|
+
# the location and column. For example, +format_at[:body][:shares].commas+
|
28
|
+
# is set either true or false depending on whether the +:shares+ column in
|
29
|
+
# the table body is to have grouping commas inserted in the output.
|
30
|
+
attr_reader :format_at
|
31
|
+
|
32
|
+
# A Hash of the table-wide footers to be added to the output. The key is a
|
33
|
+
# string that is to serve as the label for the footer and inserted in the
|
34
|
+
# first column of the footer if that column is otherwise not populated with
|
35
|
+
# footer content. The value is Hash in which the keys are column symbols and
|
36
|
+
# the values are symbols for the aggregate method to be applied to the
|
37
|
+
# column to provide a value in the footer for that column. Thus,
|
38
|
+
# +footers['Total'][:shares]+ might be set to +:sum+ to indicate that the
|
39
|
+
# +:shares+ column is to be summed in the footer labeled 'Total'.
|
40
|
+
attr_reader :footers
|
41
|
+
|
42
|
+
# A Hash of the group footers to be added to the output. The key is a string
|
43
|
+
# that is to serve as the label for the footer and inserted in the first
|
44
|
+
# column of the footer if that column is otherwise not populated with group
|
45
|
+
# footer content. The value is Hash in which the keys are column symbols and
|
46
|
+
# the values are symbols for the aggregate method to be applied to the
|
47
|
+
# group's column to provide a value in the group footer for that column.
|
48
|
+
# Thus, +gfooters['Average'][:shares]+ might be set to +:avg+ to indicate that
|
49
|
+
# the +:shares+ column is to be averaged in the group footer labeled 'Average'.
|
50
|
+
attr_reader :gfooters
|
51
|
+
|
52
|
+
class_attribute :default_format
|
53
|
+
self.default_format = {
|
54
|
+
nil_text: '',
|
55
|
+
case: :none,
|
56
|
+
alignment: :left,
|
57
|
+
bold: false,
|
58
|
+
italic: false,
|
59
|
+
color: 'none',
|
60
|
+
bgcolor: 'none',
|
61
|
+
hms: false,
|
62
|
+
pre_digits: 0,
|
63
|
+
post_digits: -1,
|
64
|
+
commas: false,
|
65
|
+
currency: false,
|
66
|
+
datetime_fmt: '%F %H:%M:%S',
|
67
|
+
date_fmt: '%F',
|
68
|
+
true_text: 'T',
|
69
|
+
false_text: 'F',
|
70
|
+
true_color: 'none',
|
71
|
+
true_bgcolor: 'none',
|
72
|
+
false_color: 'none',
|
73
|
+
false_bgcolor: 'none',
|
74
|
+
underline: false,
|
75
|
+
blink: false
|
76
|
+
}
|
77
|
+
|
78
|
+
class_attribute :valid_colors
|
79
|
+
self.valid_colors = ['none']
|
80
|
+
|
81
|
+
# :category: Constructors
|
82
|
+
|
83
|
+
# Return a new Formatter for the given +table+ which must be of the class
|
84
|
+
# FatTable::Table. The +options+ hash can specify variants for the output
|
85
|
+
# for specific subclasses of Formatter. This base class outputs the +table+
|
86
|
+
# as a string in the pipe-separated form, which is much like CSV except that
|
87
|
+
# it uses the ASCII pipe symbol +|+ to separate values rather than the
|
88
|
+
# comma, and therefore does not bother to quote strings since it assumes
|
89
|
+
# they will not contain any pipes. A new Formatter provides default
|
90
|
+
# formatting for all the cells in the table. If you give a block, the new
|
91
|
+
# Formatter is yielded to the block so that methods for formatting and
|
92
|
+
# adding footers can be called on it.
|
93
|
+
def initialize(table = Table.new, **options)
|
94
|
+
unless table && table.is_a?(Table)
|
95
|
+
raise UserError, 'must initialize Formatter with a Table'
|
96
|
+
end
|
97
|
+
@table = table
|
98
|
+
@options = options
|
99
|
+
@footers = {}
|
100
|
+
@gfooters = {}
|
101
|
+
# Formatting instructions for various "locations" within the Table, as
|
102
|
+
# a hash of hashes. The outer hash is keyed on the location, and each
|
103
|
+
# inner hash is keyed on either a column sym or a type sym, :string, :numeric,
|
104
|
+
# :datetime, :boolean, or :nil. The value of the inner hashes are
|
105
|
+
# OpenStruct structs.
|
106
|
+
@format_at = {}
|
107
|
+
[:header, :bfirst, :gfirst, :body, :footer, :gfooter].each do |loc|
|
108
|
+
@format_at[loc] = {}
|
109
|
+
table.headers.each do |h|
|
110
|
+
fmt_hash = self.class.default_format
|
111
|
+
fmt_hash[:_h] = h
|
112
|
+
fmt_hash[:_location] = loc
|
113
|
+
format_at[loc][h] = OpenStruct.new(fmt_hash)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
yield self if block_given?
|
117
|
+
end
|
118
|
+
|
119
|
+
# :category: Footers
|
120
|
+
|
121
|
+
############################################################################
|
122
|
+
# Footer methods
|
123
|
+
#
|
124
|
+
# A Table may have any number of footers and any number of group footers.
|
125
|
+
# Footers are not part of the table's data and never participate in any of
|
126
|
+
# the transformation methods on tables. They are never inherited by output
|
127
|
+
# tables from input tables in any of the transformation methods.
|
128
|
+
#
|
129
|
+
# When output, a table footer will appear at the bottom of the table, and a
|
130
|
+
# group footer will appear at the bottom of each group.
|
131
|
+
#
|
132
|
+
# Each footer must have a label, usually a string such as 'Total', to
|
133
|
+
# identify the purpose of the footer, and the label must be distinct among
|
134
|
+
# all footers of the same type. That is you may have a table footer labeled
|
135
|
+
# 'Total' and a group footer labeled 'Total', but you may not have two table
|
136
|
+
# footers with that label. If the first column of the table is not included
|
137
|
+
# in the footer, the footer's label will be placed there, otherwise, there
|
138
|
+
# will be no label output. The footers are accessible with the #footers
|
139
|
+
# method, which returns a hash indexed by the label converted to a symbol.
|
140
|
+
# The symbol is reconverted to a title-cased string on output.
|
141
|
+
#
|
142
|
+
# Note that by adding footers or gfooters to the table, you are only stating
|
143
|
+
# what footers you want on output of the table. No actual calculation is
|
144
|
+
# performed until the table is output.
|
145
|
+
#
|
146
|
+
# Add a table footer to the table with a label given in the first parameter,
|
147
|
+
# defaulting to 'Total'. After the label, you can given any number of
|
148
|
+
# headers (as symbols) for columns to be summed, and then any number of hash
|
149
|
+
# parameters for columns for with to apply an aggregate other than :sum.
|
150
|
+
# For example, these are valid footer definitions.
|
151
|
+
#
|
152
|
+
# Just sum the shares column with a label of 'Total'
|
153
|
+
# tab.footer(:shares)
|
154
|
+
#
|
155
|
+
# Change the label and sum the :price column as well
|
156
|
+
# tab.footer('Grand Total', :shares, :price)
|
157
|
+
#
|
158
|
+
# Average then show standard deviation of several columns
|
159
|
+
# tab.footer.('Average', date: avg, shares: :avg, price: avg)
|
160
|
+
# tab.footer.('Sigma', date: dev, shares: :dev, price: :dev)
|
161
|
+
#
|
162
|
+
# Do some sums and some other aggregates: sum shares, average date and
|
163
|
+
# price.
|
164
|
+
# tab.footer.('Summary', :shares, date: avg, price: avg)
|
165
|
+
def footer(label, *sum_cols, **agg_cols)
|
166
|
+
label = label.to_s
|
167
|
+
foot = {}
|
168
|
+
sum_cols.each do |h|
|
169
|
+
unless table.headers.include?(h)
|
170
|
+
raise UserError, "No '#{h}' column in table to sum in the footer"
|
171
|
+
end
|
172
|
+
foot[h] = :sum
|
173
|
+
end
|
174
|
+
agg_cols.each do |h, agg|
|
175
|
+
unless table.headers.include?(h)
|
176
|
+
raise UserError, "No '#{h}' column in table to #{agg} in the footer"
|
177
|
+
end
|
178
|
+
foot[h] = agg
|
179
|
+
end
|
180
|
+
@footers[label] = foot
|
181
|
+
self
|
182
|
+
end
|
183
|
+
|
184
|
+
# :category: Footers
|
185
|
+
|
186
|
+
# Add a group footer to the table with a label given in the first parameter,
|
187
|
+
# defaulting to 'Total'. After the label, you can given any number of
|
188
|
+
# headers (as symbols) for columns to be summed, and then any number of hash
|
189
|
+
# parameters for columns for with to apply an aggregate other than :sum. For
|
190
|
+
# example, these are valid gfooter definitions.
|
191
|
+
#
|
192
|
+
# Just sum the shares column with a label of 'Total' tab.gfooter(:shares)
|
193
|
+
#
|
194
|
+
# Change the label and sum the :price column as well tab.gfooter('Total',
|
195
|
+
# :shares, :price)
|
196
|
+
#
|
197
|
+
# Average then show standard deviation of several columns
|
198
|
+
# tab.gfooter.('Average', date: avg, shares: :avg, price: avg)
|
199
|
+
# tab.gfooter.('Sigma', date: dev, shares: :dev, price: :dev)
|
200
|
+
#
|
201
|
+
# Do some sums and some other aggregates: sum shares, average date and
|
202
|
+
# price. tab.gfooter.('Summary', :shares, date: avg, price: avg)
|
203
|
+
def gfooter(label, *sum_cols, **agg_cols)
|
204
|
+
label = label.to_s
|
205
|
+
foot = {}
|
206
|
+
sum_cols.each do |h|
|
207
|
+
unless table.headers.include?(h)
|
208
|
+
raise UserError, "No '#{h}' column in table to sum in the group footer"
|
209
|
+
end
|
210
|
+
foot[h] = :sum
|
211
|
+
end
|
212
|
+
agg_cols.each do |h, agg|
|
213
|
+
unless table.headers.include?(h)
|
214
|
+
raise UserError, "No '#{h}' column in table to #{agg} in the group footer"
|
215
|
+
end
|
216
|
+
foot[h] = agg
|
217
|
+
end
|
218
|
+
@gfooters[label] = foot
|
219
|
+
self
|
220
|
+
end
|
221
|
+
|
222
|
+
# :category: Footers
|
223
|
+
|
224
|
+
# Add table footer to sum the +cols+ given as header symbols.
|
225
|
+
def sum_footer(*cols)
|
226
|
+
footer('Total', *cols)
|
227
|
+
end
|
228
|
+
|
229
|
+
# :category: Footers
|
230
|
+
|
231
|
+
# Add group footer to sum the +cols+ given as header symbols.
|
232
|
+
def sum_gfooter(*cols)
|
233
|
+
gfooter('Group Total', *cols)
|
234
|
+
end
|
235
|
+
|
236
|
+
# :category: Footers
|
237
|
+
|
238
|
+
# Add table footer to average the +cols+ given as header symbols.
|
239
|
+
def avg_footer(*cols)
|
240
|
+
hsh = {}
|
241
|
+
cols.each do |c|
|
242
|
+
hsh[c] = :avg
|
243
|
+
end
|
244
|
+
footer('Average', hsh)
|
245
|
+
end
|
246
|
+
|
247
|
+
# :category: Footers
|
248
|
+
|
249
|
+
# Add group footer to average the +cols+ given as header symbols.
|
250
|
+
def avg_gfooter(*cols)
|
251
|
+
hsh = {}
|
252
|
+
cols.each do |c|
|
253
|
+
hsh[c] = :avg
|
254
|
+
end
|
255
|
+
gfooter('Group Average', hsh)
|
256
|
+
end
|
257
|
+
|
258
|
+
# :category: Footers
|
259
|
+
|
260
|
+
# Add table footer to display the minimum value of the +cols+ given as
|
261
|
+
# header symbols.
|
262
|
+
def min_footer(*cols)
|
263
|
+
hsh = {}
|
264
|
+
cols.each do |c|
|
265
|
+
hsh[c] = :min
|
266
|
+
end
|
267
|
+
footer('Minimum', hsh)
|
268
|
+
end
|
269
|
+
|
270
|
+
# :category: Footers
|
271
|
+
|
272
|
+
# Add group footer to display the minimum value of the +cols+ given as
|
273
|
+
# header symbols.
|
274
|
+
def min_gfooter(*cols)
|
275
|
+
hsh = {}
|
276
|
+
cols.each do |c|
|
277
|
+
hsh[c] = :min
|
278
|
+
end
|
279
|
+
gfooter('Group Minimum', hsh)
|
280
|
+
end
|
281
|
+
|
282
|
+
# :category: Footers
|
283
|
+
|
284
|
+
# Add table footer to display the maximum value of the +cols+ given as
|
285
|
+
# header symbols.
|
286
|
+
def max_footer(*cols)
|
287
|
+
hsh = {}
|
288
|
+
cols.each do |c|
|
289
|
+
hsh[c] = :max
|
290
|
+
end
|
291
|
+
footer('Maximum', hsh)
|
292
|
+
end
|
293
|
+
|
294
|
+
# :category: Footers
|
295
|
+
|
296
|
+
# Add group footer to display the maximum value of the +cols+ given as
|
297
|
+
# header symbols.
|
298
|
+
def max_gfooter(*cols)
|
299
|
+
hsh = {}
|
300
|
+
cols.each do |c|
|
301
|
+
hsh[c] = :max
|
302
|
+
end
|
303
|
+
gfooter('Group Maximum', hsh)
|
304
|
+
end
|
305
|
+
|
306
|
+
############################################################################
|
307
|
+
# Formatting methods
|
308
|
+
#
|
309
|
+
#
|
310
|
+
# :category: Formatting
|
311
|
+
#
|
312
|
+
# A Formatter can specify a hash to hold the formatting instructions for
|
313
|
+
# columns by using the column head as a key and the value as the format
|
314
|
+
# instructions. In addition, the keys, :numeric, :string, :datetime,
|
315
|
+
# :boolean, and :nil, can be used to specify the default format instructions
|
316
|
+
# for columns of the given type is no other instructions have been given.
|
317
|
+
#
|
318
|
+
# Formatting instructions are strings, and what are valid strings depend on
|
319
|
+
# the type of the column:
|
320
|
+
#
|
321
|
+
# ==== String
|
322
|
+
#
|
323
|
+
# For string columns, the following instructions are valid:
|
324
|
+
#
|
325
|
+
# u:: convert the element to all lowercase,
|
326
|
+
#
|
327
|
+
# U:: convert the element to all uppercase,
|
328
|
+
#
|
329
|
+
# t:: title case the element, that is, upcase the initial letter in each
|
330
|
+
# word and lower case the other letters
|
331
|
+
#
|
332
|
+
# B or ~B:: make the element bold, or not
|
333
|
+
#
|
334
|
+
# I or ~I:: make the element italic, or not
|
335
|
+
#
|
336
|
+
# R:: align the element on the right of the column
|
337
|
+
#
|
338
|
+
# L:: align the element on the left of the column
|
339
|
+
#
|
340
|
+
# C:: align the element in the center of the column
|
341
|
+
#
|
342
|
+
# \c\[color\]:: render the element in the given color; the color can have
|
343
|
+
# the form fgcolor, fgcolor.bgcolor, or .bgcolor, to set the
|
344
|
+
# foreground or background colors respectively, and each of
|
345
|
+
# those can be an ANSI or X11 color name in addition to the
|
346
|
+
# special color, 'none', which keeps the terminal's default
|
347
|
+
# color.
|
348
|
+
#
|
349
|
+
# \_ or ~\_:: underline the element, or not,
|
350
|
+
#
|
351
|
+
# \* ~\*:: cause the element to blink, or not,
|
352
|
+
#
|
353
|
+
# ==== Numeric
|
354
|
+
#
|
355
|
+
# For a numeric, all the instructions valid for string are available, in
|
356
|
+
# addition to the following:
|
357
|
+
#
|
358
|
+
# , or ~, :: insert grouping commas, or not,
|
359
|
+
#
|
360
|
+
# $ or ~$:: format the number as currency according to the locale, or not,
|
361
|
+
#
|
362
|
+
# m.n:: include at least m digits before the decimal point, padding on the
|
363
|
+
# left with zeroes as needed, and round the number to the n decimal
|
364
|
+
# places and include n digits after the decimal point, padding on the
|
365
|
+
# right with zeroes as needed,
|
366
|
+
#
|
367
|
+
# H or ~H:: convert the number (assumed to be in units of seconds) to
|
368
|
+
# HH:MM:SS.ss form, or not. So a column that is the result of
|
369
|
+
# subtracting two :datetime forms will result in a :numeric
|
370
|
+
# expressed as seconds and can be displayed in hours, minutes, and
|
371
|
+
# seconds with this formatting instruction.
|
372
|
+
#
|
373
|
+
# ==== DateTime
|
374
|
+
#
|
375
|
+
# For a DateTime column, all the instructions valid for string are
|
376
|
+
# available, in addition to the following:
|
377
|
+
#
|
378
|
+
# \d\[fmt\]:: apply the format to a datetime that has no or zero hour,
|
379
|
+
# minute, second components, where fmt is a valid format string
|
380
|
+
# for Date#strftime, otherwise, the datetime will be formatted
|
381
|
+
# as an ISO 8601 string, YYYY-MM-DD.
|
382
|
+
#
|
383
|
+
# \D\[fmt\]:: apply the format to a datetime that has at least a non-zero
|
384
|
+
# hour component where fmt is a valid format string for
|
385
|
+
# Date#strftime, otherwise, the datetime will be formatted as an
|
386
|
+
# ISO 8601 string, YYYY-MM-DD.
|
387
|
+
#
|
388
|
+
# ==== Boolean
|
389
|
+
#
|
390
|
+
# All the instructions valid for string are available, in addition to the
|
391
|
+
# following:
|
392
|
+
#
|
393
|
+
# Y:: print true as 'Y' and false as 'N',
|
394
|
+
#
|
395
|
+
# T:: print true as 'T' and false as 'F',
|
396
|
+
#
|
397
|
+
# X:: print true as 'X' and false as '',
|
398
|
+
#
|
399
|
+
# \b\[xxx,yyy\] :: print true as the string given as xxx and false as the
|
400
|
+
# string given as yyy,
|
401
|
+
#
|
402
|
+
# \c\[tcolor,fcolor\]:: color a true element with tcolor and a false element
|
403
|
+
# with fcolor. Each of the colors may be specified in
|
404
|
+
# the same manner as colors for strings described
|
405
|
+
# above.
|
406
|
+
#
|
407
|
+
# ==== NilClass
|
408
|
+
#
|
409
|
+
# By default, nil elements are rendered as blank cells, but you can make
|
410
|
+
# them visible with the following, and in that case, all the formatting
|
411
|
+
# instructions valid for strings are available:
|
412
|
+
#
|
413
|
+
# \n\[niltext\]:: render a nil item with the given text.
|
414
|
+
def format(**fmts)
|
415
|
+
[:header, :bfirst, :gfirst, :body, :footer, :gfooter].each do |loc|
|
416
|
+
format_for(loc, fmts)
|
417
|
+
end
|
418
|
+
self
|
419
|
+
end
|
420
|
+
|
421
|
+
# :category: Formatting
|
422
|
+
#
|
423
|
+
# Define a formatting directives for the given location. The following are
|
424
|
+
# the valid +location+ symbols.
|
425
|
+
#
|
426
|
+
# :header:: instructions for the headers of the table,
|
427
|
+
#
|
428
|
+
# :bfirst:: instructions for the first row in the body of the table,
|
429
|
+
#
|
430
|
+
# :gfirst:: instructions for the cells in the first row of a group, to the
|
431
|
+
# extent not governed by :bfirst.
|
432
|
+
#
|
433
|
+
# :body:: instructions for the cells in the body of the table, to the extent
|
434
|
+
# they are not governed by :bfirst or :gfirst.
|
435
|
+
#
|
436
|
+
# :gfooter:: instructions for the cells of a group footer, and
|
437
|
+
#
|
438
|
+
# :footer:: instructions for the cells of a footer.
|
439
|
+
#
|
440
|
+
# Formatting directives are specified with hash arguments where the keys are
|
441
|
+
# either
|
442
|
+
#
|
443
|
+
# 1. the name of a table column in symbol form, or
|
444
|
+
#
|
445
|
+
# 2. the name of a column type in symbol form, i.e., :string, :numeric, or
|
446
|
+
# :datetime, :boolean, or :nil (for empty cells or untyped columns).
|
447
|
+
#
|
448
|
+
# The value given for the hash arguments should be strings that contain
|
449
|
+
# "directives" on how elements of that column or of that type are to be
|
450
|
+
# formatted on output. Formatting directives for a column name take
|
451
|
+
# precedence over those specified by type. And more specific locations take
|
452
|
+
# precedence over less specific ones.
|
453
|
+
#
|
454
|
+
# For example, the first line of a table is part of :body, :gfirst, and
|
455
|
+
# :bfirst, but since its identity as the first row of the table is the most
|
456
|
+
# specific (there is only one of those, there may be many rows that qualify
|
457
|
+
# as :gfirst, and even more that qualify as :body rows) any :bfirst
|
458
|
+
# specification would have priority over :gfirst or :body.
|
459
|
+
#
|
460
|
+
# For purposes of formatting, all headers are considered of the :string type
|
461
|
+
# and all nil cells are considered to be of the :nilclass type. All other
|
462
|
+
# cells have the type of the column to which they belong, including all
|
463
|
+
# cells in group or table footers. See ::format for details on formatting
|
464
|
+
# directives.
|
465
|
+
def format_for(location, **fmts)
|
466
|
+
unless LOCATIONS.include?(location)
|
467
|
+
raise UserError, "unknown format location '#{location}'"
|
468
|
+
end
|
469
|
+
valid_keys = table.headers + [:string, :numeric, :datetime, :boolean, :nil]
|
470
|
+
invalid_keys = (fmts.keys - valid_keys).uniq
|
471
|
+
unless invalid_keys.empty?
|
472
|
+
msg = "invalid #{location} column or type: #{invalid_keys.join(',')}"
|
473
|
+
raise UserError, msg
|
474
|
+
end
|
475
|
+
|
476
|
+
@format_at[location] ||= {}
|
477
|
+
table.headers.each do |h|
|
478
|
+
# Default formatting hash
|
479
|
+
format_h =
|
480
|
+
if format_at[location][h].empty?
|
481
|
+
default_format.dup
|
482
|
+
else
|
483
|
+
format_at[location][h].to_h
|
484
|
+
end
|
485
|
+
|
486
|
+
unless location == :header
|
487
|
+
# Merge in string and nil formatting, but not in header. Header is
|
488
|
+
# always typed a string, so it will get formatted in type-based
|
489
|
+
# formatting below. And headers are never nil.
|
490
|
+
if fmts.keys.include?(:string)
|
491
|
+
typ_fmt = parse_string_fmt(fmts[:string])
|
492
|
+
format_h = format_h.merge(typ_fmt)
|
493
|
+
end
|
494
|
+
if fmts.keys.include?(:nil)
|
495
|
+
typ_fmt = parse_nil_fmt(fmts[:nil]).first
|
496
|
+
format_h = format_h.merge(typ_fmt)
|
497
|
+
end
|
498
|
+
end
|
499
|
+
typ = location == :header ? :string : table.type(h).as_sym
|
500
|
+
parse_typ_method_name = 'parse_' + typ.to_s + '_fmt'
|
501
|
+
if fmts.keys.include?(typ)
|
502
|
+
# Merge in type-based formatting
|
503
|
+
typ_fmt = send(parse_typ_method_name, fmts[typ])
|
504
|
+
format_h = format_h.merge(typ_fmt)
|
505
|
+
end
|
506
|
+
if fmts[h]
|
507
|
+
# Merge in column formatting
|
508
|
+
col_fmt = send(parse_typ_method_name, fmts[h], strict: location != :header)
|
509
|
+
format_h = format_h.merge(col_fmt)
|
510
|
+
end
|
511
|
+
|
512
|
+
if location == :body
|
513
|
+
# Copy :body formatting for column h to :bfirst and :gfirst if they
|
514
|
+
# still have the default formatting. Can be overridden with a format_for
|
515
|
+
# call with those locations.
|
516
|
+
format_h.each_pair do |k, v|
|
517
|
+
if format_at[:bfirst][h].send(k) == default_format[k]
|
518
|
+
format_at[:bfirst][h].send("#{k}=", v)
|
519
|
+
end
|
520
|
+
if format_at[:gfirst][h].send(k) == default_format[k]
|
521
|
+
format_at[:gfirst][h].send("#{k}=", v)
|
522
|
+
end
|
523
|
+
end
|
524
|
+
elsif location == :gfirst
|
525
|
+
# Copy :gfirst formatting to :bfirst if it is still the default
|
526
|
+
format_h.each_pair do |k, v|
|
527
|
+
if format_at[:bfirst][h].send(k) == default_format[k]
|
528
|
+
format_at[:bfirst][h].send("#{k}=", v)
|
529
|
+
end
|
530
|
+
end
|
531
|
+
end
|
532
|
+
|
533
|
+
# Record its origin (using leading underscore so not to clash with any
|
534
|
+
# headers named h or location) and convert to struct
|
535
|
+
format_h[:_h] = h
|
536
|
+
format_h[:_location] = location
|
537
|
+
format_at[location][h] = OpenStruct.new(format_h)
|
538
|
+
end
|
539
|
+
self
|
540
|
+
end
|
541
|
+
|
542
|
+
###############################################################################
|
543
|
+
# Parsing and validation routines
|
544
|
+
###############################################################################
|
545
|
+
|
546
|
+
private
|
547
|
+
|
548
|
+
# Re to match a color name
|
549
|
+
CLR_RE = /(?:[-_a-zA-Z0-9 ]*)/
|
550
|
+
|
551
|
+
# Return a hash that reflects the formatting instructions given in the
|
552
|
+
# string fmt. Raise an error if it contains invalid formatting instructions.
|
553
|
+
# If fmt contains conflicting instructions, say C and L, there is no
|
554
|
+
# guarantee which will win, but it will not be considered an error to do so.
|
555
|
+
def parse_string_fmt(fmt, strict: true)
|
556
|
+
format, fmt = parse_str_fmt(fmt)
|
557
|
+
unless fmt.blank? || !strict
|
558
|
+
raise UserError, "unrecognized string formatting instructions '#{fmt}'"
|
559
|
+
end
|
560
|
+
format
|
561
|
+
end
|
562
|
+
|
563
|
+
# Utility method that extracts string instructions and returns a hash for
|
564
|
+
# of the instructions and the unconsumed part of the instruction string.
|
565
|
+
# This is called to cull string-based instructions from a formatting string
|
566
|
+
# intended for other types, such as numeric, etc.
|
567
|
+
def parse_str_fmt(fmt)
|
568
|
+
# We parse the more complex formatting constructs first, and after each
|
569
|
+
# parse, we remove the matched construct from fmt. At the end, any
|
570
|
+
# remaining characters in fmt should be invalid.
|
571
|
+
fmt_hash = {}
|
572
|
+
if fmt =~ /c\[(#{CLR_RE})(\.(#{CLR_RE}))?\]/
|
573
|
+
fmt_hash[:color] = $1 unless $1.blank?
|
574
|
+
fmt_hash[:bgcolor] = $3 unless $3.blank?
|
575
|
+
validate_color(fmt_hash[:color])
|
576
|
+
validate_color(fmt_hash[:bgcolor])
|
577
|
+
fmt = fmt.sub($&, '')
|
578
|
+
end
|
579
|
+
# Nil formatting can apply to strings as well
|
580
|
+
nil_hash, fmt = parse_nil_fmt(fmt)
|
581
|
+
fmt_hash = fmt_hash.merge(nil_hash)
|
582
|
+
if fmt =~ /u/
|
583
|
+
fmt_hash[:case] = :lower
|
584
|
+
fmt = fmt.sub($&, '')
|
585
|
+
end
|
586
|
+
if fmt =~ /U/
|
587
|
+
fmt_hash[:case] = :upper
|
588
|
+
fmt = fmt.sub($&, '')
|
589
|
+
end
|
590
|
+
if fmt =~ /t/
|
591
|
+
fmt_hash[:case] = :title
|
592
|
+
fmt = fmt.sub($&, '')
|
593
|
+
end
|
594
|
+
if fmt =~ /(~\s*)?B/
|
595
|
+
fmt_hash[:bold] = !!!$1
|
596
|
+
fmt = fmt.sub($&, '')
|
597
|
+
end
|
598
|
+
if fmt =~ /(~\s*)?I/
|
599
|
+
fmt_hash[:italic] = !!!$1
|
600
|
+
fmt = fmt.sub($&, '')
|
601
|
+
end
|
602
|
+
if fmt =~ /R/
|
603
|
+
fmt_hash[:alignment] = :right
|
604
|
+
fmt = fmt.sub($&, '')
|
605
|
+
end
|
606
|
+
if fmt =~ /C/
|
607
|
+
fmt_hash[:alignment] = :center
|
608
|
+
fmt = fmt.sub($&, '')
|
609
|
+
end
|
610
|
+
if fmt =~ /L/
|
611
|
+
fmt_hash[:alignment] = :left
|
612
|
+
fmt = fmt.sub($&, '')
|
613
|
+
end
|
614
|
+
if fmt =~ /(~\s*)?_/
|
615
|
+
fmt_hash[:underline] = !!!$1
|
616
|
+
fmt = fmt.sub($&, '')
|
617
|
+
end
|
618
|
+
if fmt =~ /(~\s*)?\*/
|
619
|
+
fmt_hash[:blink] = !!!$1
|
620
|
+
fmt = fmt.sub($&, '')
|
621
|
+
end
|
622
|
+
[fmt_hash, fmt]
|
623
|
+
end
|
624
|
+
|
625
|
+
# Utility method that extracts nil instructions and returns a hash of the
|
626
|
+
# instructions and the unconsumed part of the instruction string. This is
|
627
|
+
# called to cull nil-based instructions from a formatting string intended
|
628
|
+
# for other types, such as numeric, etc.
|
629
|
+
def parse_nil_fmt(fmt, _strict: true)
|
630
|
+
# We parse the more complex formatting constructs first, and after each
|
631
|
+
# parse, we remove the matched construct from fmt. At the end, any
|
632
|
+
# remaining characters in fmt should be invalid.
|
633
|
+
fmt_hash = {}
|
634
|
+
if fmt =~ /n\[\s*([^\]]*)\s*\]/
|
635
|
+
fmt_hash[:nil_text] = $1.clean
|
636
|
+
fmt = fmt.sub($&, '')
|
637
|
+
end
|
638
|
+
[fmt_hash, fmt]
|
639
|
+
end
|
640
|
+
|
641
|
+
# Return a hash that reflects the numeric or string formatting instructions
|
642
|
+
# given in the string fmt. Raise an error if it contains invalid formatting
|
643
|
+
# instructions. If fmt contains conflicting instructions, there is no
|
644
|
+
# guarantee which will win, but it will not be considered an error to do so.
|
645
|
+
def parse_numeric_fmt(fmt, strict: true)
|
646
|
+
# We parse the more complex formatting constructs first, and after each
|
647
|
+
# parse, we remove the matched construct from fmt. At the end, any
|
648
|
+
# remaining characters in fmt should be invalid.
|
649
|
+
fmt_hash, fmt = parse_str_fmt(fmt)
|
650
|
+
if fmt =~ /(\d+).(\d+)/
|
651
|
+
fmt_hash[:pre_digits] = $1.to_i
|
652
|
+
fmt_hash[:post_digits] = $2.to_i
|
653
|
+
fmt = fmt.sub($&, '')
|
654
|
+
end
|
655
|
+
if fmt =~ /(~\s*)?,/
|
656
|
+
fmt_hash[:commas] = !!!$1
|
657
|
+
fmt = fmt.sub($&, '')
|
658
|
+
end
|
659
|
+
if fmt =~ /(~\s*)?\$/
|
660
|
+
fmt_hash[:currency] = !!!$1
|
661
|
+
fmt = fmt.sub($&, '')
|
662
|
+
end
|
663
|
+
if fmt =~ /(~\s*)?H/
|
664
|
+
fmt_hash[:hms] = !!!$1
|
665
|
+
fmt = fmt.sub($&, '')
|
666
|
+
end
|
667
|
+
unless fmt.blank? || !strict
|
668
|
+
raise UserError, "unrecognized numeric formatting instructions '#{fmt}'"
|
669
|
+
end
|
670
|
+
fmt_hash
|
671
|
+
end
|
672
|
+
|
673
|
+
# Return a hash that reflects the datetime or string formatting instructions
|
674
|
+
# given in the string fmt. Raise an error if it contains invalid formatting
|
675
|
+
# instructions. If fmt contains conflicting instructions, there is no
|
676
|
+
# guarantee which will win, but it will not be considered an error to do so.
|
677
|
+
def parse_datetime_fmt(fmt, strict: true)
|
678
|
+
# We parse the more complex formatting constructs first, and after each
|
679
|
+
# parse, we remove the matched construct from fmt. At the end, any
|
680
|
+
# remaining characters in fmt should be invalid.
|
681
|
+
fmt_hash, fmt = parse_str_fmt(fmt)
|
682
|
+
if fmt =~ /d\[([^\]]*)\]/
|
683
|
+
fmt_hash[:date_fmt] = $1
|
684
|
+
fmt = fmt.sub($&, '')
|
685
|
+
end
|
686
|
+
if fmt =~ /D\[([^\]]*)\]/
|
687
|
+
fmt_hash[:date_fmt] = $1
|
688
|
+
fmt = fmt.sub($&, '')
|
689
|
+
end
|
690
|
+
unless fmt.blank? || !strict
|
691
|
+
raise UserError, "unrecognized datetime formatting instructions '#{fmt}'"
|
692
|
+
end
|
693
|
+
fmt_hash
|
694
|
+
end
|
695
|
+
|
696
|
+
# Return a hash that reflects the boolean or string formatting instructions
|
697
|
+
# given in the string fmt. Raise an error if it contains invalid formatting
|
698
|
+
# instructions. If fmt contains conflicting instructions, there is no
|
699
|
+
# guarantee which will win, but it will not be considered an error to do so.
|
700
|
+
def parse_boolean_fmt(fmt, strict: true)
|
701
|
+
# We parse the more complex formatting constructs first, and after each
|
702
|
+
# parse, we remove the matched construct from fmt. At the end, any
|
703
|
+
# remaining characters in fmt should be invalid.
|
704
|
+
fmt_hash = {}
|
705
|
+
if fmt =~ /b\[\s*([^\],]*),([^\]]*)\s*\]/
|
706
|
+
fmt_hash[:true_text] = $1.clean
|
707
|
+
fmt_hash[:false_text] = $2.clean
|
708
|
+
fmt = fmt.sub($&, '')
|
709
|
+
end
|
710
|
+
# Since true_text, false_text and nil_text may want to have internal
|
711
|
+
# spaces, defer removing extraneous spaces until after they are parsed.
|
712
|
+
if fmt =~ /c\[(#{CLR_RE})(\.(#{CLR_RE}))?,\s*(#{CLR_RE})(\.(#{CLR_RE}))?\]/
|
713
|
+
fmt_hash[:true_color] = $1 unless $1.blank?
|
714
|
+
fmt_hash[:true_bgcolor] = $3 unless $3.blank?
|
715
|
+
fmt_hash[:false_color] = $4 unless $4.blank?
|
716
|
+
fmt_hash[:false_bgcolor] = $6 unless $6.blank?
|
717
|
+
fmt = fmt.sub($&, '')
|
718
|
+
end
|
719
|
+
str_fmt_hash, fmt = parse_str_fmt(fmt)
|
720
|
+
fmt_hash = fmt_hash.merge(str_fmt_hash)
|
721
|
+
if fmt =~ /Y/
|
722
|
+
fmt_hash[:true_text] = 'Y'
|
723
|
+
fmt_hash[:false_text] = 'N'
|
724
|
+
fmt = fmt.sub($&, '')
|
725
|
+
end
|
726
|
+
if fmt =~ /T/
|
727
|
+
fmt_hash[:true_text] = 'T'
|
728
|
+
fmt_hash[:false_text] = 'F'
|
729
|
+
fmt = fmt.sub($&, '')
|
730
|
+
end
|
731
|
+
if fmt =~ /X/
|
732
|
+
fmt_hash[:true_text] = 'X'
|
733
|
+
fmt_hash[:false_text] = ''
|
734
|
+
fmt = fmt.sub($&, '')
|
735
|
+
end
|
736
|
+
unless fmt.blank? || !strict
|
737
|
+
raise UserError, "unrecognized boolean formatting instructions '#{fmt}'"
|
738
|
+
end
|
739
|
+
fmt_hash
|
740
|
+
end
|
741
|
+
|
742
|
+
###############################################################################
|
743
|
+
# Applying formatting
|
744
|
+
###############################################################################
|
745
|
+
|
746
|
+
public
|
747
|
+
|
748
|
+
# :stopdoc:
|
749
|
+
|
750
|
+
# Convert a value to a string based on the instructions in istruct,
|
751
|
+
# depending on the type of val. "Formatting," which changes the content of
|
752
|
+
# the string, such as adding commas, is always performed, except alignment
|
753
|
+
# which is only performed when the width parameter is non-nil. "Decorating",
|
754
|
+
# which changes the appearance without changing the content, is performed
|
755
|
+
# only if the decorate parameter is true.
|
756
|
+
def format_cell(val, istruct, width: nil, decorate: false)
|
757
|
+
case val
|
758
|
+
when Numeric
|
759
|
+
str = format_numeric(val, istruct)
|
760
|
+
str = format_string(str, istruct, width)
|
761
|
+
decorate ? decorate_string(str, istruct) : str
|
762
|
+
when DateTime, Date
|
763
|
+
str = format_datetime(val, istruct)
|
764
|
+
str = format_string(str, istruct, width)
|
765
|
+
decorate ? decorate_string(str, istruct) : str
|
766
|
+
when TrueClass
|
767
|
+
str = format_boolean(val, istruct)
|
768
|
+
str = format_string(str, istruct, width)
|
769
|
+
true_istruct = istruct.dup
|
770
|
+
true_istruct.color = istruct.true_color
|
771
|
+
true_istruct.bgcolor = istruct.true_bgcolor
|
772
|
+
decorate ? decorate_string(str, true_istruct) : str
|
773
|
+
when FalseClass
|
774
|
+
str = format_boolean(val, istruct)
|
775
|
+
str = format_string(str, istruct, width)
|
776
|
+
false_istruct = istruct.dup
|
777
|
+
false_istruct.color = istruct.false_color
|
778
|
+
false_istruct.bgcolor = istruct.false_bgcolor
|
779
|
+
decorate ? decorate_string(str, false_istruct) : str
|
780
|
+
when NilClass
|
781
|
+
str = istruct.nil_text
|
782
|
+
str = format_string(str, istruct, width)
|
783
|
+
decorate ? decorate_string(str, istruct) : str
|
784
|
+
when String
|
785
|
+
str = format_string(val, istruct, width)
|
786
|
+
decorate ? decorate_string(str, istruct) : str
|
787
|
+
else
|
788
|
+
raise UserError,
|
789
|
+
"cannot format value '#{val}' of class #{val.class}"
|
790
|
+
end
|
791
|
+
end
|
792
|
+
|
793
|
+
private
|
794
|
+
|
795
|
+
# Add LaTeX control sequences, ANSI terminal escape codes, or other
|
796
|
+
# decorations to string to decorate it with the given attributes. None of
|
797
|
+
# the decorations may affect the displayed width of the string. Return the
|
798
|
+
# decorated string.
|
799
|
+
def decorate_string(str, _istruct)
|
800
|
+
str
|
801
|
+
end
|
802
|
+
|
803
|
+
# Convert a boolean to a string according to instructions in istruct, which
|
804
|
+
# is assumed to be the result of parsing a formatting instruction string as
|
805
|
+
# above. Only device-independent formatting is done here. Device dependent
|
806
|
+
# formatting (e.g., color) can be done in a subclass of Formatter by
|
807
|
+
# specializing this method.
|
808
|
+
def format_boolean(val, istruct)
|
809
|
+
return istruct.nil_text if val.nil?
|
810
|
+
val ? istruct.true_text : istruct.false_text
|
811
|
+
end
|
812
|
+
|
813
|
+
# Convert a datetime to a string according to instructions in istruct, which
|
814
|
+
# is assumed to be the result of parsing a formatting instruction string as
|
815
|
+
# above. Only device-independent formatting is done here. Device dependent
|
816
|
+
# formatting (e.g., color) can be done in a subclass of Formatter by
|
817
|
+
# specializing this method.
|
818
|
+
def format_datetime(val, istruct)
|
819
|
+
return istruct.nil_text if val.nil?
|
820
|
+
if val.to_date == val
|
821
|
+
# It is a Date, with no time component.
|
822
|
+
val.strftime(istruct.date_fmt)
|
823
|
+
else
|
824
|
+
val.strftime(istruct.datetime_fmt)
|
825
|
+
end
|
826
|
+
end
|
827
|
+
|
828
|
+
# Convert a numeric to a string according to instructions in istruct, which
|
829
|
+
# is assumed to be the result of parsing a formatting instruction string as
|
830
|
+
# above. Only device-independent formatting is done here. Device dependent
|
831
|
+
# formatting (e.g., color) can be done in a subclass of Formatter by
|
832
|
+
# specializing this method.
|
833
|
+
def format_numeric(val, istruct)
|
834
|
+
return istruct.nil_text if val.nil?
|
835
|
+
val = val.round(istruct.post_digits) if istruct.post_digits >= 0
|
836
|
+
if istruct.hms
|
837
|
+
result = val.secs_to_hms
|
838
|
+
istruct.commas = false
|
839
|
+
elsif istruct.currency
|
840
|
+
prec = istruct.post_digits == 0 ? 2 : istruct.post_digits
|
841
|
+
delim = istruct.commas ? ',' : ''
|
842
|
+
result = val.to_s(:currency, precision: prec, delimiter: delim,
|
843
|
+
unit: FatTable.currency_symbol)
|
844
|
+
istruct.commas = false
|
845
|
+
elsif istruct.pre_digits.positive?
|
846
|
+
if val.whole?
|
847
|
+
# No fractional part, ignore post_digits
|
848
|
+
result = sprintf("%0#{istruct.pre_digits}d", val)
|
849
|
+
elsif istruct.post_digits >= 0
|
850
|
+
# There's a fractional part and pre_digits. sprintf width includes
|
851
|
+
# space for fractional part and decimal point, so add pre, post, and 1
|
852
|
+
# to get the proper sprintf width.
|
853
|
+
wid = istruct.pre_digits + 1 + istruct.post_digits
|
854
|
+
result = sprintf("%0#{wid}.#{istruct.post_digits}f", val)
|
855
|
+
else
|
856
|
+
val = val.round(0)
|
857
|
+
result = sprintf("%0#{istruct.pre_digits}d", val)
|
858
|
+
end
|
859
|
+
elsif istruct.post_digits >= 0
|
860
|
+
# Round to post_digits but no padding of whole number, pad fraction with
|
861
|
+
# trailing zeroes.
|
862
|
+
result = sprintf("%.#{istruct.post_digits}f", val)
|
863
|
+
else
|
864
|
+
result = val.to_s
|
865
|
+
end
|
866
|
+
if istruct.commas
|
867
|
+
# Commify the whole number part if not done already.
|
868
|
+
result = result.commify
|
869
|
+
end
|
870
|
+
result
|
871
|
+
end
|
872
|
+
|
873
|
+
# Apply non-device-dependent string formatting instructions.
|
874
|
+
def format_string(val, istruct, width = nil)
|
875
|
+
val = istruct.nil_text if val.nil?
|
876
|
+
val =
|
877
|
+
case istruct.case
|
878
|
+
when :lower
|
879
|
+
val.downcase
|
880
|
+
when :upper
|
881
|
+
val.upcase
|
882
|
+
when :title
|
883
|
+
val.entitle
|
884
|
+
when :none
|
885
|
+
val
|
886
|
+
end
|
887
|
+
if width && aligned?
|
888
|
+
pad = width - width(val)
|
889
|
+
case istruct.alignment
|
890
|
+
when :left
|
891
|
+
val += ' ' * pad
|
892
|
+
when :right
|
893
|
+
val = ' ' * pad + val
|
894
|
+
when :center
|
895
|
+
lpad = pad / 2 + (pad.odd? ? 1 : 0)
|
896
|
+
rpad = pad / 2
|
897
|
+
val = ' ' * lpad + val + ' ' * rpad
|
898
|
+
else
|
899
|
+
val = val
|
900
|
+
end
|
901
|
+
val = ' ' + val + ' '
|
902
|
+
end
|
903
|
+
val
|
904
|
+
end
|
905
|
+
|
906
|
+
###############################################################################
|
907
|
+
# Output routines
|
908
|
+
###############################################################################
|
909
|
+
|
910
|
+
public
|
911
|
+
|
912
|
+
# :startdoc:
|
913
|
+
|
914
|
+
# :category: Output
|
915
|
+
|
916
|
+
# Return the +table+ as either a string in the target format or as a Ruby
|
917
|
+
# data structure if that is the target. In the latter case, all the cells
|
918
|
+
# are converted to strings formatted according to the Formatter's formatting
|
919
|
+
# directives given in Formatter.format_for or Formatter.format.
|
920
|
+
def output
|
921
|
+
# This results in a hash of two-element arrays. The key is the header and
|
922
|
+
# the value is an array of the header and formatted header. We do the
|
923
|
+
# latter so the structure parallels the structure for rows explained next.
|
924
|
+
formatted_headers = build_formatted_headers
|
925
|
+
|
926
|
+
# These produce an array with each element representing a row of the
|
927
|
+
# table. Each element of the array is a two-element array. The location of
|
928
|
+
# the row in the table (:bfirst, :body, :gfooter, etc.) is the first
|
929
|
+
# element and a hash of the row is the second element. The keys for the
|
930
|
+
# hash are the row headers as in the Table, but the values are two element
|
931
|
+
# arrays as well. First is the raw, unformatted value of the cell, the
|
932
|
+
# second is a string of the first value formatted according to the
|
933
|
+
# instructions for the column and location in which it appears. The
|
934
|
+
# formatting done on this pass is only formatting that affects the
|
935
|
+
# contents of the cells, such as inserting commas, that would affect the
|
936
|
+
# width of the columns as displayed. We keep both the raw value and
|
937
|
+
# unformatted value around because we have to make two passes over the
|
938
|
+
# table if there is any alignment, and we want to know the type of the raw
|
939
|
+
# element for the second pass of formatting for type-specific formatting
|
940
|
+
# (e.g., true_color, false_color, etc.).
|
941
|
+
new_rows = build_formatted_body
|
942
|
+
new_rows += build_formatted_footers
|
943
|
+
|
944
|
+
# Having formatted the cells, we can now compute column widths so we can
|
945
|
+
# do any alignment called for if this is a Formatter that performs its own
|
946
|
+
# alignment. On this pass, we also decorate the cells with colors, bold,
|
947
|
+
# etc.
|
948
|
+
if aligned?
|
949
|
+
widths = width_map(formatted_headers, new_rows)
|
950
|
+
table.headers.each do |h|
|
951
|
+
fmt_h = formatted_headers[h].last
|
952
|
+
istruct = format_at[:header][h]
|
953
|
+
formatted_headers[h] =
|
954
|
+
[h, format_cell(fmt_h, istruct, width: widths[h], decorate: true)]
|
955
|
+
end
|
956
|
+
aligned_rows = []
|
957
|
+
new_rows.each do |loc_row|
|
958
|
+
if loc_row.nil?
|
959
|
+
aligned_rows << nil
|
960
|
+
next
|
961
|
+
end
|
962
|
+
loc, row = *loc_row
|
963
|
+
aligned_row = {}
|
964
|
+
row.each_pair do |h, (val, _fmt_v)|
|
965
|
+
istruct = format_at[loc][h]
|
966
|
+
aligned_row[h] =
|
967
|
+
[val, format_cell(val, istruct, width: widths[h], decorate: true)]
|
968
|
+
end
|
969
|
+
aligned_rows << [loc, aligned_row]
|
970
|
+
end
|
971
|
+
new_rows = aligned_rows
|
972
|
+
end
|
973
|
+
|
974
|
+
# Now that the contents of the output table cells have been computed and
|
975
|
+
# alignment applied, we can actually construct the table using the methods
|
976
|
+
# for constructing table parts, pre_table, etc. We expect that these will
|
977
|
+
# be overridden by subclasses of Formatter for specific output targets. In
|
978
|
+
# any event, the result is a single string (or ruby object if eval is true
|
979
|
+
# for the Formatter) representing the table in the syntax of the output
|
980
|
+
# target.
|
981
|
+
result = ''
|
982
|
+
result += pre_table
|
983
|
+
if include_header_row?
|
984
|
+
result += pre_header(widths)
|
985
|
+
result += pre_row
|
986
|
+
cells = []
|
987
|
+
formatted_headers.each_pair do |h, (_v, fmt_v)|
|
988
|
+
cells << pre_cell(h) + quote_cell(fmt_v) + post_cell
|
989
|
+
end
|
990
|
+
result += cells.join(inter_cell)
|
991
|
+
result += post_row
|
992
|
+
result += post_header(widths)
|
993
|
+
end
|
994
|
+
new_rows.each do |loc_row|
|
995
|
+
result += hline(widths) if loc_row.nil?
|
996
|
+
next if loc_row.nil?
|
997
|
+
_loc, row = *loc_row
|
998
|
+
result += pre_row
|
999
|
+
cells = []
|
1000
|
+
row.each_pair do |h, (_v, fmt_v)|
|
1001
|
+
cells << pre_cell(h) + quote_cell(fmt_v) + post_cell
|
1002
|
+
end
|
1003
|
+
result += cells.join(inter_cell)
|
1004
|
+
result += post_row
|
1005
|
+
end
|
1006
|
+
result += post_footers(widths)
|
1007
|
+
result += post_table
|
1008
|
+
|
1009
|
+
# If this Formatter targets a ruby data structure (e.g., AoaFormatter), we
|
1010
|
+
# eval the string to get the object.
|
1011
|
+
evaluate? ? eval(result) : result
|
1012
|
+
end
|
1013
|
+
|
1014
|
+
private
|
1015
|
+
|
1016
|
+
# Return a hash mapping the table's headers to their formatted versions. If
|
1017
|
+
# a hash of column widths is given, perform alignment within the given field
|
1018
|
+
# widths.
|
1019
|
+
def build_formatted_headers(widths = {})
|
1020
|
+
# Don't decorate if this Formatter calls for alignment. It will be done
|
1021
|
+
# in the second pass.
|
1022
|
+
decorate = !aligned?
|
1023
|
+
map = {}
|
1024
|
+
table.headers.each do |h|
|
1025
|
+
istruct = format_at[:header][h]
|
1026
|
+
map[h] = [h, format_cell(h.as_string, istruct, decorate: decorate)]
|
1027
|
+
end
|
1028
|
+
map
|
1029
|
+
end
|
1030
|
+
|
1031
|
+
# Return an array of two-element arrays, with the first element of the inner
|
1032
|
+
# array being the location of the row and the second element being a hash,
|
1033
|
+
# using the table's headers as keys and an array of the raw and
|
1034
|
+
# formatted cells as the values. Add formatted group footers along the way.
|
1035
|
+
def build_formatted_body
|
1036
|
+
# Don't decorate if this Formatter calls for alignment. It will be done
|
1037
|
+
# in the second pass.
|
1038
|
+
decorate = !aligned?
|
1039
|
+
new_rows = []
|
1040
|
+
tbl_row_k = 0
|
1041
|
+
table.groups.each_with_index do |grp, grp_k|
|
1042
|
+
# Mark the beginning of a group if this is the first group after the
|
1043
|
+
# header or the second or later group.
|
1044
|
+
new_rows << nil if include_header_row? || grp_k.positive?
|
1045
|
+
# Compute group body
|
1046
|
+
grp_col = {}
|
1047
|
+
grp.each_with_index do |row, grp_row_k|
|
1048
|
+
new_row = {}
|
1049
|
+
location =
|
1050
|
+
if tbl_row_k.zero?
|
1051
|
+
:bfirst
|
1052
|
+
elsif grp_row_k.zero?
|
1053
|
+
:gfirst
|
1054
|
+
else
|
1055
|
+
:body
|
1056
|
+
end
|
1057
|
+
table.headers.each do |h|
|
1058
|
+
grp_col[h] ||= Column.new(header: h)
|
1059
|
+
grp_col[h] << row[h]
|
1060
|
+
istruct = format_at[location][h]
|
1061
|
+
new_row[h] = [row[h], format_cell(row[h], istruct, decorate: decorate)]
|
1062
|
+
end
|
1063
|
+
new_rows << [location, new_row]
|
1064
|
+
tbl_row_k += 1
|
1065
|
+
end
|
1066
|
+
# Compute group footers
|
1067
|
+
gfooters.each_pair do |label, gfooter|
|
1068
|
+
# Mark the beginning of a group footer
|
1069
|
+
new_rows << nil
|
1070
|
+
gfoot_row = {}
|
1071
|
+
first_h = nil
|
1072
|
+
grp_col.each_pair do |h, col|
|
1073
|
+
first_h ||= h
|
1074
|
+
gfoot_row[h] =
|
1075
|
+
if gfooter[h]
|
1076
|
+
val = col.send(gfooter[h])
|
1077
|
+
istruct = format_at[:gfooter][h]
|
1078
|
+
[val, format_cell(val, istruct, decorate: decorate)]
|
1079
|
+
else
|
1080
|
+
[nil, '']
|
1081
|
+
end
|
1082
|
+
end
|
1083
|
+
if gfoot_row[first_h].last.blank?
|
1084
|
+
istruct = format_at[:gfooter][first_h]
|
1085
|
+
gfoot_row[first_h] =
|
1086
|
+
[label, format_cell(label, istruct, decorate: decorate)]
|
1087
|
+
end
|
1088
|
+
new_rows << [:gfooter, gfoot_row]
|
1089
|
+
end
|
1090
|
+
end
|
1091
|
+
new_rows
|
1092
|
+
end
|
1093
|
+
|
1094
|
+
def build_formatted_footers
|
1095
|
+
# Don't decorate if this Formatter calls for alignment. It will be done
|
1096
|
+
# in the second pass.
|
1097
|
+
decorate = !aligned?
|
1098
|
+
new_rows = []
|
1099
|
+
# Done with body, compute the table footers.
|
1100
|
+
footers.each_pair do |label, footer|
|
1101
|
+
# Mark the beginning of a footer
|
1102
|
+
new_rows << nil
|
1103
|
+
foot_row = {}
|
1104
|
+
first_h = nil
|
1105
|
+
table.columns.each do |col|
|
1106
|
+
h = col.header
|
1107
|
+
first_h ||= h
|
1108
|
+
foot_row[h] =
|
1109
|
+
if footer[h]
|
1110
|
+
val = col.send(footer[h])
|
1111
|
+
istruct = format_at[:footer][h]
|
1112
|
+
[val, format_cell(val, istruct, decorate: decorate)]
|
1113
|
+
else
|
1114
|
+
[nil, '']
|
1115
|
+
end
|
1116
|
+
end
|
1117
|
+
# Put the label in the first column of footer unless it has been
|
1118
|
+
# formatted as part of footer.
|
1119
|
+
if foot_row[first_h].last.blank?
|
1120
|
+
istruct = format_at[:footer][first_h]
|
1121
|
+
foot_row[first_h] =
|
1122
|
+
[label, format_cell(label, istruct, decorate: decorate)]
|
1123
|
+
end
|
1124
|
+
new_rows << [:footer, foot_row]
|
1125
|
+
end
|
1126
|
+
new_rows
|
1127
|
+
end
|
1128
|
+
|
1129
|
+
# Return a hash of the maximum widths of all the given headers and rows.
|
1130
|
+
def width_map(formatted_headers, rows)
|
1131
|
+
widths = {}
|
1132
|
+
formatted_headers.each_pair do |h, (_v, fmt_v)|
|
1133
|
+
widths[h] ||= 0
|
1134
|
+
widths[h] = [widths[h], width(fmt_v)].max
|
1135
|
+
end
|
1136
|
+
rows.each do |loc_row|
|
1137
|
+
next if loc_row.nil?
|
1138
|
+
_loc, row = *loc_row
|
1139
|
+
row.each_pair do |h, (_v, fmt_v)|
|
1140
|
+
widths[h] ||= 0
|
1141
|
+
widths[h] = [widths[h], width(fmt_v)].max
|
1142
|
+
end
|
1143
|
+
end
|
1144
|
+
widths
|
1145
|
+
end
|
1146
|
+
|
1147
|
+
# Raise an error unless the given color is valid for this Formatter.
|
1148
|
+
def validate_color(clr)
|
1149
|
+
return true unless clr
|
1150
|
+
raise UserError, invalid_color_msg(clr) unless color_valid?(clr)
|
1151
|
+
end
|
1152
|
+
|
1153
|
+
###########################################################################
|
1154
|
+
# Class-specific methods. Many of these should be overriden in any subclass
|
1155
|
+
# of Formatter to implement a specific target output medium.
|
1156
|
+
###########################################################################
|
1157
|
+
|
1158
|
+
# Return whether clr is a valid color for this Formatter
|
1159
|
+
def color_valid?(_clr)
|
1160
|
+
true
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
# Return an error message string to display when clr is an invalid color.
|
1164
|
+
def invalid_color_msg(_clr)
|
1165
|
+
''
|
1166
|
+
end
|
1167
|
+
|
1168
|
+
# Does this Formatter require a second pass over the cells to align the
|
1169
|
+
# columns according to the alignment formatting instruction to the width of
|
1170
|
+
# the widest cell in each column? If no alignment is needed, as for
|
1171
|
+
# AoaFormatter, or if the external target medium does alignment, as for
|
1172
|
+
# LaTeXFormatter, this should be false. For TextFormatter or TermFormatter,
|
1173
|
+
# where we must pad out the cells with spaces, it should be true.
|
1174
|
+
def aligned?
|
1175
|
+
false
|
1176
|
+
end
|
1177
|
+
|
1178
|
+
# Should the string result of #output be evaluated to form a Ruby data
|
1179
|
+
# structure? For example, AoaFormatter wants to return an array of arrays of
|
1180
|
+
# strings, so it should build a ruby expression to do that, then have it
|
1181
|
+
# eval'ed.
|
1182
|
+
def evaluate?
|
1183
|
+
false
|
1184
|
+
end
|
1185
|
+
|
1186
|
+
# Compute the width of the string as displayed, taking into account the
|
1187
|
+
# characteristics of the target device. For example, a colored string
|
1188
|
+
# should not include in the width terminal control characters that simply
|
1189
|
+
# change the color without occupying any space. Thus, this method must be
|
1190
|
+
# overridden in a subclass if a simple character count does not reflect the
|
1191
|
+
# width as displayed.
|
1192
|
+
def width(str)
|
1193
|
+
str.length
|
1194
|
+
end
|
1195
|
+
|
1196
|
+
def pre_table
|
1197
|
+
''
|
1198
|
+
end
|
1199
|
+
|
1200
|
+
def post_table
|
1201
|
+
''
|
1202
|
+
end
|
1203
|
+
|
1204
|
+
def include_header_row?
|
1205
|
+
true
|
1206
|
+
end
|
1207
|
+
|
1208
|
+
def pre_header(_widths)
|
1209
|
+
''
|
1210
|
+
end
|
1211
|
+
|
1212
|
+
def post_header(_widths)
|
1213
|
+
''
|
1214
|
+
end
|
1215
|
+
|
1216
|
+
def pre_row
|
1217
|
+
''
|
1218
|
+
end
|
1219
|
+
|
1220
|
+
def pre_cell(_h)
|
1221
|
+
''
|
1222
|
+
end
|
1223
|
+
|
1224
|
+
def quote_cell(v)
|
1225
|
+
v
|
1226
|
+
end
|
1227
|
+
|
1228
|
+
def post_cell
|
1229
|
+
''
|
1230
|
+
end
|
1231
|
+
|
1232
|
+
def inter_cell
|
1233
|
+
'|'
|
1234
|
+
end
|
1235
|
+
|
1236
|
+
def post_row
|
1237
|
+
"\n"
|
1238
|
+
end
|
1239
|
+
|
1240
|
+
def hline(_widths)
|
1241
|
+
''
|
1242
|
+
end
|
1243
|
+
|
1244
|
+
def post_footers(_widths)
|
1245
|
+
''
|
1246
|
+
end
|
1247
|
+
end
|
1248
|
+
end
|