table_print 0.2.3 → 1.0.0.rc3
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/.rspec +1 -0
- data/.rvmrc +1 -1
- data/.travis.yml +5 -0
- data/Gemfile +11 -10
- data/README.rdoc +85 -32
- data/Rakefile +13 -13
- data/VERSION +1 -1
- data/features/adding_columns.feature +48 -0
- data/features/configuring_output.feature +57 -0
- data/features/excluding_columns.feature +28 -0
- data/features/sensible_defaults.feature +86 -0
- data/features/support/step_definitions/before.rb +3 -0
- data/features/support/step_definitions/steps.rb +77 -0
- data/lib/cattr.rb +46 -0
- data/lib/column.rb +45 -0
- data/lib/config.rb +36 -0
- data/lib/config_resolver.rb +91 -0
- data/lib/fingerprinter.rb +85 -0
- data/lib/formatter.rb +45 -0
- data/lib/hash_extensions.rb +37 -0
- data/lib/kernel_extensions.rb +12 -0
- data/lib/printable.rb +22 -0
- data/lib/returnable.rb +21 -0
- data/lib/row_group.rb +227 -0
- data/lib/table_print.rb +33 -389
- data/spec/column_spec.rb +71 -0
- data/spec/config_resolver_spec.rb +236 -0
- data/spec/config_spec.rb +52 -0
- data/spec/fingerprinter_spec.rb +151 -0
- data/spec/formatter_spec.rb +78 -0
- data/spec/hash_extensions_spec.rb +21 -0
- data/spec/printable_spec.rb +51 -0
- data/spec/returnable_spec.rb +23 -0
- data/spec/row_group_spec.rb +466 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/table_print_spec.rb +59 -0
- data/table_print.gemspec +50 -26
- metadata +147 -68
- data/Gemfile.lock +0 -20
- data/test/helper.rb +0 -56
- data/test/test_column.rb +0 -379
- data/test/test_table_print.rb +0 -162
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'returnable'
|
2
|
+
|
3
|
+
module Kernel
|
4
|
+
def tp(data=[], *options)
|
5
|
+
start = Time.now
|
6
|
+
printer = TablePrint::Printer.new(data, options)
|
7
|
+
puts printer.table_print unless data.is_a? Class
|
8
|
+
TablePrint::Returnable.new(Time.now - start) # we have to return *something*, might as well be execution time.
|
9
|
+
end
|
10
|
+
|
11
|
+
module_function :tp
|
12
|
+
end
|
data/lib/printable.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module TablePrint
|
2
|
+
module Printable
|
3
|
+
# Sniff the data class for non-standard methods to use as a baseline for display
|
4
|
+
def self.default_display_methods(target)
|
5
|
+
return target.class.columns.collect(&:name) if target.class.respond_to? :columns
|
6
|
+
|
7
|
+
methods = []
|
8
|
+
target.methods.each do |method_name|
|
9
|
+
method = target.method(method_name)
|
10
|
+
|
11
|
+
if method.owner == target.class
|
12
|
+
if method.arity == 0 #
|
13
|
+
methods << method_name.to_s
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
methods.delete_if { |m| m[-1].chr == "!" } # don't use dangerous methods
|
19
|
+
methods
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/returnable.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
module TablePrint
|
2
|
+
class Returnable
|
3
|
+
def initialize(string_value="")
|
4
|
+
@string_value = string_value
|
5
|
+
end
|
6
|
+
|
7
|
+
def set(klass, *config)
|
8
|
+
TablePrint::Config.set(klass, config)
|
9
|
+
"Set table_print config for #{klass}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def clear(klass)
|
13
|
+
TablePrint::Config.clear(klass)
|
14
|
+
"Cleared table_print config for #{klass}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
@string_value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/lib/row_group.rb
ADDED
@@ -0,0 +1,227 @@
|
|
1
|
+
require 'formatter'
|
2
|
+
require 'column'
|
3
|
+
|
4
|
+
module TablePrint
|
5
|
+
|
6
|
+
module RowRecursion
|
7
|
+
attr_accessor :parent
|
8
|
+
attr_accessor :children
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@children = []
|
12
|
+
@columns = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_child(child)
|
16
|
+
@children << child
|
17
|
+
child.parent = self
|
18
|
+
end
|
19
|
+
|
20
|
+
def insert_children(i, children)
|
21
|
+
@children.insert(i, children).flatten!
|
22
|
+
children.each {|c| c.parent = self }
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_children(children)
|
27
|
+
@children.concat children
|
28
|
+
children.each { |c| c.parent = self }
|
29
|
+
self
|
30
|
+
end
|
31
|
+
|
32
|
+
def child_count
|
33
|
+
@children.length
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_column(name, column)
|
37
|
+
return parent.set_column(name, column) if parent
|
38
|
+
@columns[name.to_s] = column
|
39
|
+
end
|
40
|
+
|
41
|
+
def columns
|
42
|
+
return parent.columns if parent
|
43
|
+
|
44
|
+
raw_column_names.collect{|k, v| column_for(k)}
|
45
|
+
end
|
46
|
+
|
47
|
+
def column_count
|
48
|
+
return parent.column_count if parent
|
49
|
+
@columns.size
|
50
|
+
end
|
51
|
+
|
52
|
+
def column_for(name)
|
53
|
+
return parent.column_for(name) if parent
|
54
|
+
column = @columns[name.to_s] ||= Column.new(:name => name)
|
55
|
+
|
56
|
+
# assign the data sets to the column before we return it
|
57
|
+
# do this as late as possible, since new rows could be added at any time
|
58
|
+
column.data ||= raw_column_data(column.name)
|
59
|
+
column
|
60
|
+
end
|
61
|
+
|
62
|
+
def width
|
63
|
+
return parent.width if parent
|
64
|
+
columns.collect(&:width).inject(&:+) + (columns.length - 1) * 3 # add (n-1)*3 for the 3-character separator
|
65
|
+
end
|
66
|
+
|
67
|
+
def horizontal_separator
|
68
|
+
'-' * header.length # columns don't know how to respect max_width (formatter does that) so just match the header
|
69
|
+
end
|
70
|
+
|
71
|
+
def header
|
72
|
+
padded_names = columns.collect do |column|
|
73
|
+
f = FixedWidthFormatter.new(column.width)
|
74
|
+
f.format(column.name)
|
75
|
+
end
|
76
|
+
|
77
|
+
padded_names.join(" | ").upcase
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_formatter(name, formatter)
|
81
|
+
return unless column_for(name)
|
82
|
+
column_for(name).add_formatter(formatter)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
class RowGroup
|
87
|
+
include RowRecursion
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
super
|
91
|
+
@skip_first_row = false
|
92
|
+
end
|
93
|
+
|
94
|
+
def raw_column_data(column_name)
|
95
|
+
@children.collect { |r| r.raw_column_data(column_name) }.flatten
|
96
|
+
end
|
97
|
+
|
98
|
+
def raw_column_names
|
99
|
+
return @raw_column_names if @raw_column_names
|
100
|
+
@raw_column_names = @children.collect { |r| r.raw_column_names }.flatten.uniq
|
101
|
+
end
|
102
|
+
|
103
|
+
def vis(prefix="")
|
104
|
+
puts "#{prefix}group"
|
105
|
+
children.each{|c| c.vis(prefix + " ")}
|
106
|
+
end
|
107
|
+
|
108
|
+
def collapse!
|
109
|
+
@children.each(&:collapse!)
|
110
|
+
end
|
111
|
+
|
112
|
+
def format
|
113
|
+
rows = @children
|
114
|
+
rows = @children[1..-1] if @skip_first_row
|
115
|
+
rows ||= []
|
116
|
+
rows = rows.collect { |row| row.format }.join("\n")
|
117
|
+
|
118
|
+
return nil if rows.length == 0
|
119
|
+
rows
|
120
|
+
end
|
121
|
+
|
122
|
+
def skip_first_row!
|
123
|
+
@skip_first_row = true
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
class Row
|
128
|
+
attr_reader :cells
|
129
|
+
|
130
|
+
include RowRecursion
|
131
|
+
|
132
|
+
def initialize
|
133
|
+
super
|
134
|
+
@cells = {}
|
135
|
+
end
|
136
|
+
|
137
|
+
# helpful for debugging
|
138
|
+
def vis(prefix="")
|
139
|
+
puts "#{prefix}row #{cells.inspect.to_s}"
|
140
|
+
children.each{|c| c.vis(prefix + " ")}
|
141
|
+
end
|
142
|
+
|
143
|
+
def collapse!
|
144
|
+
children.each(&:collapse!) # depth-first. start collapsing from the bottom and work our way up.
|
145
|
+
|
146
|
+
to_absorb = []
|
147
|
+
children.each do |group|
|
148
|
+
next unless can_absorb?(group)
|
149
|
+
to_absorb << group
|
150
|
+
end
|
151
|
+
|
152
|
+
to_absorb.each do |absorbable_group|
|
153
|
+
absorbable_row = absorbable_group.children.shift
|
154
|
+
@cells.merge!(absorbable_row.cells)
|
155
|
+
|
156
|
+
i = children.index(absorbable_group)
|
157
|
+
children.delete(absorbable_group) if absorbable_group.children.empty?
|
158
|
+
insert_children(i, absorbable_row.children) if absorbable_row.children.any?
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def set_cell_values(values_hash)
|
163
|
+
values_hash.each do |k, v|
|
164
|
+
@cells[k.to_s] = v
|
165
|
+
end
|
166
|
+
self
|
167
|
+
end
|
168
|
+
|
169
|
+
def format
|
170
|
+
column_names = columns.collect(&:name)
|
171
|
+
|
172
|
+
output = [column_names.collect { |name| apply_formatters(name, @cells[name]) }.join(" | ")]
|
173
|
+
output.concat @children.collect { |g| g.format }
|
174
|
+
|
175
|
+
output.join("\n")
|
176
|
+
end
|
177
|
+
|
178
|
+
def absorb_children(column_names, rollup)
|
179
|
+
@children.each do |group|
|
180
|
+
next unless can_absorb?(group)
|
181
|
+
group.skip_first_row!
|
182
|
+
|
183
|
+
column_names.collect do |name|
|
184
|
+
next unless group.children and group.children.length > 0
|
185
|
+
value = group.children.first.cells[name]
|
186
|
+
rollup[name] = value if value
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def raw_column_data(column_name)
|
192
|
+
output = [@cells[column_name.to_s]]
|
193
|
+
output << @children.collect { |g| g.raw_column_data(column_name) }
|
194
|
+
output.flatten
|
195
|
+
end
|
196
|
+
|
197
|
+
def raw_column_names
|
198
|
+
output = [@cells.keys]
|
199
|
+
output << @children.collect { |g| g.raw_column_names }
|
200
|
+
output.flatten.uniq
|
201
|
+
end
|
202
|
+
|
203
|
+
def apply_formatters(column_name, value)
|
204
|
+
column_name = column_name.to_s
|
205
|
+
return value unless column_for(column_name)
|
206
|
+
|
207
|
+
column = column_for(column_name)
|
208
|
+
formatters = column.formatters || []
|
209
|
+
|
210
|
+
formatters << TimeFormatter.new(column.time_format)
|
211
|
+
formatters << NoNewlineFormatter.new
|
212
|
+
formatters << FixedWidthFormatter.new(column_for(column_name).width)
|
213
|
+
|
214
|
+
# successively apply the formatters for a column
|
215
|
+
formatters.inject(value) do |value, formatter|
|
216
|
+
formatter.format(value)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
def can_absorb?(group)
|
221
|
+
return true if group.child_count == 1
|
222
|
+
|
223
|
+
return false if @already_absorbed_a_multigroup
|
224
|
+
@already_absorbed_a_multigroup = true # only call this method once
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
data/lib/table_print.rb
CHANGED
@@ -1,405 +1,49 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
# bugs
|
11
|
-
#
|
12
|
-
# handle multibyte (see https://skitch.com/arches/r3cbg/multibyte-bug)
|
1
|
+
require 'column'
|
2
|
+
require 'config_resolver'
|
3
|
+
require 'config'
|
4
|
+
require 'fingerprinter'
|
5
|
+
require 'formatter'
|
6
|
+
require 'hash_extensions'
|
7
|
+
require 'kernel_extensions'
|
8
|
+
require 'printable'
|
9
|
+
require 'row_group'
|
13
10
|
|
14
|
-
|
11
|
+
module TablePrint
|
12
|
+
class Printer
|
15
13
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
# TODO: make options for things like column order
|
20
|
-
end
|
21
|
-
|
22
|
-
def tp(data, options = {})
|
23
|
-
# TODO: show documentation if invoked with no arguments
|
24
|
-
# TODO: use *args instead of options
|
25
|
-
|
26
|
-
self.separator = options[:separator] || " | "
|
27
|
-
|
28
|
-
stack = Array(data).compact
|
29
|
-
|
30
|
-
if stack.empty?
|
31
|
-
return "No data."
|
32
|
-
end
|
33
|
-
|
34
|
-
self.display_methods = get_display_methods(data.first, options) # these are all strings now
|
35
|
-
unless self.display_methods.length > 0
|
36
|
-
return stack.inspect.to_s
|
37
|
-
end
|
38
|
-
|
39
|
-
# make columns for all the display methods
|
40
|
-
self.columns = {}
|
41
|
-
self.display_methods.each do |m|
|
42
|
-
self.columns[m] = ColumnHelper.new(data, m, options[m] || options[m.to_sym])
|
43
|
-
end
|
44
|
-
|
45
|
-
output = [] # a list of rows. we'll join this with newlines when we're done
|
46
|
-
|
47
|
-
# column headers
|
48
|
-
row = []
|
49
|
-
self.display_methods.each do |m|
|
50
|
-
row << self.columns[m].formatted_header
|
51
|
-
end
|
52
|
-
output << row.join(self.separator)
|
53
|
-
|
54
|
-
# a row of hyphens to separate the headers from the data
|
55
|
-
output << ("-" * output.first.length)
|
56
|
-
|
57
|
-
while stack.length > 0
|
58
|
-
format_row(stack, output)
|
59
|
-
end
|
60
|
-
|
61
|
-
output.join("\n")
|
62
|
-
end
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def format_row(stack, output)
|
67
|
-
|
68
|
-
# method_chain is a dot-delimited list of methods, eg "user.blogs.url". It represents the path from the top-level
|
69
|
-
# objects to the data_obj.
|
70
|
-
data_obj, method_chain = stack.shift
|
71
|
-
|
72
|
-
# top level objects don't have a method_chain, give them one so we don't have to null-check everywhere
|
73
|
-
method_chain ||= ""
|
74
|
-
|
75
|
-
# represent method_chain strings we've seen for this row as a tree of hash keys.
|
76
|
-
# eg, if we have columns for "user.blogs.url" and "user.blogs.title", we only want to add one set of user.blogs to the stack
|
77
|
-
method_hash = {}
|
78
|
-
|
79
|
-
# if no columns in this row produce any data, we don't want to append it to the output. eg, if our only columns are
|
80
|
-
# ["id", "blogs.title"] we don't want to print a blank row for every blog we iterate over. We want to entirely skip
|
81
|
-
# printing a row for that level of the hierarchy.
|
82
|
-
found_data = false
|
83
|
-
|
84
|
-
# dive right in!
|
85
|
-
row = []
|
86
|
-
self.display_methods.each do |m|
|
87
|
-
column = self.columns[m]
|
88
|
-
|
89
|
-
# If this column happens to begin a recursion, get those objects on the stack. Pass in the stack-tracking info
|
90
|
-
# we've saved: method_chain and method_hash.
|
91
|
-
column.add_stack_objects(stack, data_obj, method_chain, method_hash)
|
92
|
-
|
93
|
-
# all rows show all cells. Even if there's no data we still have to generate an empty cell of the proper width
|
94
|
-
row << column.formatted_cell_value(data_obj, method_chain)
|
95
|
-
found_data = true unless row[-1].strip.empty?
|
96
|
-
end
|
97
|
-
|
98
|
-
output << row.join(self.separator) if found_data
|
99
|
-
end
|
100
|
-
|
101
|
-
# Sort out the user options into a set of display methods we're going to show. This always returns strings.
|
102
|
-
def get_display_methods(data_obj, options)
|
103
|
-
# determine what methods we're going to use
|
104
|
-
|
105
|
-
# using options:
|
106
|
-
# TODO: maybe rename these a little? cascade/include are somewhat mixed with respect to rails lexicon
|
107
|
-
# :except - use the default set of methods but NOT the ones passed here
|
108
|
-
# :include - use the default set of methods AND the ones passed here
|
109
|
-
# :only - discard the default set of methods in favor of this list
|
110
|
-
# :cascade - show all methods in child objects
|
111
|
-
|
112
|
-
if options.has_key? :only
|
113
|
-
display_methods = Array(options[:only]).map { |m| m.to_s }
|
114
|
-
return display_methods if display_methods.length > 0
|
115
|
-
else
|
116
|
-
display_methods = get_default_display_methods(data_obj) # start with what we can deduce
|
117
|
-
display_methods.concat(Array(options[:include])).map! { |m| m.to_s } # add the includes
|
118
|
-
display_methods = (display_methods - Array(options[:except]).map! { |m| m.to_s }) # remove the excepts
|
119
|
-
end
|
120
|
-
|
121
|
-
display_methods.uniq.compact
|
122
|
-
end
|
123
|
-
|
124
|
-
# Sniff the data class for non-standard methods to use as a baseline for display
|
125
|
-
def get_default_display_methods(data_obj)
|
126
|
-
# ActiveRecord
|
127
|
-
return data_obj.class.columns.collect { |c| c.name } if defined?(ActiveRecord) and data_obj.is_a? ActiveRecord::Base
|
128
|
-
|
129
|
-
methods = []
|
130
|
-
data_obj.methods.each do |method_name|
|
131
|
-
method = data_obj.method(method_name)
|
132
|
-
|
133
|
-
if method.owner == data_obj.class
|
134
|
-
if method.arity == 0 #
|
135
|
-
methods << method_name.to_s
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
139
|
-
|
140
|
-
methods.delete_if { |m| m[-1].chr == "=" } # don't use assignment methods
|
141
|
-
methods.delete_if { |m| m[-1].chr == "!" } # don't use dangerous methods
|
142
|
-
methods.map! { |m| m.to_s } # make any symbols into strings
|
143
|
-
methods
|
144
|
-
end
|
145
|
-
|
146
|
-
class ColumnHelper
|
147
|
-
attr_accessor :field_length, :max_field_length, :method, :name, :options
|
148
|
-
|
149
|
-
def initialize(data, method, options = {})
|
150
|
-
self.method = method
|
151
|
-
self.options = options || {} # could have been passed an explicit nil
|
152
|
-
|
153
|
-
self.name = self.options[:name] || method.gsub("_", " ").gsub(".", " > ")
|
154
|
-
|
155
|
-
self.max_field_length = self.options[:max_field_length] || 30
|
156
|
-
self.max_field_length = [self.max_field_length, 1].max # numbers less than one are meaningless
|
157
|
-
|
158
|
-
initialize_field_length(data)
|
159
|
-
end
|
160
|
-
|
161
|
-
def formatted_header
|
162
|
-
"%-#{self.field_length}s" % truncate(self.name.upcase)
|
163
|
-
end
|
164
|
-
|
165
|
-
def formatted_cell_value(data_obj, method_chain)
|
166
|
-
cell_value = ""
|
167
|
-
|
168
|
-
# top-level objects don't have method chain. Need to check explicitly whether our method is top-level, otherwise
|
169
|
-
# if the last method in our chain matches a top-level method we could accidentally print its data in our column.
|
170
|
-
#
|
171
|
-
# The method chain is what we've been building up as we were "recursing" through previous objects. You could think of
|
172
|
-
# it as a prefix for this row. Eg, we could be looping through the columns with a method_chain of "locker.assets",
|
173
|
-
# indicating that we've recursed down from user to locker and are now interested in printing assets. So
|
174
|
-
#
|
175
|
-
unless method_chain == "" and self.method.include? "."
|
176
|
-
our_method_chain = self.method.split(".")
|
177
|
-
our_method = our_method_chain.pop
|
178
|
-
|
179
|
-
# check whether the method_chain fully qualifies the path to this particular object. If this is the bottom level
|
180
|
-
# of object in the tree, and the method_chain matches all the way down, then it's finally time to print this cell.
|
181
|
-
if method_chain == our_method_chain.join(".")
|
182
|
-
if data_obj.respond_to? our_method
|
183
|
-
cell_value = data_obj.send(our_method).to_s.gsub("\n", " ")
|
184
|
-
end
|
185
|
-
end
|
186
|
-
end
|
187
|
-
"%-#{self.field_length}s" % truncate(cell_value.to_s)
|
14
|
+
def self.table_print(data, options={})
|
15
|
+
p = new(data, options)
|
16
|
+
p.table_print
|
188
17
|
end
|
189
18
|
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
# TODO: probably a less awkward string method to do this
|
196
|
-
# current_method is the method we're going to call on our data_obj. Say our column is "locker.assets.url" and
|
197
|
-
# our chain is "locker", current_method would be "assets"
|
198
|
-
current_method = get_current_method(method_chain)
|
199
|
-
|
200
|
-
new_stack_objects = []
|
201
|
-
if current_method != "" and data_obj.respond_to? current_method
|
202
|
-
new_stack_objects = data_obj.send(current_method)
|
203
|
-
end
|
204
|
-
|
205
|
-
# Now that we've seen "locker.assets", no need to add it to the stack again for this row! Save it off in method_hash
|
206
|
-
# so when we hit "locker.assets.caption" we won't add the same assets again.
|
207
|
-
new_method_chain = method_chain == "" ? current_method : "#{method_chain}.#{current_method}"
|
208
|
-
method_hash[new_method_chain] = {}
|
209
|
-
|
210
|
-
# TODO: probably a cool array method to do this
|
211
|
-
# finally - update the stack with the object(s) we found
|
212
|
-
Array(new_stack_objects).reverse_each do |stack_obj|
|
213
|
-
stack.unshift [stack_obj, new_method_chain]
|
214
|
-
end
|
215
|
-
end
|
216
|
-
|
217
|
-
def add_to_stack?(method_chain, method_hash = {})
|
218
|
-
|
219
|
-
# Check whether we're involved in this row. method_chain lets us know the path we took to find the current set of
|
220
|
-
# data objects. If our method doesn't act upon those objects, bail.
|
221
|
-
# eg, if these objects are the result of calling "locker.assets" on top-level user objects, but our method is "blogs.title",
|
222
|
-
# all we're going to be doing on this row is pushing out empty cells.
|
223
|
-
return unless self.method.start_with? method_chain
|
224
|
-
|
225
|
-
# check whether another column has already added our objects. if we hit "locker.assets.url" already and we're
|
226
|
-
# "locker.assets.caption", the assets are already on the stack. Don't want to add them again.
|
227
|
-
new_method_chain = method_chain == "" ? get_current_method(method_chain) : "#{method_chain}.#{get_current_method(method_chain)}"
|
228
|
-
return if method_hash.has_key? new_method_chain
|
229
|
-
|
230
|
-
# OK! this column relates to the data object and hasn't been beaten to the punch. But do we have more levels to recurse, or
|
231
|
-
# is this object on the bottom rung and just needs formatting?
|
232
|
-
|
233
|
-
# if this is the top level, all we need to do is check for a dot, indicating a chain of methods
|
234
|
-
if method_chain == ""
|
235
|
-
return method.include? "."
|
236
|
-
end
|
237
|
-
|
238
|
-
# if this isn't the top level, subtract out the part of the chain we've already called before we check for further chaining
|
239
|
-
test_method = String.new(method[method_chain.length, method.length])
|
240
|
-
test_method = test_method[1, test_method.length] if test_method.start_with? "."
|
241
|
-
|
242
|
-
test_method.include? "."
|
19
|
+
def initialize(data, options={})
|
20
|
+
@data = [data].flatten.compact
|
21
|
+
@options = options
|
22
|
+
@columns = nil
|
243
23
|
end
|
244
24
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
if copy.length > self.field_length
|
251
|
-
copy = copy[0..self.field_length-1]
|
252
|
-
copy[-3..-1] = "..." unless self.field_length <= 3 # don't use ellipses when the string is tiny
|
25
|
+
def table_print
|
26
|
+
return "No data." if @data.empty?
|
27
|
+
group = TablePrint::RowGroup.new
|
28
|
+
columns.each do |c|
|
29
|
+
group.set_column(c.name, c)
|
253
30
|
end
|
254
|
-
copy
|
255
|
-
end
|
256
|
-
|
257
|
-
# determine how wide this column is going to be
|
258
|
-
def initialize_field_length(data)
|
259
|
-
# skip all this nonsense if we've been explicitly told what to do
|
260
|
-
if self.options[:field_length] and self.options[:field_length] > 0
|
261
|
-
self.field_length = self.options[:field_length]
|
262
|
-
else
|
263
|
-
self.field_length = self.name.length # it has to at least be long enough for the column header!
|
264
31
|
|
265
|
-
|
32
|
+
@data.each do |data|
|
33
|
+
group.add_children(Fingerprinter.new.lift(columns, data))
|
266
34
|
end
|
267
35
|
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
# recurse through the data set using the method chain to find the longest field (or until time's up)
|
272
|
-
def find_data_length(data, method, start)
|
273
|
-
return if (Time.now - start) > 2
|
274
|
-
return if method.nil?
|
275
|
-
return if data.nil?
|
276
|
-
return if self.field_length >= self.max_field_length
|
277
|
-
|
278
|
-
Array(data).each do |data_obj|
|
279
|
-
next_method = method.split(".").first
|
36
|
+
group.collapse!
|
37
|
+
return "No data." if group.columns.empty?
|
280
38
|
|
281
|
-
|
282
|
-
|
283
|
-
if next_method == method # done!
|
284
|
-
self.field_length = [self.field_length, data_obj.send(next_method).to_s.length].max
|
285
|
-
else # keep going
|
286
|
-
find_data_length(data_obj.send(next_method), method[(next_method.length + 1)..-1], start)
|
287
|
-
end
|
288
|
-
end
|
39
|
+
[group.header, group.horizontal_separator, group.format].join("\n")
|
289
40
|
end
|
290
41
|
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
current_method.split(".").detect { |m| m != "" }
|
297
|
-
end
|
42
|
+
def columns
|
43
|
+
return @columns if @columns
|
44
|
+
defaults = TablePrint::Printable.default_display_methods(@data.first)
|
45
|
+
c = TablePrint::ConfigResolver.new(@data.first.class, defaults, @options)
|
46
|
+
@columns = c.columns
|
298
47
|
end
|
299
48
|
end
|
300
|
-
|
301
|
-
end
|
302
|
-
|
303
|
-
module Kernel
|
304
|
-
def tp(data, options = {})
|
305
|
-
start = Time.now
|
306
|
-
table_print = TablePrint.new
|
307
|
-
puts table_print.tp(Array(data), options)
|
308
|
-
Time.now - start # we have to return *something*, might as well be execution time.
|
309
|
-
end
|
310
|
-
|
311
|
-
module_function :tp
|
312
49
|
end
|
313
|
-
|
314
|
-
## Some nested classes to make development easier! Make sure you don't commit these uncommented.
|
315
|
-
#
|
316
|
-
#class TestClass
|
317
|
-
# attr_accessor :title, :name, :blogs, :locker
|
318
|
-
#
|
319
|
-
# def initialize(title, name, blogs, locker)
|
320
|
-
# self.title = title
|
321
|
-
# self.name = name
|
322
|
-
# self.blogs = blogs
|
323
|
-
# self.locker = locker
|
324
|
-
# end
|
325
|
-
#end
|
326
|
-
#
|
327
|
-
#class Blog
|
328
|
-
# attr_accessor :title, :summary
|
329
|
-
#
|
330
|
-
# def initialize(title, summary)
|
331
|
-
# self.title = title
|
332
|
-
# self.summary = summary
|
333
|
-
# end
|
334
|
-
#end
|
335
|
-
#
|
336
|
-
#class Locker
|
337
|
-
# attr_accessor :assets
|
338
|
-
#
|
339
|
-
# def initialize(assets)
|
340
|
-
# self.assets = assets
|
341
|
-
# end
|
342
|
-
#end
|
343
|
-
#
|
344
|
-
#class Asset
|
345
|
-
# attr_accessor :url, :caption
|
346
|
-
#
|
347
|
-
# def initialize(url, caption)
|
348
|
-
# self.url = url
|
349
|
-
# self.caption = caption
|
350
|
-
# end
|
351
|
-
#end
|
352
|
-
#
|
353
|
-
#stack = [
|
354
|
-
#
|
355
|
-
# TestClass.new("one title", "one name", [
|
356
|
-
# Blog.new("one blog title1", "one blog sum1"),
|
357
|
-
# Blog.new("one blog title2", "one blog sum2"),
|
358
|
-
# Blog.new("one blog title3", "one blog sum3"),
|
359
|
-
# ],
|
360
|
-
# Locker.new([
|
361
|
-
# Asset.new("one asset url1", "one asset cap1"),
|
362
|
-
# Asset.new("one asset url2", "one asset cap2"),
|
363
|
-
# Asset.new("one asset url3", "one asset cap3"),
|
364
|
-
# ])
|
365
|
-
# ),
|
366
|
-
# TestClass.new("two title", "two name", [
|
367
|
-
# Blog.new("two blog title1", "two blog sum1"),
|
368
|
-
# Blog.new("two blog title2", "two blog sum2"),
|
369
|
-
# Blog.new("two blog title3", "two blog sum3"),
|
370
|
-
# ],
|
371
|
-
# Locker.new([
|
372
|
-
# Asset.new("two asset url1", "two asset cap1"),
|
373
|
-
# Asset.new("two asset url2", "two asset cap2"),
|
374
|
-
# Asset.new("two asset url3", "two asset cap3"),
|
375
|
-
# ])
|
376
|
-
# ),
|
377
|
-
# TestClass.new("three title", "three name", [
|
378
|
-
# Blog.new("three blog title1", "three blog sum1"),
|
379
|
-
# Blog.new("three blog title2", "three blog sum2"),
|
380
|
-
# Blog.new("three blog title3", "three blog sum3"),
|
381
|
-
# ],
|
382
|
-
# Locker.new([
|
383
|
-
# Asset.new("three asset url1", "three asset cap1"),
|
384
|
-
# Asset.new("three asset url2", "three asset cap2"),
|
385
|
-
# Asset.new("three asset url3", "three asset cap3"),
|
386
|
-
# ])
|
387
|
-
# ),
|
388
|
-
# TestClass.new("four title", "four name", [
|
389
|
-
# Blog.new("four blog title1", "four blog sum1"),
|
390
|
-
# Blog.new("four blog title2", "four blog sum2"),
|
391
|
-
# Blog.new("four blog title3", "four blog sum3"),
|
392
|
-
# ],
|
393
|
-
# Locker.new([
|
394
|
-
# Asset.new("four asset url1", "four asset cap1"),
|
395
|
-
# Asset.new("four asset url2", "four asset cap2"),
|
396
|
-
# Asset.new("four asset url3", "four asset cap3"),
|
397
|
-
# ])
|
398
|
-
# ),
|
399
|
-
#]
|
400
|
-
|
401
|
-
#tp stack, :include => ["blogs.title", "blogs.summary", "locker.assets.url", "locker.assets.caption"]
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|