table_tennis 0.0.1 → 0.0.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 +4 -4
- data/README.md +2 -0
- data/justfile +3 -2
- data/lib/table_tennis/column.rb +25 -12
- data/lib/table_tennis/config.rb +11 -2
- data/lib/table_tennis/row.rb +7 -5
- data/lib/table_tennis/stage/base.rb +2 -4
- data/lib/table_tennis/stage/format.rb +77 -46
- data/lib/table_tennis/stage/painter.rb +73 -20
- data/lib/table_tennis/stage/render.rb +9 -2
- data/lib/table_tennis/table.rb +5 -5
- data/lib/table_tennis/table_data.rb +14 -14
- data/lib/table_tennis/theme.rb +4 -0
- data/lib/table_tennis/util/colors.rb +4 -1
- data/lib/table_tennis/util/identify.rb +51 -0
- data/lib/table_tennis/util/inspectable.rb +2 -3
- data/lib/table_tennis/util/strings.rb +11 -1
- data/lib/table_tennis/util/termbg.rb +1 -1
- data/lib/table_tennis/version.rb +1 -1
- data/lib/table_tennis.rb +1 -0
- data/table_tennis.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b4fb283f2a6461183187e0833f77aa8934f40c2611a88b4e65da5a7b0e0b51a
|
4
|
+
data.tar.gz: 22b0ed370736791f838fedc5a8b3044664250f4014608525807f81d28040b4b6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24e7cff664a8edb52ecde9ef90b458480e535bc8895d6dc7a067c64ef344964a3fbc9474ad0ea97cdb3f4123aeb2212ebddac9de296628785b557d959dd803e2
|
7
|
+
data.tar.gz: 99b2fc156f0960472f8d969c89deb4067e9dbcc466826f0b55dd02d0beeacbf493f1ba0fe00843c1f3cc80eb9b927a167e268af6609726a7b4ba9eab363ece5e
|
data/README.md
CHANGED
@@ -71,6 +71,7 @@ options = {
|
|
71
71
|
| `color_scales` | ─ | Color code a column of floats, similar to the "conditional formatting" feature in Google Sheets. See [docs below](#color-scales). |
|
72
72
|
| `color` | `nil` | Are ANSI colors enabled? Specify `true` or `false`, or leave it as nil to autodetect. Autodetect will turn on color unless redirecting to a file. When using autodetect, you can force it on by setting `ENV["FORCE_COLOR"]`, or off with `ENV["NO_COLOR"]`. |
|
73
73
|
| `columns` | `nil` | Manually set which columns to include. Leave unset to show all columns.
|
74
|
+
| `delims` | true | Format ints & floats with comma delimiter, like 123,456. |
|
74
75
|
| `digits` | `3` | Format floats to this number of digits. TableTennis will look for either `Float` cells or string floats. |
|
75
76
|
| `layout` | `true` | This controls column widths. Leave unset or use `true` for autolayout. Autolayout will shrink the table to fit inside the terminal. `false` turns off layout and columns will be full width. Use an int to fix all columns to a certain width, or a hash to just set a few. |
|
76
77
|
| `mark` | ─ | `mark` is a way to highlight specific columns with a nice color. For example, use `mark: ->(row) { row[:planet] == "tatooine" }` to highlight those rows. Your lambda can also return a specific color if you want.
|
@@ -81,6 +82,7 @@ options = {
|
|
81
82
|
| `strftime` | see → | strftime string for formatting Date/Time objects. The default is `"%Y-%m-%d"`, which looks like `2025-04-21` |
|
82
83
|
| `theme` | nil | When unset, will be autodetected based on terminal background color. If autodetect fails the theme defaults to :dark. You can also manually specify `:dark`, `:light` or `:ansi`. If colors are turned off this setting has no effect.|
|
83
84
|
| `title` | ─ | Add a title line to the table. |
|
85
|
+
| `titleize` | ─ | Titleize column names, so `person_id` becomes `Person`. |
|
84
86
|
| `zebra` | `false` | Turn on zebra stripes. |
|
85
87
|
|
86
88
|
### Color Scales
|
data/justfile
CHANGED
@@ -13,8 +13,6 @@ check: lint test
|
|
13
13
|
|
14
14
|
ci: check
|
15
15
|
|
16
|
-
demo-watch *ARGS:
|
17
|
-
@watchexec --stop-timeout=0 --clear clear table-tennis-demo {{ARGS}}
|
18
16
|
|
19
17
|
format:
|
20
18
|
@just banner format...
|
@@ -30,6 +28,9 @@ lint:
|
|
30
28
|
pry:
|
31
29
|
bundle exec pry -I lib -r table_tennis.rb
|
32
30
|
|
31
|
+
tennis-watch *ARGS:
|
32
|
+
@watchexec --stop-timeout=0 --clear clear tennis {{ARGS}}
|
33
|
+
|
33
34
|
test *ARGS:
|
34
35
|
@just banner rake test {{ARGS}}
|
35
36
|
@bundle exec rake test {{ARGS}}
|
data/lib/table_tennis/column.rb
CHANGED
@@ -1,33 +1,46 @@
|
|
1
1
|
module TableTennis
|
2
|
-
#
|
3
2
|
# A single column in a table. The data is actually stored in the rows, but it
|
4
|
-
# can be enumerated from here.
|
5
|
-
#
|
6
|
-
|
3
|
+
# can be enumerated from here.
|
7
4
|
class Column
|
5
|
+
extend Forwardable
|
8
6
|
include Enumerable
|
7
|
+
include Util::Inspectable
|
9
8
|
prepend MemoWise
|
10
9
|
|
11
|
-
|
10
|
+
# c is the column index
|
11
|
+
attr_reader :name, :data, :c
|
12
12
|
attr_accessor :header, :width
|
13
|
+
def_delegators :data, *%i[rows]
|
13
14
|
|
14
|
-
def initialize(name,
|
15
|
-
@name, @data = name, data
|
15
|
+
def initialize(data, name, c)
|
16
|
+
@name, @data, @c = name, data, c
|
16
17
|
@header = name.to_s
|
18
|
+
if data&.config&.titleize?
|
19
|
+
@header = Util::Strings.titleize(@header)
|
20
|
+
end
|
17
21
|
end
|
18
22
|
|
19
23
|
def each(&block)
|
20
24
|
return to_enum(__method__) unless block_given?
|
21
|
-
|
25
|
+
rows.each { yield(_1[c]) }
|
22
26
|
self
|
23
27
|
end
|
24
28
|
|
25
|
-
def
|
26
|
-
|
27
|
-
|
29
|
+
def map!(&block) = rows.each { _1[c] = yield(_1[c]) }
|
30
|
+
|
31
|
+
# what type is this column? Sample 100 rows and guess. Will be nil if we aren't sure.
|
32
|
+
def type
|
33
|
+
samples = rows.sample(100).map { _1[c] }
|
34
|
+
Util::Identify.identify_column(samples)
|
28
35
|
end
|
36
|
+
memo_wise :type
|
29
37
|
|
30
|
-
def
|
38
|
+
def alignment
|
39
|
+
case type
|
40
|
+
when :float, :int then :right
|
41
|
+
else :left
|
42
|
+
end
|
43
|
+
end
|
31
44
|
|
32
45
|
def truncate(stop)
|
33
46
|
@header = Util::Strings.truncate(header, stop)
|
data/lib/table_tennis/config.rb
CHANGED
@@ -10,6 +10,7 @@ module TableTennis
|
|
10
10
|
color: nil, # true/false/nil (detect)
|
11
11
|
columns: nil, # array of symbols, or inferred from rows
|
12
12
|
debug: false, # true for debug output
|
13
|
+
delims: true, # true for numeric delimeters
|
13
14
|
digits: 3, # format floats
|
14
15
|
layout: true, # true/false/int or hash of columns -> width. true to infer
|
15
16
|
mark: nil, # lambda returning boolean or symbol to mark rows in output
|
@@ -20,6 +21,7 @@ module TableTennis
|
|
20
21
|
strftime: nil, # string for formatting dates
|
21
22
|
theme: nil, # :dark, :light or :ansi. :dark is the default
|
22
23
|
title: nil, # string for table title, if any
|
24
|
+
titleize: false, # if true, titleize column names
|
23
25
|
zebra: false, # turn on zebra stripes
|
24
26
|
}.freeze
|
25
27
|
|
@@ -27,7 +29,7 @@ module TableTennis
|
|
27
29
|
options = [OPTIONS, TableTennis.defaults, options].reduce { _1.merge(_2 || {}) }
|
28
30
|
options[:color] = Config.detect_color? if options[:color].nil?
|
29
31
|
options[:theme] = Config.detect_theme if options[:theme].nil?
|
30
|
-
options[:debug] = true if ENV["
|
32
|
+
options[:debug] = true if ENV["TT_DEBUG"]
|
31
33
|
options.each { self[_1] = _2 }
|
32
34
|
|
33
35
|
yield self if block_given?
|
@@ -43,6 +45,7 @@ module TableTennis
|
|
43
45
|
{
|
44
46
|
color: :bool,
|
45
47
|
debug: :bool,
|
48
|
+
delims: :bool,
|
46
49
|
digits: :int,
|
47
50
|
mark: :proc,
|
48
51
|
placeholder: :str,
|
@@ -50,6 +53,7 @@ module TableTennis
|
|
50
53
|
save: :str,
|
51
54
|
strftime: :str,
|
52
55
|
title: :str,
|
56
|
+
titleize: :bool,
|
53
57
|
zebra: :bool,
|
54
58
|
}.each do |option, type|
|
55
59
|
define_method(:"#{option}=") do |value|
|
@@ -206,7 +210,12 @@ module TableTennis
|
|
206
210
|
end
|
207
211
|
|
208
212
|
def _str(option, value)
|
209
|
-
|
213
|
+
case option
|
214
|
+
when :placeholder
|
215
|
+
value = "" if value.nil?
|
216
|
+
when :title
|
217
|
+
value = value.to_s if value.is_a?(Symbol)
|
218
|
+
end
|
210
219
|
validate(option, value) do
|
211
220
|
"expected string" if !value.is_a?(String)
|
212
221
|
end
|
data/lib/table_tennis/row.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
module TableTennis
|
2
|
-
#
|
3
|
-
class Row <
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
# We use this to store each row of data in the table. Row is an array, but with `r` too.
|
3
|
+
class Row < Array
|
4
|
+
# r is the index of this row
|
5
|
+
attr_reader :r
|
6
|
+
def initialize(r, values)
|
7
|
+
super(values)
|
8
|
+
@r = r
|
7
9
|
end
|
8
10
|
end
|
9
11
|
end
|
@@ -3,17 +3,15 @@ module TableTennis
|
|
3
3
|
# Base class for each stage of the rendering pipeline, mostly here just to
|
4
4
|
# delegate to TableData.
|
5
5
|
class Base
|
6
|
-
prepend MemoWise
|
7
6
|
extend Forwardable
|
7
|
+
prepend MemoWise
|
8
8
|
include Util::Inspectable
|
9
9
|
|
10
10
|
attr_reader :data
|
11
11
|
def_delegators :data, *%i[column_names columns config input_rows rows theme]
|
12
12
|
def_delegators :data, *%i[debug debug_if_slow]
|
13
13
|
|
14
|
-
def initialize(data)
|
15
|
-
@data = data
|
16
|
-
end
|
14
|
+
def initialize(data) = @data = data
|
17
15
|
end
|
18
16
|
end
|
19
17
|
end
|
@@ -1,68 +1,99 @@
|
|
1
1
|
module TableTennis
|
2
2
|
module Stage
|
3
|
-
# This stage "formats" the table by injesting
|
4
|
-
# ruby objects and formatting each
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# For performance reasons, formatting is implemented as a series of lambdas.
|
8
|
-
# Each fn must return a string. Minimize the number of times we examine each
|
9
|
-
# cell. Float detection/printing can be slow. Favor .match? over =~.
|
3
|
+
# This stage "formats" the table by injesting columns that contain various
|
4
|
+
# ruby objects and formatting each as a string. Cells are formatted in place
|
5
|
+
# by transforming rows.
|
10
6
|
class Format < Base
|
11
|
-
attr_reader :fns
|
12
|
-
|
13
7
|
def run
|
14
|
-
|
8
|
+
# Sample each column and infer column types. Determine which fn_xxx to
|
9
|
+
# use for each column.
|
10
|
+
fns = columns.map do
|
11
|
+
fn = case _1.type
|
12
|
+
when :float then :fn_float
|
13
|
+
when :int then :fn_int
|
14
|
+
when :time then :fn_time if config.strftime
|
15
|
+
end
|
16
|
+
fn || :fn_default
|
17
|
+
end
|
18
|
+
|
15
19
|
rows.each do |row|
|
16
|
-
row.
|
17
|
-
|
18
|
-
fn.
|
20
|
+
row.each_index do
|
21
|
+
value = row[_1]
|
22
|
+
# Try to format using the column fn. This can return nil. For
|
23
|
+
# example, a float column and value is nil, not a float, etc.
|
24
|
+
formatted = send(fns[_1], value)
|
25
|
+
# If the column formatter failed, use the default formatter
|
26
|
+
row[_1] = formatted || fn_default(value) || config.placeholder
|
19
27
|
end
|
20
28
|
end
|
21
29
|
end
|
22
30
|
|
23
|
-
|
31
|
+
#
|
32
|
+
# fns for each column type
|
33
|
+
#
|
34
|
+
|
35
|
+
def fn_float(value)
|
36
|
+
# cap memo_wise
|
37
|
+
@_memo_wise[__method__].tap { _1.clear if _1.length > 5000 }
|
38
|
+
|
24
39
|
case value
|
25
|
-
when String then
|
26
|
-
when
|
27
|
-
when Date, Time, DateTime then :time
|
28
|
-
else
|
29
|
-
if value.respond_to?(:acts_like_time)
|
30
|
-
# Rails TimeWithZone
|
31
|
-
return :time
|
32
|
-
end
|
33
|
-
:other
|
40
|
+
when String then fmt_number(to_f(value), digits: config.digits) if Util::Identify.number?(value)
|
41
|
+
when Numeric then fmt_number(value, digits: config.digits)
|
34
42
|
end
|
35
43
|
end
|
44
|
+
memo_wise :fn_float
|
36
45
|
|
37
|
-
def
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
str = ->(s) do
|
42
|
-
# normalize whitespace
|
43
|
-
if s.match?(/\s/)
|
44
|
-
s = s.strip.gsub("\n", "\\n").gsub("\r", "\\r")
|
45
|
-
end
|
46
|
-
# replace empty values with placeholder
|
47
|
-
s = placeholder if s.empty?
|
48
|
-
s
|
46
|
+
def fn_int(value)
|
47
|
+
case value
|
48
|
+
when String then fmt_number(to_i(value)) if Util::Identify.int?(value)
|
49
|
+
when Integer then fmt_number(value)
|
49
50
|
end
|
50
|
-
|
51
|
+
end
|
52
|
+
|
53
|
+
def fn_time(value)
|
54
|
+
value.strftime(config.strftime) if Util::Identify.time?(value)
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
# primitives
|
59
|
+
#
|
60
|
+
|
61
|
+
# default formatting. cleanup whitespace
|
62
|
+
def fn_default(value)
|
63
|
+
return if value.nil?
|
64
|
+
str = (value.is_a?(String) ? value : value.to_s)
|
65
|
+
str = str.strip.gsub("\n", "\\n").gsub("\r", "\\r") if str.match?(/\s/)
|
66
|
+
return if str.empty?
|
67
|
+
str
|
68
|
+
end
|
51
69
|
|
52
|
-
|
53
|
-
@fns = {other:, str:}
|
70
|
+
DELIMS = /(\d)(?=(\d\d\d)+(?!\d))/
|
54
71
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
72
|
+
# this is a bit slow but easy to understand
|
73
|
+
def fmt_number(x, digits: nil)
|
74
|
+
delims = config.delims && (x >= 1000 || x <= -1000)
|
75
|
+
|
76
|
+
# convert to string (and honor digits)
|
77
|
+
x = if digits
|
78
|
+
"%0.#{digits}f" % x
|
79
|
+
else
|
80
|
+
# be careful not to leave a trailing .0
|
81
|
+
x = x.to_i if x.is_a?(Float) && (x % 1) == 0
|
82
|
+
x.to_s
|
61
83
|
end
|
62
|
-
|
63
|
-
|
84
|
+
|
85
|
+
if delims
|
86
|
+
x, r = x.split(".")
|
87
|
+
x.gsub!(DELIMS) { "#{_1}," }
|
88
|
+
x = "#{x}.#{r}" if r
|
64
89
|
end
|
90
|
+
|
91
|
+
x
|
65
92
|
end
|
93
|
+
|
94
|
+
# str to_xxx that are resistant to commas
|
95
|
+
def to_f(str) = str.delete(",").to_f
|
96
|
+
def to_i(str) = str.delete(",").to_i
|
66
97
|
end
|
67
98
|
end
|
68
99
|
end
|
@@ -19,7 +19,7 @@ module TableTennis
|
|
19
19
|
paint_row_numbers if config.row_numbers
|
20
20
|
paint_rows if config.mark || config.zebra
|
21
21
|
paint_columns if config.color_scales
|
22
|
-
paint_placeholders
|
22
|
+
paint_placeholders
|
23
23
|
end
|
24
24
|
|
25
25
|
protected
|
@@ -42,43 +42,96 @@ module TableTennis
|
|
42
42
|
if config.zebra? && r.even?
|
43
43
|
style = :zebra
|
44
44
|
end
|
45
|
-
if (
|
46
|
-
style = mark_style
|
45
|
+
if (user_mark = config.mark&.call(input_rows[r]))
|
46
|
+
style = mark_style(user_mark)
|
47
47
|
end
|
48
48
|
set_style(r:, style:) if style
|
49
49
|
end
|
50
50
|
end
|
51
51
|
|
52
|
+
# This is the main entry point for color scales. Color scaling is pretty simple.
|
53
|
+
# Scale.interpolate uses a `t` param to interpolate between the gradient colors. We pick a "t"
|
54
|
+
# for each cell to get the right color. For numeric columns, this is easy: t = (cell_value -
|
55
|
+
# min) / (max - min). For non-numeric columns, we do roughly the same thing but we pick
|
56
|
+
# "cell_value" by creating a sorted list of all cells in the column, then using the position
|
57
|
+
# of each string as the cell value. So if the column contained A..Z, A would bend up being t=0
|
58
|
+
# and Z would be t=1.
|
52
59
|
def paint_columns
|
53
60
|
columns.each.with_index do |column, c|
|
54
|
-
scale = config.color_scales[column.name]
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# color
|
62
|
-
column.each_index.zip(floats).each do |r, f|
|
63
|
-
next if !f
|
64
|
-
t = (f - min) / (max - min)
|
65
|
-
bg = Util::Scale.interpolate(scale, t)
|
66
|
-
fg = Util::Colors.contrast(bg)
|
67
|
-
set_style(r:, c:, style: [fg, bg])
|
61
|
+
if (scale = config.color_scales[column.name])
|
62
|
+
if column.type == :float || column.type == :int
|
63
|
+
scale_numeric(c, scale)
|
64
|
+
else
|
65
|
+
scale_non_numeric(c, scale)
|
66
|
+
end
|
68
67
|
end
|
69
68
|
end
|
70
69
|
end
|
71
70
|
|
72
71
|
def paint_placeholders
|
73
|
-
placeholder = config.placeholder
|
74
72
|
rows.each.with_index do |row, r|
|
75
|
-
row.
|
76
|
-
if value == placeholder
|
73
|
+
row.each.with_index do |value, c|
|
74
|
+
if value == config.placeholder
|
77
75
|
set_style(r:, c:, style: :chrome)
|
78
76
|
end
|
79
77
|
end
|
80
78
|
end
|
81
79
|
end
|
80
|
+
|
81
|
+
#
|
82
|
+
# helpers
|
83
|
+
#
|
84
|
+
|
85
|
+
def scale_numeric(c, scale)
|
86
|
+
# focus on rows that contain values
|
87
|
+
focus = rows.select { Util::Identify.number?(_1[c]) }
|
88
|
+
return if focus.length < 2 # edge case
|
89
|
+
floats = focus.map { _1[c].delete(",").to_f }
|
90
|
+
|
91
|
+
# find a "t" for each row
|
92
|
+
min, max = floats.minmax
|
93
|
+
return if min == max # edge case
|
94
|
+
t = floats.map { (_1 - min) / (max - min) }
|
95
|
+
|
96
|
+
# now interpolate
|
97
|
+
scale(c, scale, focus, t)
|
98
|
+
end
|
99
|
+
|
100
|
+
def scale_non_numeric(c, scale)
|
101
|
+
# focus on rows that contain values
|
102
|
+
focus = rows.select { _1[c] != config.placeholder }
|
103
|
+
|
104
|
+
# find a "t" for each row. since this column is non-numeric, we create a sorted list of all
|
105
|
+
# values in the column and use the position of each cell value to calculate t. So if the
|
106
|
+
# column contained A..Z, A would end up being t=0 and Z would be t=1.
|
107
|
+
all_values = focus.map { _1[c] }.uniq.sort
|
108
|
+
return if all_values.length < 2 # edge case
|
109
|
+
all_values = all_values.map.with_index do |value, ii|
|
110
|
+
t = ii.to_f / (all_values.length - 1)
|
111
|
+
[value, t]
|
112
|
+
end.to_h
|
113
|
+
t = focus.map { all_values[_1[c]] }
|
114
|
+
|
115
|
+
# now interpolate
|
116
|
+
scale(c, scale, focus, t)
|
117
|
+
end
|
118
|
+
|
119
|
+
# interpolate column c to paint a color scale
|
120
|
+
def scale(c, scale, rows, t)
|
121
|
+
rows.map(&:r).zip(t).each do |r, t|
|
122
|
+
bg = Util::Scale.interpolate(scale, t)
|
123
|
+
fg = Util::Colors.contrast(bg)
|
124
|
+
set_style(r:, c:, style: [fg, bg])
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def mark_style(user_mark)
|
129
|
+
case user_mark
|
130
|
+
when String, Symbol then [nil, user_mark] # assume bg color
|
131
|
+
when Array then user_mark # a Paint array
|
132
|
+
else; :mark # default
|
133
|
+
end
|
134
|
+
end
|
82
135
|
end
|
83
136
|
end
|
84
137
|
end
|
@@ -58,7 +58,7 @@ module TableTennis
|
|
58
58
|
row_style = data.get_style(r:)
|
59
59
|
|
60
60
|
# assemble line by rendering cells
|
61
|
-
enum = (r != :header) ? rows[r].
|
61
|
+
enum = (r != :header) ? rows[r].each : columns.map(&:header)
|
62
62
|
line = enum.map.with_index do |value, c|
|
63
63
|
render_cell(value, r, c, row_style)
|
64
64
|
end.join(" #{pipe} ")
|
@@ -83,7 +83,14 @@ module TableTennis
|
|
83
83
|
value = value.gsub(search) { paint(_1, :search) } if search
|
84
84
|
|
85
85
|
# pad and paint
|
86
|
-
|
86
|
+
if whitespace > 0
|
87
|
+
spaces = " " * whitespace
|
88
|
+
value = if columns[c].alignment == :left
|
89
|
+
"#{value}#{spaces}"
|
90
|
+
else
|
91
|
+
"#{spaces}#{value}"
|
92
|
+
end
|
93
|
+
end
|
87
94
|
paint(value, style)
|
88
95
|
end
|
89
96
|
|
data/lib/table_tennis/table.rb
CHANGED
@@ -15,14 +15,14 @@ module TableTennis
|
|
15
15
|
include Util::Inspectable
|
16
16
|
|
17
17
|
attr_reader :data
|
18
|
-
def_delegators :data, *%i[column_names columns config
|
18
|
+
def_delegators :data, *%i[column_names columns config rows]
|
19
19
|
def_delegators :data, *%i[debug debug_if_slow]
|
20
20
|
|
21
21
|
# Create a new table with options (see Config or README). This is typically
|
22
22
|
# called using TableTennis.new.
|
23
|
-
def initialize(
|
23
|
+
def initialize(rows, options = {}, &block)
|
24
24
|
config = Config.new(options, &block)
|
25
|
-
@data = TableData.new(config:,
|
25
|
+
@data = TableData.new(config:, rows:)
|
26
26
|
sanity!
|
27
27
|
save(config.save) if config.save
|
28
28
|
end
|
@@ -41,7 +41,7 @@ module TableTennis
|
|
41
41
|
def save(path)
|
42
42
|
headers = column_names
|
43
43
|
CSV.open(path, "wb", headers:, write_headers: true) do |csv|
|
44
|
-
rows.each { csv << _1
|
44
|
+
rows.each { csv << _1 }
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
@@ -59,7 +59,7 @@ module TableTennis
|
|
59
59
|
next if rows.empty? # ignore on empty data
|
60
60
|
invalid = config[key].keys - data.column_names
|
61
61
|
if !invalid.empty?
|
62
|
-
raise ArgumentError, "#{key} columns `#{invalid.join(", ")}` not
|
62
|
+
raise ArgumentError, "#{key} columns `#{invalid.join(", ")}` not in (#{column_names})"
|
63
63
|
end
|
64
64
|
end
|
65
65
|
end
|
@@ -9,15 +9,22 @@ module TableTennis
|
|
9
9
|
# account.
|
10
10
|
# 3) Use fat_rows to calculate rows & columns.
|
11
11
|
#
|
12
|
-
#
|
12
|
+
# Generally we try to use this language:
|
13
|
+
# - `row` is a a Row, an array of cells
|
14
|
+
# - `column` is a Column. It doesn't store any data directly but it
|
15
|
+
# can be enumerated.
|
16
|
+
# - `name` is a column.name
|
17
|
+
# - `value` is the value of a cell
|
18
|
+
# - `r` and `c` are row and column indexes
|
19
|
+
#
|
13
20
|
class TableData
|
14
21
|
prepend MemoWise
|
15
22
|
include Util::Inspectable
|
16
23
|
|
17
24
|
attr_accessor :config, :input_rows, :styles
|
18
25
|
|
19
|
-
def initialize(
|
20
|
-
@config, @input_rows = config,
|
26
|
+
def initialize(rows:, config: nil)
|
27
|
+
@config, @input_rows = config, rows
|
21
28
|
|
22
29
|
if !config && !ENV["MINITEST"]
|
23
30
|
raise ArgumentError, "must provide a config"
|
@@ -45,7 +52,7 @@ module TableTennis
|
|
45
52
|
raise ArgumentError, "specified column `#{name}` not found in any row of input data"
|
46
53
|
end
|
47
54
|
end
|
48
|
-
names.map { Column.new(_1,
|
55
|
+
names.map.with_index { Column.new(self, _1, _2) }
|
49
56
|
end
|
50
57
|
memo_wise :columns
|
51
58
|
|
@@ -58,7 +65,7 @@ module TableTennis
|
|
58
65
|
# memoization to cache the result of fat_rows, and then we create final rows
|
59
66
|
# with just the columns we want
|
60
67
|
def rows
|
61
|
-
fat_rows.map { Row.new(
|
68
|
+
fat_rows.map.with_index { Row.new(_2, _1.values_at(*column_names)) }
|
62
69
|
end
|
63
70
|
memo_wise :rows
|
64
71
|
|
@@ -68,14 +75,10 @@ module TableTennis
|
|
68
75
|
|
69
76
|
# Set the style for a cell, row or column. The "style" is a
|
70
77
|
# theme symbol or hex color.
|
71
|
-
def set_style(style:, r: nil, c: nil)
|
72
|
-
styles[[r, c]] = style
|
73
|
-
end
|
78
|
+
def set_style(style:, r: nil, c: nil) = styles[[r, c]] = style
|
74
79
|
|
75
80
|
# Get the style for a cell, row or column.
|
76
|
-
def get_style(r: nil, c: nil)
|
77
|
-
styles[[r, c]]
|
78
|
-
end
|
81
|
+
def get_style(r: nil, c: nil) = styles[[r, c]]
|
79
82
|
|
80
83
|
# what is the width of the columns, not including chrome?
|
81
84
|
def data_width = columns.sum(&:width)
|
@@ -93,9 +96,6 @@ module TableTennis
|
|
93
96
|
#
|
94
97
|
def chrome_width = columns.length * 3 + 1
|
95
98
|
|
96
|
-
# this is handy for tests
|
97
|
-
def first_cell = rows.first.values&.first
|
98
|
-
|
99
99
|
# for debugging
|
100
100
|
def debug(str)
|
101
101
|
return if !config&.debug
|
data/lib/table_tennis/theme.rb
CHANGED
@@ -64,12 +64,16 @@ module TableTennis
|
|
64
64
|
# - a color that works with Colors.get (#fff, or :bold, or "steelblue")
|
65
65
|
# - an array of colors
|
66
66
|
def paint(str, value)
|
67
|
+
# cap memo_wise
|
68
|
+
@_memo_wise[__method__].tap { _1.clear if _1.length > 5000 }
|
69
|
+
|
67
70
|
if (codes = codes(value))
|
68
71
|
str = str.gsub(RESET, "#{RESET}#{codes}")
|
69
72
|
str = "#{codes}#{str}#{RESET}"
|
70
73
|
end
|
71
74
|
str
|
72
75
|
end
|
76
|
+
memo_wise :paint
|
73
77
|
|
74
78
|
# for debugging, mostly
|
75
79
|
def self.info
|
@@ -2,6 +2,8 @@ module TableTennis
|
|
2
2
|
module Util
|
3
3
|
# Named color names and misc color helpers.
|
4
4
|
module Colors
|
5
|
+
prepend MemoWise
|
6
|
+
|
5
7
|
module_function
|
6
8
|
|
7
9
|
NAMED = {
|
@@ -444,6 +446,7 @@ module TableTennis
|
|
444
446
|
def contrast(bg)
|
445
447
|
dark?(bg) ? "white" : "black"
|
446
448
|
end
|
449
|
+
memo_wise self: :contrast
|
447
450
|
|
448
451
|
# [r,g,b] => "#rrggbb"
|
449
452
|
def to_hex(rgb)
|
@@ -452,7 +455,7 @@ module TableTennis
|
|
452
455
|
|
453
456
|
# "#?(rgb|rrggbb|rrrrggggbbbb)" => [r,g,b]
|
454
457
|
def to_rgb(hex)
|
455
|
-
if !hex.match(/\A#?([0-9a-f]+)\
|
458
|
+
if !hex.match(/\A#?([0-9a-f]+)\Z/i)
|
456
459
|
return
|
457
460
|
end
|
458
461
|
hex = $1
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module TableTennis
|
2
|
+
#
|
3
|
+
# This module helps to classifying columns and values. Are they floats? Dates?
|
4
|
+
# Floats strings?
|
5
|
+
#
|
6
|
+
|
7
|
+
module Util
|
8
|
+
# Helpers for measuring and truncating strings.
|
9
|
+
module Identify
|
10
|
+
prepend MemoWise
|
11
|
+
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def identify_column(values)
|
15
|
+
# grab 100, what do we have?
|
16
|
+
types = values.filter_map { identify(_1) }.uniq
|
17
|
+
case types.length
|
18
|
+
when 0 # all nils, too bad
|
19
|
+
when 1
|
20
|
+
types.first # one type, it wins
|
21
|
+
when 2
|
22
|
+
:float if types.sort == %i[float int]
|
23
|
+
end
|
24
|
+
# all mixed up, can't do much with it
|
25
|
+
end
|
26
|
+
|
27
|
+
# Try to identify a single cell value. Peer into strings.
|
28
|
+
def identify(value)
|
29
|
+
case value
|
30
|
+
when nil, "" then return nil
|
31
|
+
when String
|
32
|
+
return :float if float?(value)
|
33
|
+
return :int if int?(value)
|
34
|
+
return nil if na?(value)
|
35
|
+
return :string
|
36
|
+
when Float then return :float
|
37
|
+
when Numeric then return :int
|
38
|
+
end
|
39
|
+
return :time if time?(value)
|
40
|
+
:unknown
|
41
|
+
end
|
42
|
+
|
43
|
+
# tests
|
44
|
+
def na?(str) = str.match?(/\A(n\/a|na|none|\s+)\Z/i)
|
45
|
+
def number?(str) = str.match?(/\A-?[\d,]+(?:[.]?\d*)?\Z/)
|
46
|
+
def float?(str) = str.match?(/\A-?[\d,]+[.]\d*\Z/)
|
47
|
+
def int?(str) = str.match?(/\A-?[\d,]+\Z/)
|
48
|
+
def time?(value) = value.respond_to?(:strftime)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -3,10 +3,9 @@ module TableTennis
|
|
3
3
|
# A mixin to avoid putting all row data into xxx.inspect. This makes
|
4
4
|
# development much easier.
|
5
5
|
module Inspectable
|
6
|
-
def inspect
|
7
|
-
|
8
|
-
def inspect_without_rows
|
6
|
+
def inspect
|
9
7
|
vars = instance_variables.filter_map do
|
8
|
+
next if _1 == :@data || _1 == :@_memo_wise
|
10
9
|
value = instance_variable_get(_1)
|
11
10
|
if value.is_a?(Array) && _1.to_s =~ /rows$/
|
12
11
|
value = if value.length == 1
|
@@ -9,6 +9,15 @@ module TableTennis
|
|
9
9
|
# strip ansi codes
|
10
10
|
def unpaint(str) = str.gsub(/\e\[[0-9;]*m/, "")
|
11
11
|
|
12
|
+
# similar to rails titleize
|
13
|
+
def titleize(str)
|
14
|
+
str = str.gsub(/_id$/, "") # remove _id
|
15
|
+
str = str.tr("_", " ") # remove underscores
|
16
|
+
str = str.gsub(/(\w)([A-Z])/, '\1 \2') # OneTwo => One Two
|
17
|
+
str = str.split.map(&:capitalize).join(" ") # capitalize
|
18
|
+
str
|
19
|
+
end
|
20
|
+
|
12
21
|
def width(text)
|
13
22
|
simple?(text) ? text.length : Unicode::DisplayWidth.of(text)
|
14
23
|
end
|
@@ -38,9 +47,10 @@ module TableTennis
|
|
38
47
|
|
39
48
|
# we've gone too far. do we need to pop for the ellipsis?
|
40
49
|
text = list[0, _1]
|
41
|
-
text.pop if w ==
|
50
|
+
text.pop if width - w == stop
|
42
51
|
return "#{text.join}…"
|
43
52
|
end
|
53
|
+
|
44
54
|
text
|
45
55
|
end
|
46
56
|
|
data/lib/table_tennis/version.rb
CHANGED
data/lib/table_tennis.rb
CHANGED
@@ -24,6 +24,7 @@ require "table_tennis/stage/painter"
|
|
24
24
|
require "table_tennis/stage/render"
|
25
25
|
|
26
26
|
require "table_tennis/util/colors"
|
27
|
+
require "table_tennis/util/identify"
|
27
28
|
require "table_tennis/util/scale"
|
28
29
|
require "table_tennis/util/strings"
|
29
30
|
require "table_tennis/util/termbg"
|
data/table_tennis.gemspec
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: table_tennis
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Adam Doppelt
|
8
8
|
bindir: bin
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-04-
|
10
|
+
date: 2025-04-19 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: csv
|
@@ -106,6 +106,7 @@ files:
|
|
106
106
|
- lib/table_tennis/table_data.rb
|
107
107
|
- lib/table_tennis/theme.rb
|
108
108
|
- lib/table_tennis/util/colors.rb
|
109
|
+
- lib/table_tennis/util/identify.rb
|
109
110
|
- lib/table_tennis/util/inspectable.rb
|
110
111
|
- lib/table_tennis/util/scale.rb
|
111
112
|
- lib/table_tennis/util/strings.rb
|