fat_table 0.2.7 → 0.3.1
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 +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
|