table_print 0.2.3 → 1.0.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
|