fat_table 0.9.8 → 1.0.0

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.
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
  require 'rdoc/task'
@@ -13,4 +15,7 @@ end
13
15
 
14
16
  RSpec::Core::RakeTask.new(:spec)
15
17
 
18
+ require "gem_docs"
19
+ GemDocs.install
20
+
16
21
  task :default => [:spec, :rubocop]
data/bin/ft_console CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'fat_table'
data/fat_table.gemspec CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  lib = File.expand_path('../lib', __FILE__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'fat_table/version'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Array
2
4
  # Map booleans true to 1 and false to 0 so they can be compared in a sort
3
5
  # with the <=> operator.
@@ -0,0 +1,196 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'debug'
4
+
5
+ # This refinement contains methods that extend the String class to massage
6
+ # strings that represent numbers, such as adding commas, pre- and
7
+ # post-padding, etc.
8
+ module NumericString
9
+ # You can control how NumericString behaves by supplying a new
10
+ # NumericString::Config to the config: parameter of these methods. Calling
11
+ # `NumericString::Config.build` gives you the deafult configuration with
12
+ # whatever overrides you give it. You can also modify an existing config
13
+ # with some overrides. For example:
14
+ #
15
+ # #+begin_src ruby
16
+ # cfg = NumericString::Config.build(group_size: 4)
17
+ # cfg2 = cfg.with(group_char: '_')
18
+ #
19
+ # "1234567.89".add_grouping(config: cfg)
20
+ # => "123,4567.89"
21
+ # "1234567.89".add_grouping(config: cfg2)
22
+ # => '123_4567.89'
23
+ # #+end_src
24
+ Config = Struct.new(
25
+ :group_char,
26
+ :group_size,
27
+ :decimal_char,
28
+ :currency_symbol,
29
+ :pre_pad_char,
30
+ :post_pad_char,
31
+ keyword_init: true,
32
+ ) do
33
+ DEFAULTS = {
34
+ group_char: ',',
35
+ group_size: 3,
36
+ decimal_char: '.',
37
+ currency_symbol: '$',
38
+ pre_pad_char: '0',
39
+ post_pad_char: '0',
40
+ }.freeze
41
+
42
+ def self.default
43
+ @default ||= new(**DEFAULTS).freeze
44
+ end
45
+
46
+ # Build from defaults, overriding selectively
47
+ def self.build(**overrides)
48
+ new(**DEFAULTS.merge(overrides)).freeze
49
+ end
50
+
51
+ # Clone-with-changes (very Ruby, very nice)
52
+ def with(**overrides)
53
+ self.class.build(
54
+ group_char: overrides.fetch(:group_char, group_char),
55
+ group_size: overrides.fetch(:group_size, group_size),
56
+ decimal_char: overrides.fetch(:decimal_char, decimal_char),
57
+ currency_symbol: overrides.fetch(:currency_symbol, currency_symbol),
58
+ pre_pad_char: overrides.fetch(:pre_pad_char, pre_pad_char),
59
+ post_pad_char: overrides.fetch(:post_pad_char, post_pad_char),
60
+ )
61
+ end
62
+ end
63
+
64
+ refine String do
65
+ # If self is a valid decimal number, add grouping commas to the whole
66
+ # part, retaining any fractional part and currency symbol undisturbed.
67
+ # The optional cond: parameter can contain a test to determine if the
68
+ # grouping ought to be performed. If (1) self is not a valid decimal
69
+ # number string, (2) the whole part already contains grouping characters,
70
+ # or (3) cond: is falsey, return self.
71
+ def add_grouping(cond: true, config: Config.default)
72
+ return self unless cond
73
+ return self unless valid_num?(config:)
74
+
75
+ cur, whole, frac = cur_whole_frac(config:)
76
+ return self if whole.include?(config.group_char)
77
+
78
+ whole = whole.split('').reverse
79
+ .each_slice(config.group_size).to_a
80
+ .map { |a| a.reverse.join }
81
+ .reverse
82
+ .join(config.group_char)
83
+ cur + whole + frac
84
+ end
85
+
86
+ alias_method :add_commas, :add_grouping
87
+
88
+ # If self is a valid decimal number, add the currency symbol (per
89
+ # NumericString::Config.currency_symbol) to the front of the number
90
+ # string, retaining any grouping characters undisturbed. The optional
91
+ # cond: parameter can contain a test to determine if the currency symbol
92
+ # ought to be pre-pended. If (1) self is not a valid decimal number
93
+ # string, (2) the currency symbol is already present, or (3) cond: is
94
+ # falsey, return self.
95
+ def add_currency(cond: true, config: Config.default)
96
+ return self unless cond
97
+ return self unless valid_num?(config:)
98
+
99
+ md = match(num_re(config:))
100
+ return self unless md[:cur].blank?
101
+
102
+ config.currency_symbol + self
103
+ end
104
+
105
+ def add_pre_digits(n, cond: true, config: Config.default)
106
+ return self unless cond
107
+ return self if n <= 0
108
+ return self unless valid_num?(config:)
109
+
110
+ cur, whole, frac = cur_whole_frac(config:)
111
+ n_pads = [n - whole.delete(config.group_char).size, 0].max
112
+ padding = config.pre_pad_char * n_pads
113
+ "#{cur}#{padding}#{whole}#{frac}"
114
+ end
115
+
116
+ def add_post_digits(n, cond: true, config: Config.default)
117
+ return self unless cond
118
+ return self unless valid_num?(config:)
119
+
120
+ cur, whole, frac = cur_whole_frac(config:)
121
+ frac_digs = frac.size - 1 # frac includes the decimal character
122
+ if n >= frac_digs
123
+ n_pads = [n - frac_digs, 0].max
124
+ padding = config.post_pad_char * n_pads
125
+ "#{cur}#{whole}#{frac}#{padding}"
126
+ elsif n.zero?
127
+ # Round up last digit of whole if first digit of frac >= 5
128
+ if frac[1].to_i >= 5
129
+ whole = whole[0..-2] + (whole[-1].to_i + 1).to_s
130
+ end
131
+ # No fractional part
132
+ "#{cur}#{whole}"
133
+ elsif n.negative?
134
+ # This calls for rounding the whole part to nearest 10^n.abs and
135
+ # dropping the frac part.
136
+ ndigs_in_whole = whole.delete(config.group_char).size
137
+ nplaces = [ndigs_in_whole - 1, n.abs].min
138
+ # Replace the right-most nplaces digs with the pre-pad character.
139
+ replace_count = 0
140
+ new_whole = +''
141
+ round_char = whole.delete(config.group_char)[-1]
142
+ rounded = false
143
+ whole.split('').reverse_each do |c|
144
+ if c == config.group_char
145
+ new_whole << c
146
+ elsif replace_count < nplaces
147
+ new_whole << config.pre_pad_char
148
+ round_char = c
149
+ replace_count += 1
150
+ elsif !rounded
151
+ new_whole <<
152
+ if round_char.to_i >= 5
153
+ (c.to_i + 1).to_s
154
+ else
155
+ c
156
+ end
157
+ rounded = true
158
+ else
159
+ new_whole << c
160
+ end
161
+ end
162
+ "#{cur}#{new_whole.reverse}"
163
+ else
164
+ # We have to shorten the fractional part, which required rounding.
165
+ last_frac_dig = frac[n]
166
+ following_frac_dig = frac[n + 1]
167
+ if following_frac_dig.to_i >= 5
168
+ last_frac_dig = (last_frac_dig.to_i + 1).to_s
169
+ end
170
+ frac = frac[0..(n - 1)] + last_frac_dig
171
+ padding = ''
172
+ "#{cur}#{whole}#{frac}#{padding}"
173
+ end
174
+ end
175
+
176
+ private
177
+
178
+ def num_re(config: Config.default)
179
+ cur_sym = Regexp.quote(config.currency_symbol)
180
+ grp_char = Regexp.quote(config.group_char)
181
+ dec_char = Regexp.quote(config.decimal_char)
182
+ /\A(?<cur>#{cur_sym})?(?<whole>[0-9#{grp_char}]+)(?<frac>#{dec_char}[0-9]*)?\z/
183
+ end
184
+
185
+ # Return the currency, whole and fractional parts of a string with a possible
186
+ # decimal point attached to the frac part if present.
187
+ def cur_whole_frac(config: Config.default)
188
+ match = match(num_re(config:))
189
+ [match[:cur].to_s, match[:whole].to_s, match[:frac].to_s]
190
+ end
191
+
192
+ def valid_num?(config: Config.default)
193
+ match?(num_re(config:))
194
+ end
195
+ end
196
+ end
data/lib/core_ext.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'core_ext/array'
4
+ require_relative 'core_ext/numeric_string'
@@ -86,7 +86,11 @@ module FatTable
86
86
  # col.header #=> :prices
87
87
  # col.sum #=> 18376.75
88
88
  #
89
- # @param
89
+ # @param header [String, Symbol] the name of the column header
90
+ # @param items [Array<String>, Array<DateTime>, Array<Numeric>, Array<Boolean>] the initial data items in column
91
+ # @param type [String] the column type: 'String', 'Numeric', 'DateTime', 'Boolean', or 'NilClass'
92
+ # @param tolerant [Boolean] whether the column accepts unconvertable items not of its type as Strings
93
+ # @return [Column] the new Column
90
94
  def initialize(header:, items: [], type: 'NilClass', tolerant: false)
91
95
  @raw_header = header
92
96
  @header =
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FatTable
2
4
  module Convert
3
5
  # Convert val to the type of key, a ruby class constant, such as Date,
@@ -213,7 +213,7 @@ module FatTable
213
213
  end
214
214
 
215
215
  # Insert a possibly calculated value for the label in the appropriate
216
- # @values column.
216
+ # `@values` column.
217
217
  def insert_labels_in_label_col
218
218
  if group
219
219
  @values[@label_col] = []
@@ -67,7 +67,7 @@ module FatTable
67
67
  bgcolor: 'none',
68
68
  hms: false,
69
69
  pre_digits: 0,
70
- post_digits: 0,
70
+ post_digits: 4,
71
71
  commas: false,
72
72
  currency: false,
73
73
  datetime_fmt: '%F %H:%M:%S',
@@ -729,11 +729,11 @@ module FatTable
729
729
  # Parsing and validation routines
730
730
  ############################################################################
731
731
 
732
- private
733
-
734
732
  # Re to match a color name
735
733
  CLR_RE = /(?:[-_a-zA-Z0-9 ]*)/
736
734
 
735
+ private
736
+
737
737
  # Return a hash that reflects the formatting instructions given in the
738
738
  # string fmt. Raise an error if it contains invalid formatting instructions.
739
739
  # If fmt contains conflicting instructions, say C and L, there is no
@@ -1026,63 +1026,37 @@ module FatTable
1026
1026
  # above. Only device-independent formatting is done here. Device dependent
1027
1027
  # formatting (e.g., color) can be done in a subclass of Formatter by
1028
1028
  # specializing this method.
1029
+
1030
+ using NumericString
1031
+
1029
1032
  def format_numeric(val, istruct)
1030
1033
  return istruct[:nil_text] if val.nil?
1031
1034
  return val.secs_to_hms if istruct[:hms]
1032
1035
 
1033
- if istruct[:commas]
1034
- # Commify the whole number part if not done already.
1035
- result = val.commas(istruct[:post_digits])
1036
- else
1037
- result = val.round(istruct[:post_digits]).to_s
1038
- match = result.match(/\.(\d+)\z/)
1039
- if match && (match[1]&.size&.< istruct[:post_digits])
1040
- # Add trailing zeros to pad out post_digits
1041
- n_zeros = [istruct[:post_digits] - match[1].size, 0].max
1042
- zeros = '0' * n_zeros
1043
- result += zeros
1044
- end
1045
- result
1046
- end
1047
-
1048
- if istruct[:pre_digits].positive?
1049
- match = result.match(/\A([\d,]+)(\.\d+)?\z/)
1050
- whole_part = match[1]
1051
- frac_part = match[2]
1052
- n_zeros = [istruct[:pre_digits] - whole_part.delete(',').size, 0].max
1053
- result =
1054
- if n_zeros.positive?
1055
- if istruct[:commas]
1056
- # Insert leading zeros with commas
1057
- pre_comma_match = whole_part.match(/\A(\d+),/)
1058
- if pre_comma_match
1059
- n_partial_zeros = 3 - pre_comma_match[1].size
1060
- whole_part = "0" * n_partial_zeros + whole_part
1061
- n_zeros -= n_partial_zeros
1062
- end
1063
- zeros = ''
1064
- if n_zeros.positive?
1065
- zeros = "0" * n_zeros
1066
- if n_zeros > 3 && istruct[:commas]
1067
- zeros = zeros.reverse.gsub!(/([0-9]{3})/, "\\1,").reverse
1068
- end
1069
- end
1070
- "#{zeros},#{whole_part}#{frac_part}"
1071
- else
1072
- # Insert leading zeros without commas
1073
- zeros = "0" * n_zeros
1074
- "#{zeros}#{whole_part}#{frac_part}"
1075
- end
1076
- else
1077
- "#{whole_part}#{frac_part}"
1078
- end
1036
+ case val
1037
+ when Integer
1038
+ val.to_s
1039
+ .add_pre_digits(istruct[:pre_digits], cond: istruct[:pre_digits].positive?)
1040
+ .add_commas(cond: istruct[:commas])
1041
+ .add_currency(cond: istruct[:currency])
1042
+ when Rational
1043
+ num = val.numerator.to_s
1044
+ .add_pre_digits(istruct[:pre_digits], cond: istruct[:pre_digits].positive?)
1045
+ .add_commas(cond: istruct[:commas])
1046
+ .add_currency(cond: istruct[:currency])
1047
+ den = val.denominator.to_s
1048
+ .add_pre_digits(istruct[:pre_digits], cond: istruct[:pre_digits].positive?)
1049
+ .add_commas(cond: istruct[:commas])
1050
+ "#{num}/#{den}"
1051
+ when Float, BigDecimal
1052
+ val.to_s
1053
+ .add_pre_digits(istruct[:pre_digits], cond: istruct[:pre_digits].positive?)
1054
+ .add_post_digits(istruct[:post_digits])
1055
+ .add_commas(cond: istruct[:commas])
1056
+ .add_currency(cond: istruct[:currency])
1079
1057
  else
1080
- result
1081
- end
1082
- if istruct[:currency]
1083
- result = "#{FatTable.currency_symbol}#{result}"
1058
+ raise ArgumentError, "cannot apply format_numeric to #{val} of class #{val.class}"
1084
1059
  end
1085
- result
1086
1060
  end
1087
1061
 
1088
1062
  # Apply non-device-dependent string formatting instructions.
@@ -1251,7 +1225,7 @@ module FatTable
1251
1225
  map = {}
1252
1226
  table.headers.each do |h|
1253
1227
  istruct = format_at[:header][h]
1254
- map[h] = [h, format_cell(h.as_string, istruct, decorate: decorate)]
1228
+ map[h] = [h, format_cell(h.entitle, istruct, decorate: decorate)]
1255
1229
  end
1256
1230
  map
1257
1231
  end
@@ -1294,7 +1268,7 @@ module FatTable
1294
1268
  # Format group footers
1295
1269
  gfooters.each_pair do |_label, gfooter|
1296
1270
  out_rows << nil
1297
- gfoot_row = Hash.new([nil, ''])
1271
+ gfoot_row = Hash.new([nil, ''].freeze)
1298
1272
  gfooter.to_h(grp_k).each_pair do |h, v|
1299
1273
  istruct = format_at[:gfooter][h]
1300
1274
  gfoot_row[h] = [v, format_cell(v, istruct, decorate: decorate)]
@@ -1313,7 +1287,7 @@ module FatTable
1313
1287
 
1314
1288
  footers.each_pair do |_label, foot|
1315
1289
  out_rows << nil
1316
- foot_row = Hash.new([nil, ''])
1290
+ foot_row = Hash.new([nil, ''].freeze)
1317
1291
  foot.to_h.each_pair do |h, v|
1318
1292
  istruct = format_at[:gfooter][h]
1319
1293
  foot_row[h] = [v, format_cell(v, istruct, decorate: decorate)]
@@ -26,8 +26,8 @@ module FatTable
26
26
  end
27
27
 
28
28
  # Taken from the Rainbow gem's list of valid colors.
29
-
30
- self.valid_colors = File.readlines(File.join(__dir__, 'xcolors.txt'), chomp: true)
29
+ color_path = File.expand_path("../../../data/xcolors.txt", __dir__)
30
+ self.valid_colors = File.readlines(color_path, chomp: true)
31
31
 
32
32
  # LaTeX commands to load the needed packages based on the :environement
33
33
  # option. For now, just handles the default 'longtable' :environment. The
@@ -49,6 +49,24 @@ module FatTable
49
49
  result
50
50
  end
51
51
 
52
+ # :stopdoc:
53
+ # Unicode line-drawing characters. We use double lines before and after the
54
+ # table and single lines for the sides and hlines between groups and
55
+ # footers.
56
+ UPPER_LEFT = "\u2552"
57
+ UPPER_RIGHT = "\u2555"
58
+ DOUBLE_RULE = "\u2550"
59
+ UPPER_TEE = "\u2564"
60
+ VERTICAL_RULE = "\u2502"
61
+ LEFT_TEE = "\u251C"
62
+ HORIZONTAL_RULE = "\u2500"
63
+ SINGLE_CROSS = "\u253C"
64
+ RIGHT_TEE = "\u2524"
65
+ LOWER_LEFT = "\u2558"
66
+ LOWER_RIGHT = "\u255B"
67
+ LOWER_TEE = "\u2567"
68
+ # :startdoc:
69
+
52
70
  private
53
71
 
54
72
  def color_valid?(clr)
@@ -99,24 +117,6 @@ module FatTable
99
117
  colorize(str, @options[:frame_fg], @options[:frame_bg])
100
118
  end
101
119
 
102
- # :stopdoc:
103
- # Unicode line-drawing characters. We use double lines before and after the
104
- # table and single lines for the sides and hlines between groups and
105
- # footers.
106
- UPPER_LEFT = "\u2552"
107
- UPPER_RIGHT = "\u2555"
108
- DOUBLE_RULE = "\u2550"
109
- UPPER_TEE = "\u2564"
110
- VERTICAL_RULE = "\u2502"
111
- LEFT_TEE = "\u251C"
112
- HORIZONTAL_RULE = "\u2500"
113
- SINGLE_CROSS = "\u253C"
114
- RIGHT_TEE = "\u2524"
115
- LOWER_LEFT = "\u2558"
116
- LOWER_RIGHT = "\u255B"
117
- LOWER_TEE = "\u2567"
118
- # :startdoc:
119
-
120
120
  def upper_left
121
121
  if options[:unicode]
122
122
  UPPER_LEFT
@@ -135,7 +135,7 @@ module FatTable
135
135
  # might have been set by a subclass instance.
136
136
  def empty_dup(result_cols = nil)
137
137
  result_cols ||= heads
138
- result_types = types.select { |k, _v| result_cols.include?(k) }
138
+ result_types = types.slice(*result_cols)
139
139
  result = self.class.new(result_cols, **result_types)
140
140
  tolerant_cols.each do |h|
141
141
  result.tolerant_cols << h
@@ -721,7 +721,7 @@ module FatTable
721
721
  # row in the table as a group boundary. An attempt to add a boundary to
722
722
  # an empty table has no effect. We adopt the convention that the last row
723
723
  # of the table always marks an implicit boundary even if it is not in the
724
- # @explicit_boundaries array. When we "mark" a boundary, we intend it to
724
+ # `@explicit_boundaries` array. When we "mark" a boundary, we intend it to
725
725
  # be an explicit boundary, even if it marks the last row of the table.
726
726
  def mark_boundary(row_num = nil)
727
727
  return self if empty?
@@ -774,7 +774,7 @@ module FatTable
774
774
  # user's point of view are indexed starting at 0.
775
775
  def row_index_to_group_index(row_num)
776
776
  boundaries.each_with_index do |b_last, g_num|
777
- return (g_num + 1) if row_num <= b_last
777
+ return g_num + 1 if row_num <= b_last
778
778
  end
779
779
  0
780
780
  end
@@ -918,8 +918,8 @@ module FatTable
918
918
  # access the instance variable @row, as the row number of the row being
919
919
  # evaluated, and @group, as the group number of the row being evaluated.
920
920
  #
921
- # 4. a hash in +new_cols+ with one of the special keys, +ivars: {literal
922
- # hash}+, +before_hook: 'ruby-code'+, or +after_hook: 'ruby-code'+ for
921
+ # 4. a hash in +new_cols+ with one of the special keys, `+ivars: {literal
922
+ # hash}+`, +before_hook: 'ruby-code'+, or +after_hook: 'ruby-code'+ for
923
923
  # defining custom instance variables to be used during evaluation of
924
924
  # parameters described in point 3 and hooks of ruby code snippets to be
925
925
  # evaluated before and after processing each row.
@@ -1150,7 +1150,7 @@ module FatTable
1150
1150
  # exception will be thrown. Duplicates are eliminated from the result. Any
1151
1151
  # groups present in either Table are eliminated in the output Table.
1152
1152
  def intersect(other)
1153
- set_operation(other, :intersect, distinct: true)
1153
+ set_operation(other, :&, distinct: true)
1154
1154
  end
1155
1155
 
1156
1156
  # :category: Operators
@@ -1163,7 +1163,7 @@ module FatTable
1163
1163
  # will be thrown. Duplicates are not eliminated from the result. Resets
1164
1164
  # groups.
1165
1165
  def intersect_all(other)
1166
- set_operation(other, :intersect, distinct: false)
1166
+ set_operation(other, :&, distinct: false)
1167
1167
  end
1168
1168
 
1169
1169
  # :category: Operators
@@ -1176,7 +1176,7 @@ module FatTable
1176
1176
  # are eliminated from the result. Any groups present in either Table are
1177
1177
  # eliminated in the output Table.
1178
1178
  def except(other)
1179
- set_operation(other, :difference, distinct: true)
1179
+ set_operation(other, :-, distinct: true)
1180
1180
  end
1181
1181
 
1182
1182
  # :category: Operators
@@ -1189,7 +1189,7 @@ module FatTable
1189
1189
  # are /not/ eliminated from the result. Any groups present in either Table
1190
1190
  # are eliminated in the output Table.
1191
1191
  def except_all(other)
1192
- set_operation(other, :difference, distinct: false)
1192
+ set_operation(other, :-, distinct: false)
1193
1193
  end
1194
1194
 
1195
1195
  # An Array of symbols for the valid join types.
@@ -1521,7 +1521,7 @@ module FatTable
1521
1521
  groups = sorted_tab.rows.group_by do |r|
1522
1522
  group_cols.map { |k| r[k] }
1523
1523
  end
1524
- grp_types = types.select { |k, _v| group_cols.include?(k) }
1524
+ grp_types = types.slice(*group_cols)
1525
1525
  result = Table.new(*group_cols, **grp_types)
1526
1526
  groups.each_pair do |_vals, grp_rows|
1527
1527
  result << row_from_group(grp_rows, group_cols, agg_cols)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module FatTable
4
4
  # The current version of FatTable
5
- VERSION = '0.9.8'
5
+ VERSION = '1.0.0'
6
6
  end