fat_table 0.2.7 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.gitignore +2 -0
- data/.rubocop.yml +18 -0
- data/.travis.yml +7 -4
- data/.yardopts +5 -1
- data/Gemfile +2 -0
- data/README.org +82 -76
- data/README.rdoc +4 -4
- data/fat_table.gemspec +8 -8
- data/lib/fat_table.rb +14 -3
- data/lib/fat_table/column.rb +39 -27
- data/lib/fat_table/db_handle.rb +19 -47
- data/lib/fat_table/errors.rb +2 -0
- data/lib/fat_table/evaluator.rb +11 -5
- data/lib/fat_table/formatters.rb +2 -0
- data/lib/fat_table/formatters/aoa_formatter.rb +7 -5
- data/lib/fat_table/formatters/aoh_formatter.rb +8 -6
- data/lib/fat_table/formatters/formatter.rb +78 -61
- data/lib/fat_table/formatters/latex_formatter.rb +7 -5
- data/lib/fat_table/formatters/org_formatter.rb +5 -3
- data/lib/fat_table/formatters/term_formatter.rb +33 -28
- data/lib/fat_table/formatters/text_formatter.rb +5 -3
- data/lib/fat_table/patches.rb +5 -2
- data/lib/fat_table/table.rb +78 -57
- data/lib/fat_table/version.rb +3 -1
- data/{README.md → md/README.md} +5 -6
- metadata +49 -39
data/lib/fat_table.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This module provides objects for treating tables as a data type on which you
|
2
4
|
# can (1) perform operations, such as select, where, join, and others and (2)
|
3
5
|
# output the tables in several formats, including text, ANSI terminal, LaTeX,
|
@@ -24,6 +26,15 @@ module FatTable
|
|
24
26
|
require 'fat_table/db_handle'
|
25
27
|
require 'fat_table/errors'
|
26
28
|
|
29
|
+
# Add paths for common db gems to the load paths
|
30
|
+
%w[pg mysql2 sqlite].each do |gem_name|
|
31
|
+
path = Dir.glob("#{ENV['GEM_HOME']}/gems/#{gem_name}*").sort.last
|
32
|
+
if path
|
33
|
+
path = File.join(path, 'lib')
|
34
|
+
$LOAD_PATH << path unless $LOAD_PATH.include?(path)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
27
38
|
# Valid output formats as symbols.
|
28
39
|
FORMATS = %i[psv aoa aoh latex org term text].freeze
|
29
40
|
|
@@ -114,7 +125,7 @@ module FatTable
|
|
114
125
|
end
|
115
126
|
|
116
127
|
# Construct a Table by running a SQL query against the database set up with
|
117
|
-
# FatTable.
|
128
|
+
# FatTable.connect. Return the Table with the query results as rows and the
|
118
129
|
# headers from the query, converted to symbols, as headers.
|
119
130
|
def self.from_sql(query)
|
120
131
|
Table.from_sql(query)
|
@@ -149,9 +160,9 @@ module FatTable
|
|
149
160
|
raise UserError, "unknown format '#{fmt}'" unless FORMATS.include?(fmt)
|
150
161
|
method = "to_#{fmt}"
|
151
162
|
if block_given?
|
152
|
-
send
|
163
|
+
send(method, table, options, &Proc.new)
|
153
164
|
else
|
154
|
-
send
|
165
|
+
send(method, table, options)
|
155
166
|
end
|
156
167
|
end
|
157
168
|
|
data/lib/fat_table/column.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FatTable
|
2
4
|
# Column objects are a thin wrapper around an Array to allow columns to be
|
3
5
|
# summed and have other aggregate operations performed on them, but compacting
|
@@ -21,7 +23,7 @@ module FatTable
|
|
21
23
|
attr_reader :type
|
22
24
|
|
23
25
|
# An Array of the items of this Column, all of which must be values of the
|
24
|
-
#
|
26
|
+
# Column's type or a nil. This Array contains the value of the item after
|
25
27
|
# conversion to a native Ruby type, such as TrueClass, Date, DateTime,
|
26
28
|
# Integer, String, etc. Thus, you can perform operations on the items,
|
27
29
|
# perhaps after removing nils with +.items.compact+.
|
@@ -92,6 +94,7 @@ module FatTable
|
|
92
94
|
@type = 'NilClass'
|
93
95
|
msg = "unknown column type '#{type}"
|
94
96
|
raise UserError, msg unless TYPES.include?(@type.to_s)
|
97
|
+
|
95
98
|
@items = []
|
96
99
|
items.each { |i| self << i }
|
97
100
|
end
|
@@ -103,8 +106,8 @@ module FatTable
|
|
103
106
|
# :category: Attributes
|
104
107
|
|
105
108
|
# Return the item of the Column at the given index.
|
106
|
-
def [](
|
107
|
-
items[
|
109
|
+
def [](idx)
|
110
|
+
items[idx]
|
108
111
|
end
|
109
112
|
|
110
113
|
# :category: Attributes
|
@@ -229,11 +232,13 @@ module FatTable
|
|
229
232
|
# average back to a DateTime.
|
230
233
|
def avg
|
231
234
|
only_with('avg', 'DateTime', 'Numeric')
|
235
|
+
itms = items.compact
|
236
|
+
size = itms.size.to_d
|
232
237
|
if type == 'DateTime'
|
233
|
-
avg_jd =
|
238
|
+
avg_jd = itms.map(&:jd).sum / size
|
234
239
|
DateTime.jd(avg_jd)
|
235
240
|
else
|
236
|
-
sum /
|
241
|
+
itms.sum / size
|
237
242
|
end
|
238
243
|
end
|
239
244
|
|
@@ -480,34 +485,41 @@ module FatTable
|
|
480
485
|
end
|
481
486
|
end
|
482
487
|
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
488
|
+
ISO_DATE_RE = %r{(?<yr>\d\d\d\d)[-\/]
|
489
|
+
(?<mo>\d\d?)[-\/]
|
490
|
+
(?<dy>\d\d?)\s*
|
491
|
+
(T?\s*\d\d:\d\d(:\d\d)?
|
492
|
+
([-+](\d\d?)(:\d\d?))?)?}x
|
493
|
+
|
494
|
+
AMR_DATE_RE = %r{(?<dy>\d\d?)[-/](?<mo>\d\d?)[-/](?<yr>\d\d\d\d)\s*
|
495
|
+
(?<tm>T\d\d:\d\d:\d\d(\+\d\d:\d\d)?)?}x
|
487
496
|
|
488
|
-
# Convert the val to a DateTime if it is either a DateTime, a Date, or a
|
497
|
+
# Convert the val to a DateTime if it is either a DateTime, a Date, a Time, or a
|
489
498
|
# String that can be parsed as a DateTime, otherwise return nil. It only
|
490
|
-
# recognizes strings that contain a something like '2016-01-14' or
|
491
|
-
#
|
492
|
-
#
|
493
|
-
#
|
499
|
+
# recognizes strings that contain a something like '2016-01-14' or '2/12/1985'
|
500
|
+
# within them, otherwise DateTime.parse would treat many bare numbers as dates,
|
501
|
+
# such as '2841381', which it would recognize as a valid date, but the user
|
502
|
+
# probably does not intend it to be so treated.
|
494
503
|
def convert_to_date_time(val)
|
495
504
|
return val if val.is_a?(DateTime)
|
496
505
|
return val if val.is_a?(Date)
|
497
506
|
begin
|
498
|
-
|
499
|
-
return nil if
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
507
|
+
str = val.to_s.clean
|
508
|
+
return nil if str.blank?
|
509
|
+
|
510
|
+
if str.match(ISO_DATE_RE)
|
511
|
+
date = DateTime.parse(val)
|
512
|
+
elsif str =~ AMR_DATE_RE
|
513
|
+
date = DateTime.new(Regexp.last_match[:yr].to_i,
|
514
|
+
Regexp.last_match[:mo].to_i,
|
515
|
+
Regexp.last_match[:dy].to_i)
|
504
516
|
else
|
505
517
|
return nil
|
506
518
|
end
|
507
|
-
val = val.to_date if
|
508
|
-
|
519
|
+
# val = val.to_date if
|
520
|
+
date.seconds_since_midnight.zero? ? date.to_date : date
|
509
521
|
rescue ArgumentError
|
510
|
-
|
522
|
+
nil
|
511
523
|
end
|
512
524
|
end
|
513
525
|
|
@@ -515,7 +527,7 @@ module FatTable
|
|
515
527
|
# looks like one. Any Float is promoted to a BigDecimal. Otherwise return
|
516
528
|
# nil.
|
517
529
|
def convert_to_numeric(val)
|
518
|
-
return BigDecimal
|
530
|
+
return BigDecimal(val, Float::DIG) if val.is_a?(Float)
|
519
531
|
return val if val.is_a?(Numeric)
|
520
532
|
# Eliminate any commas, $'s (or other currency symbol), or _'s.
|
521
533
|
cursym = Regexp.quote(FatTable.currency_symbol)
|
@@ -524,11 +536,11 @@ module FatTable
|
|
524
536
|
return nil if val.blank?
|
525
537
|
case val
|
526
538
|
when /(\A[-+]?\d+\.\d*\z)|(\A[-+]?\d*\.\d+\z)/
|
527
|
-
BigDecimal
|
539
|
+
BigDecimal(val.to_s.clean)
|
528
540
|
when /\A[-+]?[\d]+\z/
|
529
541
|
val.to_i
|
530
|
-
when %r{\A([-+]?\d+)\s*[:/]\s*([-+]?\d+)\z}
|
531
|
-
Rational(
|
542
|
+
when %r{\A(?<nm>[-+]?\d+)\s*[:/]\s*(?<dn>[-+]?\d+)\z}
|
543
|
+
Rational(Regexp.last_match[:nm], Regexp.last_match[:dn])
|
532
544
|
end
|
533
545
|
end
|
534
546
|
|
data/lib/fat_table/db_handle.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# Set and access a database by module-level methods.
|
2
4
|
module FatTable
|
3
5
|
class << self
|
@@ -18,10 +20,11 @@ module FatTable
|
|
18
20
|
# Sequel's adapter-specific connection methods.
|
19
21
|
# http://sequel.jeremyevans.net/rdoc/files/doc/opening_databases_rdoc.html
|
20
22
|
#
|
21
|
-
# +
|
23
|
+
# +adapter+::
|
22
24
|
# One of 'pg' (for Postgresql), 'mysql' or 'mysql2' (for Mysql), or
|
23
|
-
# 'sqlite' (for SQLite3)
|
24
|
-
#
|
25
|
+
# 'sqlite' (for SQLite3) (or any other adapter supported by the +Sequel+
|
26
|
+
# gem) to specify the driver to use. You may have to install the
|
27
|
+
# appropriate driver to make this work.
|
25
28
|
#
|
26
29
|
# +database+::
|
27
30
|
# The name of the database to access. There is no default for this.
|
@@ -51,63 +54,32 @@ module FatTable
|
|
51
54
|
# successfully, this establishes the database handle to use for all subsequent
|
52
55
|
# calls to FatTable.from_sql or FatTable::Table.from_sql. You can then access
|
53
56
|
# the handle if needed with FatTable.db.
|
54
|
-
def self.
|
57
|
+
def self.connect(args)
|
55
58
|
# Set the dsn for Sequel
|
56
59
|
begin
|
57
60
|
self.handle = Sequel.connect(args)
|
58
|
-
rescue Sequel::
|
59
|
-
|
61
|
+
rescue Sequel::AdapterNotFound => ex
|
62
|
+
case ex.to_s
|
63
|
+
when /pg/
|
64
|
+
raise TransientError, 'You need to install the postgres adapter pg'
|
65
|
+
when /mysql/
|
66
|
+
raise TransientError, 'You need to install the mysql adapter'
|
67
|
+
when /sqlite/
|
68
|
+
raise TransientError, 'You need to install the sqlite adapter'
|
69
|
+
else
|
70
|
+
raise ex
|
71
|
+
end
|
60
72
|
end
|
61
73
|
handle
|
62
74
|
end
|
63
75
|
|
64
|
-
# def self.set_db(adapter: 'postgres',
|
65
|
-
# database:,
|
66
|
-
# user: ENV['LOGNAME'],
|
67
|
-
# password: nil,
|
68
|
-
# host: 'localhost',
|
69
|
-
# port: nil,
|
70
|
-
# socket: '/tmp/.s.PGSQL.5432')
|
71
|
-
# if db
|
72
|
-
# self.handle = db
|
73
|
-
# else
|
74
|
-
# raise UserError, 'must supply database name to set_db' unless database
|
75
|
-
|
76
|
-
# valid_drivers = %w[postgres mysql mysql2 sqlite]
|
77
|
-
# unless valid_drivers.include?(driver)
|
78
|
-
# msg = "'#{driver}' driver must be one of #{valid_drivers.join(' or ')}"
|
79
|
-
# raise UserError, msg
|
80
|
-
# end
|
81
|
-
# if database.blank?
|
82
|
-
# raise UserError, 'must supply database parameter to set_db'
|
83
|
-
# end
|
84
|
-
|
85
|
-
# if driver == 'sqlite'
|
86
|
-
# dsn = "sqlite://#{database}"
|
87
|
-
# else
|
88
|
-
# pw_part = password ? ":#{password}" : ''
|
89
|
-
# hst_part = host ? "@#{host}" : ''
|
90
|
-
# prt_part = port ? ":#{port}" : ''
|
91
|
-
# dsn = "#{driver}:://#{user}#{pw_part}#{hst_part}#{prt_part}/#{database}"
|
92
|
-
# end
|
93
|
-
|
94
|
-
# # Set the dsn for Sequel
|
95
|
-
# begin
|
96
|
-
# self.handle = Sequel.connect(dsn)
|
97
|
-
# rescue Sequel::Error => ex
|
98
|
-
# raise TransientError, "#{dsn}: #{ex}"
|
99
|
-
# end
|
100
|
-
# end
|
101
|
-
# handle
|
102
|
-
# end
|
103
|
-
|
104
76
|
# Return the +Sequel+ database handle.
|
105
77
|
def self.db
|
106
78
|
handle
|
107
79
|
end
|
108
80
|
|
109
81
|
# Directly set the db handle to a Sequel connection formed without
|
110
|
-
# FatTable.
|
82
|
+
# FatTable.connect.
|
111
83
|
def self.db=(db)
|
112
84
|
self.handle = db
|
113
85
|
end
|
data/lib/fat_table/errors.rb
CHANGED
data/lib/fat_table/evaluator.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FatTable
|
2
4
|
# The Evaluator class provides a class for evaluating Ruby expressions based
|
3
5
|
# on variable settings provided at runtime. If the same Evaluator object is
|
@@ -20,23 +22,25 @@ module FatTable
|
|
20
22
|
def initialize(ivars: {}, before: nil, after: nil)
|
21
23
|
@before = before
|
22
24
|
@after = after
|
23
|
-
|
25
|
+
instance_vars(ivars)
|
24
26
|
end
|
25
27
|
|
26
28
|
# Set the @group instance variable to the given value.
|
27
29
|
def update_ivars(ivars)
|
28
|
-
|
30
|
+
instance_vars(ivars)
|
29
31
|
end
|
30
32
|
|
31
33
|
# Run any before hook in the context of the given local variables.
|
32
34
|
def eval_before_hook(locals: {})
|
33
35
|
return if @before.blank?
|
36
|
+
|
34
37
|
evaluate(@before, locals: locals)
|
35
38
|
end
|
36
39
|
|
37
40
|
# Run any after hook in the context of the given local variables.
|
38
41
|
def eval_after_hook(locals: {})
|
39
42
|
return if @after.blank?
|
43
|
+
|
40
44
|
evaluate(@after, locals: locals)
|
41
45
|
end
|
42
46
|
|
@@ -44,19 +48,21 @@ module FatTable
|
|
44
48
|
# instance variables set in Evaluator.new and any local variables set in the
|
45
49
|
# Hash parameter +locals+ are available to the expression.
|
46
50
|
def evaluate(expr = '', locals: {})
|
47
|
-
eval(expr,
|
51
|
+
eval(expr, local_vars(binding, locals))
|
48
52
|
end
|
49
53
|
|
50
54
|
private
|
51
55
|
|
52
|
-
|
56
|
+
# Set the instance variables according to Hash vars.
|
57
|
+
def instance_vars(vars = {})
|
53
58
|
vars.each_pair do |name, val|
|
54
59
|
name = "@#{name}" unless name.to_s.start_with?('@')
|
55
60
|
instance_variable_set(name, val)
|
56
61
|
end
|
57
62
|
end
|
58
63
|
|
59
|
-
|
64
|
+
# Set the local variables within the binding bnd according to Hash vars.
|
65
|
+
def local_vars(bnd, vars = {})
|
60
66
|
vars.each_pair do |name, val|
|
61
67
|
bnd.local_variable_set(name, val)
|
62
68
|
end
|
data/lib/fat_table/formatters.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FatTable
|
2
4
|
# A subclass of Formatter for rendering the table as a Ruby Array of Arrays.
|
3
5
|
# Each cell is formatted as a string in accordance with the formatting
|
@@ -30,20 +32,20 @@ module FatTable
|
|
30
32
|
'['
|
31
33
|
end
|
32
34
|
|
33
|
-
def pre_cell(
|
35
|
+
def pre_cell(_val)
|
34
36
|
"'"
|
35
37
|
end
|
36
38
|
|
37
39
|
# Because the cell, after conversion to a single-quoted string will be
|
38
40
|
# eval'ed, we need to escape any single-quotes (') that appear in the
|
39
41
|
# string.
|
40
|
-
def quote_cell(
|
41
|
-
if
|
42
|
+
def quote_cell(val)
|
43
|
+
if val.match?(/'/)
|
42
44
|
# Use a negative look-behind to only quote single-quotes that are not
|
43
45
|
# already preceded by a backslash
|
44
|
-
|
46
|
+
val.gsub(/(?<!\\)'/, "'" => "\\'")
|
45
47
|
else
|
46
|
-
|
48
|
+
val
|
47
49
|
end
|
48
50
|
end
|
49
51
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FatTable
|
2
4
|
# A subclass of Formatter for rendering the table as a Ruby Array of Hashes.
|
3
5
|
# Each row of the Array is a Hash representing one row of the table with the
|
@@ -30,20 +32,20 @@ module FatTable
|
|
30
32
|
'{'
|
31
33
|
end
|
32
34
|
|
33
|
-
def pre_cell(
|
34
|
-
":#{
|
35
|
+
def pre_cell(head)
|
36
|
+
":#{head.as_sym} => '"
|
35
37
|
end
|
36
38
|
|
37
39
|
# Because the cell, after conversion to a single-quoted string will be
|
38
40
|
# eval'ed, we need to escape any single-quotes (') that appear in the
|
39
41
|
# string.
|
40
|
-
def quote_cell(
|
41
|
-
if
|
42
|
+
def quote_cell(val)
|
43
|
+
if val.match?(/'/)
|
42
44
|
# Use a negative look-behind to only quote single-quotes that are not
|
43
45
|
# already preceded by a backslash
|
44
|
-
|
46
|
+
val.gsub(/(?<!\\)'/, "'" => "\\'")
|
45
47
|
else
|
46
|
-
|
48
|
+
val
|
47
49
|
end
|
48
50
|
end
|
49
51
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module FatTable
|
2
4
|
# A Formatter is for use in Table output routines, and provides methods for
|
3
5
|
# adding group and table footers to the output and instructions for how the
|
@@ -73,7 +75,7 @@ module FatTable
|
|
73
75
|
false_color: 'none',
|
74
76
|
false_bgcolor: 'none',
|
75
77
|
underline: false,
|
76
|
-
blink: false
|
78
|
+
blink: false,
|
77
79
|
}
|
78
80
|
|
79
81
|
class_attribute :valid_colors
|
@@ -92,9 +94,10 @@ module FatTable
|
|
92
94
|
# Formatter is yielded to the block so that methods for formatting and
|
93
95
|
# adding footers can be called on it.
|
94
96
|
def initialize(table = Table.new, **options)
|
95
|
-
unless table
|
97
|
+
unless table&.is_a?(Table)
|
96
98
|
raise UserError, 'must initialize Formatter with a Table'
|
97
99
|
end
|
100
|
+
|
98
101
|
@table = table
|
99
102
|
@options = options
|
100
103
|
@footers = {}
|
@@ -170,12 +173,14 @@ module FatTable
|
|
170
173
|
unless table.headers.include?(h)
|
171
174
|
raise UserError, "No '#{h}' column in table to sum in the footer"
|
172
175
|
end
|
176
|
+
|
173
177
|
foot[h] = :sum
|
174
178
|
end
|
175
179
|
agg_cols.each do |h, agg|
|
176
180
|
unless table.headers.include?(h)
|
177
181
|
raise UserError, "No '#{h}' column in table to #{agg} in the footer"
|
178
182
|
end
|
183
|
+
|
179
184
|
foot[h] = agg
|
180
185
|
end
|
181
186
|
@footers[label] = foot
|
@@ -208,12 +213,14 @@ module FatTable
|
|
208
213
|
unless table.headers.include?(h)
|
209
214
|
raise UserError, "No '#{h}' column in table for group sum footer"
|
210
215
|
end
|
216
|
+
|
211
217
|
foot[h] = :sum
|
212
218
|
end
|
213
219
|
agg_cols.each do |h, agg|
|
214
220
|
unless table.headers.include?(h)
|
215
221
|
raise UserError, "No '#{h}' column in table for #{agg} group footer"
|
216
222
|
end
|
223
|
+
|
217
224
|
foot[h] = agg
|
218
225
|
end
|
219
226
|
@gfooters[label] = foot
|
@@ -242,7 +249,7 @@ module FatTable
|
|
242
249
|
cols.each do |c|
|
243
250
|
hsh[c] = :avg
|
244
251
|
end
|
245
|
-
footer('Average', hsh)
|
252
|
+
footer('Average', **hsh)
|
246
253
|
end
|
247
254
|
|
248
255
|
# :category: Footers
|
@@ -253,7 +260,7 @@ module FatTable
|
|
253
260
|
cols.each do |c|
|
254
261
|
hsh[c] = :avg
|
255
262
|
end
|
256
|
-
gfooter('Group Average', hsh)
|
263
|
+
gfooter('Group Average', **hsh)
|
257
264
|
end
|
258
265
|
|
259
266
|
# :category: Footers
|
@@ -265,7 +272,7 @@ module FatTable
|
|
265
272
|
cols.each do |c|
|
266
273
|
hsh[c] = :min
|
267
274
|
end
|
268
|
-
footer('Minimum', hsh)
|
275
|
+
footer('Minimum', **hsh)
|
269
276
|
end
|
270
277
|
|
271
278
|
# :category: Footers
|
@@ -277,7 +284,7 @@ module FatTable
|
|
277
284
|
cols.each do |c|
|
278
285
|
hsh[c] = :min
|
279
286
|
end
|
280
|
-
gfooter('Group Minimum', hsh)
|
287
|
+
gfooter('Group Minimum', **hsh)
|
281
288
|
end
|
282
289
|
|
283
290
|
# :category: Footers
|
@@ -289,7 +296,7 @@ module FatTable
|
|
289
296
|
cols.each do |c|
|
290
297
|
hsh[c] = :max
|
291
298
|
end
|
292
|
-
footer('Maximum', hsh)
|
299
|
+
footer('Maximum', **hsh)
|
293
300
|
end
|
294
301
|
|
295
302
|
# :category: Footers
|
@@ -301,7 +308,7 @@ module FatTable
|
|
301
308
|
cols.each do |c|
|
302
309
|
hsh[c] = :max
|
303
310
|
end
|
304
|
-
gfooter('Group Maximum', hsh)
|
311
|
+
gfooter('Group Maximum', **hsh)
|
305
312
|
end
|
306
313
|
|
307
314
|
# :category: Formatting
|
@@ -413,7 +420,7 @@ module FatTable
|
|
413
420
|
# \n\[niltext\]:: render a nil item with the given text.
|
414
421
|
def format(**fmts)
|
415
422
|
%i[header bfirst gfirst body footer gfooter].each do |loc|
|
416
|
-
format_for(loc, fmts)
|
423
|
+
format_for(loc, **fmts)
|
417
424
|
end
|
418
425
|
self
|
419
426
|
end
|
@@ -466,6 +473,7 @@ module FatTable
|
|
466
473
|
unless LOCATIONS.include?(location)
|
467
474
|
raise UserError, "unknown format location '#{location}'"
|
468
475
|
end
|
476
|
+
|
469
477
|
valid_keys = table.headers + %i[string numeric datetime boolean nil]
|
470
478
|
invalid_keys = (fmts.keys - valid_keys).uniq
|
471
479
|
unless invalid_keys.empty?
|
@@ -487,18 +495,18 @@ module FatTable
|
|
487
495
|
# Merge in string and nil formatting, but not in header. Header is
|
488
496
|
# always typed a string, so it will get formatted in type-based
|
489
497
|
# formatting below. And headers are never nil.
|
490
|
-
if fmts.
|
498
|
+
if fmts.key?(:string)
|
491
499
|
typ_fmt = parse_string_fmt(fmts[:string])
|
492
500
|
format_h = format_h.merge(typ_fmt)
|
493
501
|
end
|
494
|
-
if fmts.
|
502
|
+
if fmts.key?(:nil)
|
495
503
|
typ_fmt = parse_nil_fmt(fmts[:nil]).first
|
496
504
|
format_h = format_h.merge(typ_fmt)
|
497
505
|
end
|
498
506
|
end
|
499
507
|
typ = location == :header ? :string : table.type(h).as_sym
|
500
508
|
parse_typ_method_name = 'parse_' + typ.to_s + '_fmt'
|
501
|
-
if fmts.
|
509
|
+
if fmts.key?(typ)
|
502
510
|
# Merge in type-based formatting
|
503
511
|
typ_fmt = send(parse_typ_method_name, fmts[typ])
|
504
512
|
format_h = format_h.merge(typ_fmt)
|
@@ -547,7 +555,7 @@ module FatTable
|
|
547
555
|
private
|
548
556
|
|
549
557
|
# Re to match a color name
|
550
|
-
CLR_RE = /(?:[-_a-zA-Z0-9 ]*)
|
558
|
+
CLR_RE = /(?:[-_a-zA-Z0-9 ]*)/.freeze
|
551
559
|
|
552
560
|
# Return a hash that reflects the formatting instructions given in the
|
553
561
|
# string fmt. Raise an error if it contains invalid formatting instructions.
|
@@ -558,6 +566,7 @@ module FatTable
|
|
558
566
|
unless fmt.blank? || !strict
|
559
567
|
raise UserError, "unrecognized string formatting instructions '#{fmt}'"
|
560
568
|
end
|
569
|
+
|
561
570
|
format
|
562
571
|
end
|
563
572
|
|
@@ -570,9 +579,9 @@ module FatTable
|
|
570
579
|
# parse, we remove the matched construct from fmt. At the end, any
|
571
580
|
# remaining characters in fmt should be invalid.
|
572
581
|
fmt_hash = {}
|
573
|
-
if fmt =~ /c\[(
|
574
|
-
fmt_hash[:color] =
|
575
|
-
fmt_hash[:bgcolor] =
|
582
|
+
if fmt =~ /c\[(?<co>#{CLR_RE})(\.(?<bg>#{CLR_RE}))?\]/
|
583
|
+
fmt_hash[:color] = Regexp.last_match[:co] unless Regexp.last_match[:co].blank?
|
584
|
+
fmt_hash[:bgcolor] = Regexp.last_match[:bg] unless Regexp.last_match[:bg].blank?
|
576
585
|
validate_color(fmt_hash[:color])
|
577
586
|
validate_color(fmt_hash[:bgcolor])
|
578
587
|
fmt = fmt.sub($&, '')
|
@@ -592,12 +601,12 @@ module FatTable
|
|
592
601
|
fmt_hash[:case] = :title
|
593
602
|
fmt = fmt.sub($&, '')
|
594
603
|
end
|
595
|
-
if fmt =~ /(
|
596
|
-
fmt_hash[:bold] =
|
604
|
+
if fmt =~ /(?<neg>~\s*)?B/
|
605
|
+
fmt_hash[:bold] = !Regexp.last_match[:neg]
|
597
606
|
fmt = fmt.sub($&, '')
|
598
607
|
end
|
599
|
-
if fmt =~ /(
|
600
|
-
fmt_hash[:italic] =
|
608
|
+
if fmt =~ /(?<neg>~\s*)?I/
|
609
|
+
fmt_hash[:italic] = !Regexp.last_match[:neg]
|
601
610
|
fmt = fmt.sub($&, '')
|
602
611
|
end
|
603
612
|
if fmt =~ /R/
|
@@ -612,12 +621,12 @@ module FatTable
|
|
612
621
|
fmt_hash[:alignment] = :left
|
613
622
|
fmt = fmt.sub($&, '')
|
614
623
|
end
|
615
|
-
if fmt =~ /(
|
616
|
-
fmt_hash[:underline] =
|
624
|
+
if fmt =~ /(?<neg>~\s*)?_/
|
625
|
+
fmt_hash[:underline] = !Regexp.last_match[:neg]
|
617
626
|
fmt = fmt.sub($&, '')
|
618
627
|
end
|
619
|
-
if fmt =~ /(
|
620
|
-
fmt_hash[:blink] =
|
628
|
+
if fmt =~ /(?<neg>~\s*)?\*/
|
629
|
+
fmt_hash[:blink] = !Regexp.last_match[:neg]
|
621
630
|
fmt = fmt.sub($&, '')
|
622
631
|
end
|
623
632
|
[fmt_hash, fmt]
|
@@ -632,9 +641,9 @@ module FatTable
|
|
632
641
|
# parse, we remove the matched construct from fmt. At the end, any
|
633
642
|
# remaining characters in fmt should be invalid.
|
634
643
|
fmt_hash = {}
|
635
|
-
if fmt =~ /n\[\s*([^\]]*)\s*\]/
|
636
|
-
fmt_hash[:nil_text] =
|
637
|
-
fmt = fmt.sub(
|
644
|
+
if fmt =~ /n\[\s*(?<bdy>[^\]]*)\s*\]/
|
645
|
+
fmt_hash[:nil_text] = Regexp.last_match[:bdy].clean
|
646
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
638
647
|
end
|
639
648
|
[fmt_hash, fmt]
|
640
649
|
end
|
@@ -648,26 +657,27 @@ module FatTable
|
|
648
657
|
# parse, we remove the matched construct from fmt. At the end, any
|
649
658
|
# remaining characters in fmt should be invalid.
|
650
659
|
fmt_hash, fmt = parse_str_fmt(fmt)
|
651
|
-
if fmt =~ /(
|
652
|
-
fmt_hash[:pre_digits] =
|
653
|
-
fmt_hash[:post_digits] =
|
654
|
-
fmt = fmt.sub(
|
660
|
+
if fmt =~ /(?<pre>\d+).(?<post>\d+)/
|
661
|
+
fmt_hash[:pre_digits] = Regexp.last_match[:pre].to_i
|
662
|
+
fmt_hash[:post_digits] = Regexp.last_match[:post].to_i
|
663
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
655
664
|
end
|
656
|
-
if fmt =~ /(
|
657
|
-
fmt_hash[:commas] =
|
658
|
-
fmt = fmt.sub(
|
665
|
+
if fmt =~ /(?<neg>~\s*)?,/
|
666
|
+
fmt_hash[:commas] = !Regexp.last_match[:neg]
|
667
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
659
668
|
end
|
660
|
-
if fmt =~ /(
|
661
|
-
fmt_hash[:currency] =
|
662
|
-
fmt = fmt.sub(
|
669
|
+
if fmt =~ /(?<neg>~\s*)?\$/
|
670
|
+
fmt_hash[:currency] = !Regexp.last_match[:neg]
|
671
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
663
672
|
end
|
664
|
-
if fmt =~ /(
|
665
|
-
fmt_hash[:hms] =
|
666
|
-
fmt = fmt.sub(
|
673
|
+
if fmt =~ /(?<neg>~\s*)?H/
|
674
|
+
fmt_hash[:hms] = !Regexp.last_match[:neg]
|
675
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
667
676
|
end
|
668
677
|
unless fmt.blank? || !strict
|
669
678
|
raise UserError, "unrecognized numeric formatting instructions '#{fmt}'"
|
670
679
|
end
|
680
|
+
|
671
681
|
fmt_hash
|
672
682
|
end
|
673
683
|
|
@@ -680,13 +690,13 @@ module FatTable
|
|
680
690
|
# parse, we remove the matched construct from fmt. At the end, any
|
681
691
|
# remaining characters in fmt should be invalid.
|
682
692
|
fmt_hash, fmt = parse_str_fmt(fmt)
|
683
|
-
if fmt =~ /d\[([^\]]*)\]/
|
684
|
-
fmt_hash[:date_fmt] =
|
685
|
-
fmt = fmt.sub(
|
693
|
+
if fmt =~ /d\[(?<bdy>[^\]]*)\]/
|
694
|
+
fmt_hash[:date_fmt] = Regexp.last_match[:bdy]
|
695
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
686
696
|
end
|
687
|
-
if fmt =~ /D\[([^\]]*)\]/
|
688
|
-
fmt_hash[:date_fmt] =
|
689
|
-
fmt = fmt.sub(
|
697
|
+
if fmt =~ /D\[(?<bdy>[^\]]*)\]/
|
698
|
+
fmt_hash[:date_fmt] = Regexp.last_match[:bdy]
|
699
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
690
700
|
end
|
691
701
|
unless fmt.blank? || !strict
|
692
702
|
msg = "unrecognized datetime formatting instructions '#{fmt}'"
|
@@ -704,41 +714,43 @@ module FatTable
|
|
704
714
|
# parse, we remove the matched construct from fmt. At the end, any
|
705
715
|
# remaining characters in fmt should be invalid.
|
706
716
|
fmt_hash = {}
|
707
|
-
if fmt =~ /b\[\s*([^\],]*),([^\]]*)\s*\]/
|
708
|
-
fmt_hash[:true_text] =
|
709
|
-
fmt_hash[:false_text] =
|
710
|
-
fmt = fmt.sub(
|
717
|
+
if fmt =~ /b\[\s*(?<t>[^\],]*),(?<f>[^\]]*)\s*\]/
|
718
|
+
fmt_hash[:true_text] = Regexp.last_match[:t].clean
|
719
|
+
fmt_hash[:false_text] = Regexp.last_match[:f].clean
|
720
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
711
721
|
end
|
712
722
|
# Since true_text, false_text and nil_text may want to have internal
|
713
723
|
# spaces, defer removing extraneous spaces until after they are parsed.
|
714
724
|
if fmt =~ /c\[(#{CLR_RE})(\.(#{CLR_RE}))?,
|
715
725
|
\s*(#{CLR_RE})(\.(#{CLR_RE}))?\]/x
|
716
|
-
|
717
|
-
fmt_hash[:
|
718
|
-
fmt_hash[:
|
719
|
-
fmt_hash[:
|
720
|
-
|
726
|
+
tco, _, tbg, fco, _, fbg = Regexp.last_match.captures
|
727
|
+
fmt_hash[:true_color] = tco unless tco.blank?
|
728
|
+
fmt_hash[:true_bgcolor] = tbg unless tbg.blank?
|
729
|
+
fmt_hash[:false_color] = fco unless fco.blank?
|
730
|
+
fmt_hash[:false_bgcolor] = fbg unless fbg.blank?
|
731
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
721
732
|
end
|
722
733
|
str_fmt_hash, fmt = parse_str_fmt(fmt)
|
723
734
|
fmt_hash = fmt_hash.merge(str_fmt_hash)
|
724
735
|
if fmt =~ /Y/
|
725
736
|
fmt_hash[:true_text] = 'Y'
|
726
737
|
fmt_hash[:false_text] = 'N'
|
727
|
-
fmt = fmt.sub(
|
738
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
728
739
|
end
|
729
740
|
if fmt =~ /T/
|
730
741
|
fmt_hash[:true_text] = 'T'
|
731
742
|
fmt_hash[:false_text] = 'F'
|
732
|
-
fmt = fmt.sub(
|
743
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
733
744
|
end
|
734
745
|
if fmt =~ /X/
|
735
746
|
fmt_hash[:true_text] = 'X'
|
736
747
|
fmt_hash[:false_text] = ''
|
737
|
-
fmt = fmt.sub(
|
748
|
+
fmt = fmt.sub(Regexp.last_match[0], '')
|
738
749
|
end
|
739
750
|
unless fmt.blank? || !strict
|
740
751
|
raise UserError, "unrecognized boolean formatting instructions '#{fmt}'"
|
741
752
|
end
|
753
|
+
|
742
754
|
fmt_hash
|
743
755
|
end
|
744
756
|
|
@@ -810,6 +822,7 @@ module FatTable
|
|
810
822
|
# specializing this method.
|
811
823
|
def format_boolean(val, istruct)
|
812
824
|
return istruct.nil_text if val.nil?
|
825
|
+
|
813
826
|
val ? istruct.true_text : istruct.false_text
|
814
827
|
end
|
815
828
|
|
@@ -820,6 +833,7 @@ module FatTable
|
|
820
833
|
# specializing this method.
|
821
834
|
def format_datetime(val, istruct)
|
822
835
|
return istruct.nil_text if val.nil?
|
836
|
+
|
823
837
|
if val.to_date == val
|
824
838
|
# It is a Date, with no time component.
|
825
839
|
val.strftime(istruct.date_fmt)
|
@@ -835,6 +849,7 @@ module FatTable
|
|
835
849
|
# specializing this method.
|
836
850
|
def format_numeric(val, istruct)
|
837
851
|
return istruct.nil_text if val.nil?
|
852
|
+
|
838
853
|
val = val.round(istruct.post_digits) if istruct.post_digits >= 0
|
839
854
|
if istruct.hms
|
840
855
|
result = val.secs_to_hms
|
@@ -998,6 +1013,7 @@ module FatTable
|
|
998
1013
|
new_rows.each do |loc_row|
|
999
1014
|
result += hline(widths) if loc_row.nil?
|
1000
1015
|
next if loc_row.nil?
|
1016
|
+
|
1001
1017
|
_loc, row = *loc_row
|
1002
1018
|
result += pre_row
|
1003
1019
|
cells = []
|
@@ -1140,6 +1156,7 @@ module FatTable
|
|
1140
1156
|
end
|
1141
1157
|
rows.each do |loc_row|
|
1142
1158
|
next if loc_row.nil?
|
1159
|
+
|
1143
1160
|
_loc, row = *loc_row
|
1144
1161
|
row.each_pair do |h, (_v, fmt_v)|
|
1145
1162
|
widths[h] ||= 0
|
@@ -1222,12 +1239,12 @@ module FatTable
|
|
1222
1239
|
''
|
1223
1240
|
end
|
1224
1241
|
|
1225
|
-
def pre_cell(
|
1242
|
+
def pre_cell(_head)
|
1226
1243
|
''
|
1227
1244
|
end
|
1228
1245
|
|
1229
|
-
def quote_cell(
|
1230
|
-
|
1246
|
+
def quote_cell(val)
|
1247
|
+
val
|
1231
1248
|
end
|
1232
1249
|
|
1233
1250
|
def post_cell
|