fat_table 0.2.2
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 +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
|