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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03c38407a65288ded066cc1986f53453d8dafe6e51096ea597e843c8ad115afb
4
- data.tar.gz: a76db1bbb2a0ef775a629f71f6f76c2ee5c67a42255ba9856c5ccb9f66bb89bf
3
+ metadata.gz: 1b4fb283f2a6461183187e0833f77aa8934f40c2611a88b4e65da5a7b0e0b51a
4
+ data.tar.gz: 22b0ed370736791f838fedc5a8b3044664250f4014608525807f81d28040b4b6
5
5
  SHA512:
6
- metadata.gz: 51d515f45a6f62d6ede57e5b8d752ccd5a637dc107d170e526693600d91e9a91988cef1ee570104c413951759cf673e80d569076544a137398ac09bf1b2735e7
7
- data.tar.gz: 3e269c16f3a95e4e948d55d8713d5349d2e5b5527b97c0fd5722d8cee88e17f182078d720b9bafb6d233dc9feb76f209a8b17780549ad03468817dcaaeb2aedb
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}}
@@ -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. Mostly used for layout calculations.
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
- attr_reader :name
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, data)
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
- @data.rows.each { yield(_1[name]) }
25
+ rows.each { yield(_1[c]) }
22
26
  self
23
27
  end
24
28
 
25
- def each_index(&block)
26
- return to_enum(__method__) unless block_given?
27
- @data.rows.each_index { yield(_1) }
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 map!(&block) = @data.rows.each { _1[name] = yield(_1[name]) }
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)
@@ -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["TM_DEBUG"]
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
- value = value.to_s if option == :title && value.is_a?(Symbol)
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
@@ -1,9 +1,11 @@
1
1
  module TableTennis
2
- # A single table row (a hash). Doesn't have much behavior.
3
- class Row < Hash
4
- def initialize(column_names, fat_row)
5
- super()
6
- column_names.each { self[_1] = fat_row[_1] }
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 cells that contain various
4
- # ruby objects and formatting each one as a string. `rows` are formatted in
5
- # place.
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
- setup_fns
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.transform_values! do
17
- fn = fns[fn_for(_1)] || fns[:other]
18
- fn.call(_1)
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
- def fn_for(value)
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 float?(value) ? :floatstr : :str
26
- when Float then :float
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 float?(str) = str.match?(/^-?\d+[.]\d+$/)
38
-
39
- def setup_fns
40
- placeholder = config.placeholder || ""
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
- other = ->(x) { str.call(x.to_s) }
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
- # default behavior
53
- @fns = {other:, str:}
70
+ DELIMS = /(\d)(?=(\d\d\d)+(?!\d))/
54
71
 
55
- # now mix in optional float/time formatters
56
- if config.digits
57
- fmt = "%.#{config.digits}f"
58
- @to_f_cache = Hash.new { _1[_2] = fmt % _2.to_f }
59
- fns[:float] = -> { fmt % _1 }
60
- fns[:floatstr] = -> { @to_f_cache[_1] }
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
- if config.strftime
63
- fns[:time] = -> { _1.strftime(config.strftime) }
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 if config.placeholder
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 (mark_style = config.mark&.call(input_rows[r]))
46
- style = mark_style.is_a?(Symbol) ? mark_style : :mark
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
- next if !scale
56
-
57
- floats = column.map { _1.to_f if _1 =~ /^-?\d+(\.\d+)?$/ }
58
- min, max = floats.compact.minmax
59
- next if min == max # edge case
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.each_value.with_index do |value, c|
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].each_value : columns.map(&:header)
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
- value = "#{value}#{" " * whitespace}" if whitespace > 0
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
 
@@ -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 input_rows rows]
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(input_rows, options = {}, &block)
23
+ def initialize(rows, options = {}, &block)
24
24
  config = Config.new(options, &block)
25
- @data = TableData.new(config:, input_rows:)
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.values }
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 found in input data"
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
- # Rows are hash tables and column names are symbols.
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(input_rows:, config: nil)
20
- @config, @input_rows = config, input_rows
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, self) }
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(column_names, _1) }
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
@@ -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]+)\z/i)
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 = ENV["MINITEST"] ? super : inspect_without_rows
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 == 1
50
+ text.pop if width - w == stop
42
51
  return "#{text.join}…"
43
52
  end
53
+
44
54
  text
45
55
  end
46
56
 
@@ -202,7 +202,7 @@ module TableTennis
202
202
  private_class_method :env_colorfgbg
203
203
 
204
204
  def debug(s)
205
- puts "termbg: #{s}" if ENV["TM_DEBUG"]
205
+ puts "termbg: #{s}" if ENV["TT_DEBUG"]
206
206
  end
207
207
  private_class_method :debug
208
208
  end
@@ -1,3 +1,3 @@
1
1
  module TableTennis
2
- VERSION = "0.0.1".freeze
2
+ VERSION = "0.0.2"
3
3
  end
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
@@ -16,7 +16,7 @@ Gem::Specification.new do |s|
16
16
  }
17
17
 
18
18
  # what's in the gem?
19
- s.files = `git ls-files`.split("\n").grep_v(%r{^(bin|samples|test)/})
19
+ s.files = `git ls-files`.split("\n").grep_v(%r{^(bin|demo|test)/})
20
20
  s.require_paths = ["lib"]
21
21
 
22
22
  # gem dependencies
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.1
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-11 00:00:00.000000000 Z
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