table_print 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +29 -3
- data/VERSION +1 -1
- data/lib/table_print.rb +311 -124
- data/table_print.gemspec +2 -2
- data/test/helper.rb +38 -0
- data/test/test_column.rb +274 -30
- data/test/test_table_print.rb +4 -15
- metadata +4 -4
data/README.rdoc
CHANGED
@@ -25,7 +25,7 @@ it assumes the objects in your array all respond to the same methods.
|
|
25
25
|
$ rails c
|
26
26
|
> tp array_of_objects, options = {}
|
27
27
|
|
28
|
-
|
28
|
+
You should see something like this:
|
29
29
|
|
30
30
|
NAME | SUMMARY | TITLE
|
31
31
|
-----------------------------------------------------------------------
|
@@ -44,7 +44,33 @@ on your model.
|
|
44
44
|
> tp User.limit(30), {:only => [:address, :city, :state, :zip]}
|
45
45
|
|
46
46
|
If you're not using ActiveRecord, the TablePrint default is to show all the methods on your object. Thus, the <b>:include</b>
|
47
|
-
option is useless at the moment. You are still able to use <b>:only</b> and <b>:
|
47
|
+
option is useless at the moment. You are still able to use <b>:only</b> and <b>:except</b>.
|
48
|
+
|
49
|
+
You can reference nested objects with the method chain required to reach them. Say you had some users who wrote books, and those
|
50
|
+
books had photos.
|
51
|
+
|
52
|
+
> tp array_of_objects, :only => ["name", "books.title", "books.photos.caption"]
|
53
|
+
|
54
|
+
NAME | BOOKS > TITLE | BOOKS > PHOTOS > CAPTION
|
55
|
+
-------------------------------------------------------------------------
|
56
|
+
Michael Connelly | |
|
57
|
+
| The Fifth Witness |
|
58
|
+
| | Susan was running, fast, away...
|
59
|
+
| | Along came a spider.
|
60
|
+
| Malcolm X |
|
61
|
+
| Bossypants |
|
62
|
+
| | Yes! Yes! A thousand times ye...
|
63
|
+
| | Don't see many like you aroun...
|
64
|
+
Carrot Top | |
|
65
|
+
Milton Greene | |
|
66
|
+
| How I Learned |
|
67
|
+
| | Once upon a time, I was a sma...
|
68
|
+
| | Lemons are yellow, limes are ...
|
69
|
+
| | Never as a woman her age. I l...
|
70
|
+
| Desperados |
|
71
|
+
| | Avast.
|
72
|
+
| | Giraffes lived a peaceful exi...
|
73
|
+
|
48
74
|
|
49
75
|
=== Column options
|
50
76
|
|
@@ -61,7 +87,7 @@ Columns have other options, including:
|
|
61
87
|
will ensure that the column is as skinny as possible, but never above the number you provide.
|
62
88
|
|
63
89
|
<b>field_length:</b> Useful for very large data sets, this option will explicitly set the column width regardless of the data
|
64
|
-
it contains.
|
90
|
+
it contains. The max_field_length option takes precedence over field_length - ie, field_length can't be longer than max_field_length.
|
65
91
|
|
66
92
|
|
67
93
|
== Contributing to table_print
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.0
|
data/lib/table_print.rb
CHANGED
@@ -6,6 +6,7 @@
|
|
6
6
|
# on-the-fly column definitions (pass a proc as an include, eg 'tp User.all, :include => {:column_name => "Zodiac", :display_method => lambda {|u| find_zodiac_sign(u.birthday)}}')
|
7
7
|
# allow user to pass ActiveRelation instead of a data array? That could open up so many options!
|
8
8
|
# a :short_booleans method could save a little space (replace true/false with T/F or 1/0)
|
9
|
+
# we could do some really smart stuff with polymorphic relationships, eg reusing photo column for blogs AND books!
|
9
10
|
#
|
10
11
|
# bugs
|
11
12
|
#
|
@@ -13,6 +14,7 @@
|
|
13
14
|
|
14
15
|
class TablePrint
|
15
16
|
|
17
|
+
# We need this set of built-in types when we determine the default display methods for a given object
|
16
18
|
OBJECT_CLASSES = [String, Bignum, Regexp, ThreadError, Numeric, SystemStackError, IndexError,
|
17
19
|
SecurityError, SizedQueue, IO, Range, Object, Exception, NoMethodError, TypeError, Integer, Dir,
|
18
20
|
ZeroDivisionError, Kernel, RegexpError, SystemExit, NotImplementedError, Hash,
|
@@ -23,56 +25,49 @@ class TablePrint
|
|
23
25
|
StandardError, EOFError, LoadError, NameError, NilClass, TrueClass, MatchingData,
|
24
26
|
LocalJumpError, Binding, SignalException, SystemCallError, File, ScriptError, Module, Symbol]
|
25
27
|
|
26
|
-
|
27
|
-
|
28
|
-
# TODO: make options for things like column order
|
28
|
+
attr_accessor :columns, :display_methods, :separator
|
29
|
+
|
29
30
|
def initialize(options = {})
|
31
|
+
# TODO: make options for things like column order
|
30
32
|
end
|
31
33
|
|
32
|
-
# TODO: show documentation if invoked with no arguments
|
33
|
-
# TODO: use *args instead of options
|
34
34
|
def tp(data, options = {})
|
35
|
-
|
35
|
+
# TODO: show documentation if invoked with no arguments
|
36
|
+
# TODO: use *args instead of options
|
36
37
|
|
37
|
-
|
38
|
-
if data.empty?
|
39
|
-
return "No data."
|
40
|
-
end
|
38
|
+
self.separator = options[:separator] || " | "
|
41
39
|
|
42
|
-
|
40
|
+
stack = wrap(data).compact
|
43
41
|
|
44
|
-
|
45
|
-
|
46
|
-
return data.inspect.to_s
|
42
|
+
if stack.empty?
|
43
|
+
return "No data."
|
47
44
|
end
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
46
|
+
self.display_methods = get_display_methods(data.first, options) # these are all strings now
|
47
|
+
unless self.display_methods.length > 0
|
48
|
+
return stack.inspect.to_s
|
49
|
+
end
|
53
50
|
|
54
51
|
# make columns for all the display methods
|
55
|
-
columns =
|
52
|
+
self.columns = {}
|
53
|
+
self.display_methods.each do |m|
|
54
|
+
self.columns[m] = ColumnHelper.new(data, m, options[m] || options[m.to_sym])
|
55
|
+
end
|
56
56
|
|
57
57
|
output = [] # a list of rows. we'll join this with newlines when we're done
|
58
58
|
|
59
59
|
# column headers
|
60
60
|
row = []
|
61
|
-
|
62
|
-
row <<
|
61
|
+
self.display_methods.each do |m|
|
62
|
+
row << self.columns[m].formatted_header
|
63
63
|
end
|
64
|
-
output << row.join(separator)
|
64
|
+
output << row.join(self.separator)
|
65
65
|
|
66
66
|
# a row of hyphens to separate the headers from the data
|
67
|
-
output << ("-" *
|
67
|
+
output << ("-" * output.first.length)
|
68
68
|
|
69
|
-
|
70
|
-
|
71
|
-
row = []
|
72
|
-
columns.each do |column|
|
73
|
-
row << column.formatted_field_value(data_obj)
|
74
|
-
end
|
75
|
-
output << row.join(separator)
|
69
|
+
while stack.length > 0
|
70
|
+
format_row(stack, output)
|
76
71
|
end
|
77
72
|
|
78
73
|
output.join("\n")
|
@@ -80,6 +75,42 @@ class TablePrint
|
|
80
75
|
|
81
76
|
private
|
82
77
|
|
78
|
+
def format_row(stack, output)
|
79
|
+
|
80
|
+
# method_chain is a dot-delimited list of methods, eg "user.blogs.url". It represents the path from the top-level
|
81
|
+
# objects to the data_obj.
|
82
|
+
data_obj, method_chain = stack.shift
|
83
|
+
|
84
|
+
# top level objects don't have a method_chain, give them one so we don't have to null-check everywhere
|
85
|
+
method_chain ||= ""
|
86
|
+
|
87
|
+
# represent method_chain strings we've seen for this row as a tree of hash keys.
|
88
|
+
# 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
|
89
|
+
method_hash = {}
|
90
|
+
|
91
|
+
# 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
|
92
|
+
# ["id", "blogs.title"] we don't want to print a blank row for every blog we iterate over. We want to entirely skip
|
93
|
+
# printing a row for that level of the hierarchy.
|
94
|
+
found_data = false
|
95
|
+
|
96
|
+
# dive right in!
|
97
|
+
row = []
|
98
|
+
self.display_methods.each do |m|
|
99
|
+
column = self.columns[m]
|
100
|
+
|
101
|
+
# If this column happens to begin a recursion, get those objects on the stack. Pass in the stack-tracking info
|
102
|
+
# we've saved: method_chain and method_hash.
|
103
|
+
column.add_stack_objects(stack, data_obj, method_chain, method_hash)
|
104
|
+
|
105
|
+
# all rows show all cells. Even if there's no data we still have to generate an empty cell of the proper width
|
106
|
+
row << column.formatted_cell_value(data_obj, method_chain)
|
107
|
+
found_data = true unless row[-1].strip.empty?
|
108
|
+
end
|
109
|
+
|
110
|
+
output << row.join(self.separator) if found_data
|
111
|
+
end
|
112
|
+
|
113
|
+
# Sort out the user options into a set of display methods we're going to show. This always returns strings.
|
83
114
|
def get_display_methods(data_obj, options)
|
84
115
|
# determine what methods we're going to use
|
85
116
|
|
@@ -88,60 +119,30 @@ class TablePrint
|
|
88
119
|
# :except - use the default set of methods but NOT the ones passed here
|
89
120
|
# :include - use the default set of methods AND the ones passed here
|
90
121
|
# :only - discard the default set of methods in favor of this list
|
91
|
-
# :cascade -
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
# tp User.limit(30) # default to using AR columns
|
96
|
-
# tp User.limit(30) :except => :display_name
|
97
|
-
# tp User.limit(30) :except => [:display_name, :created_at]
|
98
|
-
# tp User.limit(30) :except => [:display_name, :timestamps] # a rails convention - but this could just be a type-based filter instead of method-based?
|
99
|
-
# tp User.limit(30) :include => :status # not an AR column
|
100
|
-
# tp User.limit(30) :except => :display_name
|
101
|
-
# tp User.limit(30) :except => :display_name
|
102
|
-
# tp User.limit(30) :except => :display_name
|
103
|
-
# tp User.limit(30) :except => :display_name
|
104
|
-
#
|
105
|
-
# tp User.include(:blogs).limit(30) :cascade => :blog
|
106
|
-
# tp User.limit(30) :include => "blog.title" # use dot notation to traverse children
|
107
|
-
# TODO: how to handle multiple children? eg, a user has fifteen blogs
|
108
|
-
#
|
109
|
-
# tp [myClassInstance1, ...] # default to using non-base methods
|
110
|
-
# tp [myClassInstance1, ...] :except => :blah
|
111
|
-
# tp [myClassInstance1, ...] :except => [:blah, :blow]
|
112
|
-
# tp [myClassInstance1, ...] :include => :blah
|
113
|
-
# tp [myClassInstance1, ...] :include => [:blah, :blow]
|
114
|
-
# tp [myClassInstance1, ...] :include => :blah, :except => :blow
|
115
|
-
# tp [myClassInstance1, ...] :only => [:one, :two, :three]
|
116
|
-
|
117
|
-
if options.has_key? :only or options.has_key? "only"
|
118
|
-
display_methods = clean_display_methods(data_obj, wrap(options[:only]))
|
122
|
+
# :cascade - show all methods in child objects
|
123
|
+
|
124
|
+
if options.has_key? :only
|
125
|
+
display_methods = wrap(options[:only]).map { |m| m.to_s }
|
119
126
|
return display_methods if display_methods.length > 0
|
127
|
+
else
|
128
|
+
display_methods = get_default_display_methods(data_obj) # start with what we can deduce
|
129
|
+
display_methods.concat(wrap(options[:include])).map! { |m| m.to_s } # add the includes
|
130
|
+
display_methods = (display_methods - wrap(options[:except]).map! { |m| m.to_s }) # remove the excepts
|
120
131
|
end
|
121
132
|
|
122
|
-
|
123
|
-
methods_to_include = clean_display_methods(data_obj, wrap(options[:include]))
|
124
|
-
methods_to_except = clean_display_methods(data_obj, wrap(options[:except]))
|
125
|
-
|
126
|
-
# add/remove the includes/excludes from the defaults
|
127
|
-
display_methods = get_default_display_methods(data_obj)
|
128
|
-
display_methods.concat(methods_to_include).uniq!
|
129
|
-
display_methods - methods_to_except
|
133
|
+
display_methods.uniq.compact
|
130
134
|
end
|
131
135
|
|
136
|
+
# Sniff the data class for non-standard methods to use as a baseline for display
|
132
137
|
def get_default_display_methods(data_obj)
|
133
138
|
# ActiveRecord
|
134
139
|
return data_obj.class.columns.collect { |c| c.name } if defined?(ActiveRecord) and data_obj.is_a? ActiveRecord::Base
|
135
140
|
|
136
|
-
# base types
|
137
|
-
# TODO: fill out this list. any way to get this programatically? do we actually want to filter out all base ruby types? important question for custom classes inheriting from base types
|
138
|
-
return [] if [Float, Fixnum, String, Numeric, Array, Hash].include? data_obj.class
|
139
|
-
|
140
141
|
# custom class
|
141
142
|
methods = data_obj.class.instance_methods
|
142
143
|
OBJECT_CLASSES.each do |oclass|
|
143
144
|
if data_obj.is_a? oclass
|
144
|
-
methods = methods - oclass.instance_methods
|
145
|
+
methods = methods - oclass.instance_methods # we're only interested in custom methods, not ruby core methods
|
145
146
|
end
|
146
147
|
end
|
147
148
|
|
@@ -150,19 +151,9 @@ class TablePrint
|
|
150
151
|
methods
|
151
152
|
end
|
152
153
|
|
153
|
-
def clean_display_methods(data_obj, display_methods)
|
154
|
-
# TODO: this should probably be inside Column
|
155
|
-
clean_methods = []
|
156
|
-
display_methods.each do |m|
|
157
|
-
next if m.nil?
|
158
|
-
next if m == ""
|
159
|
-
next unless data_obj.respond_to? m
|
160
|
-
clean_methods << m.to_s
|
161
|
-
end
|
162
|
-
clean_methods.uniq
|
163
|
-
end
|
164
|
-
|
165
154
|
# borrowed from rails
|
155
|
+
# turn objects into an array
|
156
|
+
# TODO: this method is duped, put it someplace everyone can see it
|
166
157
|
def wrap(object)
|
167
158
|
if object.nil?
|
168
159
|
[]
|
@@ -173,69 +164,174 @@ class TablePrint
|
|
173
164
|
end
|
174
165
|
end
|
175
166
|
|
176
|
-
class
|
177
|
-
attr_accessor :
|
167
|
+
class ColumnHelper
|
168
|
+
attr_accessor :field_length, :max_field_length, :method, :name, :options
|
169
|
+
|
170
|
+
# method is a string
|
171
|
+
def initialize(data, method, options = {})
|
172
|
+
self.method = method
|
173
|
+
self.options = options || {} # could have been passed an explicit nil
|
174
|
+
|
175
|
+
self.name = self.options[:name] || method.gsub("_", " ").gsub(".", " > ")
|
178
176
|
|
179
|
-
def initialize(data, display_method, options = {})
|
180
|
-
self.options = options || {} # could have been passed an explicit nil
|
181
|
-
self.display_method = display_method
|
182
|
-
self.name = self.options[:name] || display_method.gsub("_", " ")
|
183
177
|
self.max_field_length = self.options[:max_field_length] || 30
|
184
|
-
self.max_field_length = [self.max_field_length, 1].max
|
178
|
+
self.max_field_length = [self.max_field_length, 1].max # numbers less than one are meaningless
|
185
179
|
|
186
|
-
|
187
|
-
self.initialize_field_length(data)
|
180
|
+
initialize_field_length(data)
|
188
181
|
end
|
189
182
|
|
190
183
|
def formatted_header
|
191
184
|
"%-#{self.field_length}s" % truncate(self.name.upcase)
|
192
185
|
end
|
193
186
|
|
194
|
-
def
|
195
|
-
"
|
187
|
+
def formatted_cell_value(data_obj, method_chain)
|
188
|
+
cell_value = ""
|
189
|
+
|
190
|
+
# top-level objects don't have method chain. Need to check explicitly whether our method is top-level, otherwise
|
191
|
+
# if the last method in our chain matches a top-level method we could accidentally print its data in our column.
|
192
|
+
#
|
193
|
+
# The method chain is what we've been building up as we were "recursing" through previous objects. You could think of
|
194
|
+
# it as a prefix for this row. Eg, we could be looping through the columns with a method_chain of "locker.assets",
|
195
|
+
# indicating that we've recursed down from user to locker and are now interested in printing assets. So
|
196
|
+
#
|
197
|
+
unless method_chain == "" and self.method.include? "."
|
198
|
+
our_method_chain = self.method.split(".")
|
199
|
+
our_method = our_method_chain.pop
|
200
|
+
|
201
|
+
# check whether the method_chain fully qualifies the path to this particular object. If this is the bottom level
|
202
|
+
# of object in the tree, and the method_chain matches all the way down, then it's finally time to print this cell.
|
203
|
+
if method_chain == our_method_chain.join(".")
|
204
|
+
if data_obj.respond_to? our_method
|
205
|
+
cell_value = data_obj.send(our_method)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|
209
|
+
"%-#{self.field_length}s" % truncate(cell_value.to_s)
|
196
210
|
end
|
197
211
|
|
198
|
-
|
199
|
-
|
200
|
-
if self.options[:field_length] and self.options[:field_length] > 0
|
201
|
-
length = self.options[:field_length]
|
202
|
-
else
|
203
|
-
length = self.name.length # it has to at least be long enough for the column header!
|
204
|
-
|
205
|
-
start = Time.now
|
206
|
-
data.each do |data_obj|
|
207
|
-
next if data_obj.nil?
|
208
|
-
|
209
|
-
# fixed-width fields don't require the full loop
|
210
|
-
case data_obj.send(self.display_method)
|
211
|
-
when Time
|
212
|
-
length = data_obj.send(self.display_method).to_s.length
|
213
|
-
break
|
214
|
-
when TrueClass, FalseClass
|
215
|
-
length = [5, length].max
|
216
|
-
break
|
217
|
-
end
|
212
|
+
# Determine if we need to add some stuff to the stack. If so, put it on top and update the tracking objects.
|
213
|
+
def add_stack_objects(stack, data_obj, method_chain, method_hash)
|
218
214
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
215
|
+
return unless self.add_to_stack?(method_chain, method_hash)
|
216
|
+
|
217
|
+
# TODO: probably a less awkward string method to do this
|
218
|
+
# current_method is the method we're going to call on our data_obj. Say our column is "locker.assets.url" and
|
219
|
+
# our chain is "locker", current_method would be "assets"
|
220
|
+
current_method = get_current_method(method_chain)
|
221
|
+
|
222
|
+
new_stack_objects = []
|
223
|
+
if current_method != "" and data_obj.respond_to? current_method
|
224
|
+
new_stack_objects = data_obj.send(current_method)
|
225
|
+
end
|
226
|
+
|
227
|
+
# 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
|
228
|
+
# so when we hit "locker.assets.caption" we won't add the same assets again.
|
229
|
+
new_method_chain = method_chain == "" ? current_method : "#{method_chain}.#{current_method}"
|
230
|
+
method_hash[new_method_chain] = {}
|
231
|
+
|
232
|
+
# TODO: probably a cool array method to do this
|
233
|
+
# finally - update the stack with the object(s) we found
|
234
|
+
wrap(new_stack_objects).reverse_each do |stack_obj|
|
235
|
+
stack.unshift [stack_obj, new_method_chain]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def add_to_stack?(method_chain, method_hash = {})
|
240
|
+
|
241
|
+
# Check whether we're involved in this row. method_chain lets us know the path we took to find the current set of
|
242
|
+
# data objects. If our method doesn't act upon those objects, bail.
|
243
|
+
# eg, if these objects are the result of calling "locker.assets" on top-level user objects, but our method is "blogs.title",
|
244
|
+
# all we're going to be doing on this row is pushing out empty cells.
|
245
|
+
return unless self.method.start_with? method_chain
|
246
|
+
|
247
|
+
# check whether another column has already added our objects. if we hit "locker.assets.url" already and we're
|
248
|
+
# "locker.assets.caption", the assets are already on the stack. Don't want to add them again.
|
249
|
+
new_method_chain = method_chain == "" ? get_current_method(method_chain) : "#{method_chain}.#{get_current_method(method_chain)}"
|
250
|
+
return if method_hash.has_key? new_method_chain
|
251
|
+
|
252
|
+
# 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
|
253
|
+
# is this object on the bottom rung and just needs formatting?
|
254
|
+
|
255
|
+
# if this is the top level, all we need to do is check for a dot, indicating a chain of methods
|
256
|
+
if method_chain == ""
|
257
|
+
return method.include? "."
|
223
258
|
end
|
224
259
|
|
225
|
-
|
260
|
+
# if this isn't the top level, subtract out the part of the chain we've already called before we check for further chaining
|
261
|
+
test_method = String.new(method[method_chain.length, method.length])
|
262
|
+
test_method = test_method[1, test_method.length] if test_method.start_with? "."
|
263
|
+
|
264
|
+
test_method.include? "."
|
226
265
|
end
|
227
266
|
|
228
267
|
private
|
229
|
-
|
268
|
+
|
269
|
+
# borrowed from rails
|
270
|
+
# turn objects into an array
|
271
|
+
def wrap(object)
|
272
|
+
if object.nil?
|
273
|
+
[]
|
274
|
+
elsif object.respond_to?(:to_ary)
|
275
|
+
object.to_ary
|
276
|
+
else
|
277
|
+
[object]
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# cut off field_value based on our previously determined width
|
230
282
|
def truncate(field_value)
|
231
283
|
copy = String.new(field_value)
|
232
|
-
if copy.length > self.
|
233
|
-
copy = copy[0..self.
|
234
|
-
copy[-3..-1] = "..." unless self.
|
284
|
+
if copy.length > self.field_length
|
285
|
+
copy = copy[0..self.field_length-1]
|
286
|
+
copy[-3..-1] = "..." unless self.field_length <= 3 # don't use ellipses when the string is tiny
|
235
287
|
end
|
236
288
|
copy
|
237
289
|
end
|
290
|
+
|
291
|
+
# determine how wide this column is going to be
|
292
|
+
def initialize_field_length(data)
|
293
|
+
# skip all this nonsense if we've been explicitly told what to do
|
294
|
+
if self.options[:field_length] and self.options[:field_length] > 0
|
295
|
+
self.field_length = self.options[:field_length]
|
296
|
+
else
|
297
|
+
self.field_length = self.name.length # it has to at least be long enough for the column header!
|
298
|
+
|
299
|
+
find_data_length(data, self.method, Time.now)
|
300
|
+
end
|
301
|
+
|
302
|
+
self.field_length = [self.field_length, self.max_field_length].min # never bigger than the max
|
303
|
+
end
|
304
|
+
|
305
|
+
# recurse through the data set using the method chain to find the longest field (or until time's up)
|
306
|
+
def find_data_length(data, method, start)
|
307
|
+
return if (Time.now - start) > 2
|
308
|
+
return if method.nil?
|
309
|
+
return if data.nil?
|
310
|
+
return if self.field_length >= self.max_field_length
|
311
|
+
|
312
|
+
wrap(data).each do |data_obj|
|
313
|
+
next_method = method.split(".").first
|
314
|
+
|
315
|
+
return unless data_obj.respond_to? next_method
|
316
|
+
|
317
|
+
if next_method == method # done!
|
318
|
+
self.field_length = [self.field_length, data_obj.send(next_method).to_s.length].max
|
319
|
+
else # keep going
|
320
|
+
find_data_length(data_obj.send(next_method), method[(next_method.length + 1)..-1], start)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
# add the next level to the method_chain
|
326
|
+
def get_current_method(method_chain)
|
327
|
+
if self.method.start_with? method_chain
|
328
|
+
current_method = String.new(self.method)
|
329
|
+
current_method = current_method[method_chain.length, current_method.length]
|
330
|
+
current_method.split(".").detect { |m| m != "" }
|
331
|
+
end
|
332
|
+
end
|
238
333
|
end
|
334
|
+
|
239
335
|
end
|
240
336
|
|
241
337
|
module Kernel
|
@@ -243,10 +339,101 @@ module Kernel
|
|
243
339
|
start = Time.now
|
244
340
|
table_print = TablePrint.new
|
245
341
|
puts table_print.tp(data, options)
|
246
|
-
Time.now - start
|
342
|
+
Time.now - start # we have to return *something*, might as well be execution time.
|
247
343
|
end
|
248
344
|
|
249
345
|
module_function :tp
|
250
346
|
end
|
251
347
|
|
348
|
+
## Some nested classes to make development easier! Make sure you don't commit these uncommented.
|
349
|
+
#
|
350
|
+
#class TestClass
|
351
|
+
# attr_accessor :title, :name, :blogs, :locker
|
352
|
+
#
|
353
|
+
# def initialize(title, name, blogs, locker)
|
354
|
+
# self.title = title
|
355
|
+
# self.name = name
|
356
|
+
# self.blogs = blogs
|
357
|
+
# self.locker = locker
|
358
|
+
# end
|
359
|
+
#end
|
360
|
+
#
|
361
|
+
#class Blog
|
362
|
+
# attr_accessor :title, :summary
|
363
|
+
#
|
364
|
+
# def initialize(title, summary)
|
365
|
+
# self.title = title
|
366
|
+
# self.summary = summary
|
367
|
+
# end
|
368
|
+
#end
|
369
|
+
#
|
370
|
+
#class Locker
|
371
|
+
# attr_accessor :assets
|
372
|
+
#
|
373
|
+
# def initialize(assets)
|
374
|
+
# self.assets = assets
|
375
|
+
# end
|
376
|
+
#end
|
377
|
+
#
|
378
|
+
#class Asset
|
379
|
+
# attr_accessor :url, :caption
|
380
|
+
#
|
381
|
+
# def initialize(url, caption)
|
382
|
+
# self.url = url
|
383
|
+
# self.caption = caption
|
384
|
+
# end
|
385
|
+
#end
|
386
|
+
#
|
387
|
+
#stack = [
|
388
|
+
#
|
389
|
+
# TestClass.new("one title", "one name", [
|
390
|
+
# Blog.new("one blog title1", "one blog sum1"),
|
391
|
+
# Blog.new("one blog title2", "one blog sum2"),
|
392
|
+
# Blog.new("one blog title3", "one blog sum3"),
|
393
|
+
# ],
|
394
|
+
# Locker.new([
|
395
|
+
# Asset.new("one asset url1", "one asset cap1"),
|
396
|
+
# Asset.new("one asset url2", "one asset cap2"),
|
397
|
+
# Asset.new("one asset url3", "one asset cap3"),
|
398
|
+
# ])
|
399
|
+
# ),
|
400
|
+
# TestClass.new("two title", "two name", [
|
401
|
+
# Blog.new("two blog title1", "two blog sum1"),
|
402
|
+
# Blog.new("two blog title2", "two blog sum2"),
|
403
|
+
# Blog.new("two blog title3", "two blog sum3"),
|
404
|
+
# ],
|
405
|
+
# Locker.new([
|
406
|
+
# Asset.new("two asset url1", "two asset cap1"),
|
407
|
+
# Asset.new("two asset url2", "two asset cap2"),
|
408
|
+
# Asset.new("two asset url3", "two asset cap3"),
|
409
|
+
# ])
|
410
|
+
# ),
|
411
|
+
# TestClass.new("three title", "three name", [
|
412
|
+
# Blog.new("three blog title1", "three blog sum1"),
|
413
|
+
# Blog.new("three blog title2", "three blog sum2"),
|
414
|
+
# Blog.new("three blog title3", "three blog sum3"),
|
415
|
+
# ],
|
416
|
+
# Locker.new([
|
417
|
+
# Asset.new("three asset url1", "three asset cap1"),
|
418
|
+
# Asset.new("three asset url2", "three asset cap2"),
|
419
|
+
# Asset.new("three asset url3", "three asset cap3"),
|
420
|
+
# ])
|
421
|
+
# ),
|
422
|
+
# TestClass.new("four title", "four name", [
|
423
|
+
# Blog.new("four blog title1", "four blog sum1"),
|
424
|
+
# Blog.new("four blog title2", "four blog sum2"),
|
425
|
+
# Blog.new("four blog title3", "four blog sum3"),
|
426
|
+
# ],
|
427
|
+
# Locker.new([
|
428
|
+
# Asset.new("four asset url1", "four asset cap1"),
|
429
|
+
# Asset.new("four asset url2", "four asset cap2"),
|
430
|
+
# Asset.new("four asset url3", "four asset cap3"),
|
431
|
+
# ])
|
432
|
+
# ),
|
433
|
+
#]
|
434
|
+
|
435
|
+
#tp stack, :include => ["blogs.title", "blogs.summary", "locker.assets.url", "locker.assets.caption"]
|
436
|
+
|
437
|
+
|
438
|
+
|
252
439
|
|
data/table_print.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{table_print}
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.2.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Chris Doyle"]
|
12
|
-
s.date = %q{2011-04-
|
12
|
+
s.date = %q{2011-04-26}
|
13
13
|
s.description = %q{TablePrint formats an object or array of objects into columns for easy reading. To do this, it assumes the objects in your array all respond to the same methods (vs pretty_print or awesome_print, who can't create columns because your objects could be entirely different).}
|
14
14
|
s.email = %q{archslide@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
data/test/helper.rb
CHANGED
@@ -16,3 +16,41 @@ require 'table_print'
|
|
16
16
|
|
17
17
|
class Test::Unit::TestCase
|
18
18
|
end
|
19
|
+
|
20
|
+
class MyNestedClass
|
21
|
+
attr_accessor :title, :name, :summary, :captions
|
22
|
+
|
23
|
+
def initialize(title, name, summary, captions)
|
24
|
+
self.title = title
|
25
|
+
self.name = name
|
26
|
+
self.summary = summary
|
27
|
+
self.captions = captions.collect{|c| Caption.new(c[:text], c[:photo_url])}
|
28
|
+
end
|
29
|
+
|
30
|
+
class Caption
|
31
|
+
attr_accessor :text, :photo_url
|
32
|
+
|
33
|
+
def initialize(text, photo_url)
|
34
|
+
self.text = text
|
35
|
+
self.photo_url = photo_url
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.setup
|
40
|
+
[["how to", "bert", "bertto", [{:text => "no, really, how to", :photo_url => "http://www.123.com/456.jpg"}]],
|
41
|
+
["enemies", "ernie", "ernieenemies", [{:text => "no, really, enemies", :photo_url => "http://www.234.com/567.jpg"}]],
|
42
|
+
["a walk to forget", "big bird", "bigbirdforget", [{:text => "no, really, a walk to forget", :photo_url => "http://www.345.com/678.jpg"}]],
|
43
|
+
["cookies", "cookie monster", "cookiemonstercookies", [{:text => "no, really, cookies", :photo_url => "http://www.456.com/789.jpg"}]],
|
44
|
+
["your mom", "the count", "thecountmom", [{:text => "no, really, your mom", :photo_url => "http://www.789.com/1011.jpg"}]],
|
45
|
+
["fire!", "elmo", "elmofire!", [{:text => "no, really, fire!", :photo_url => "http://www.8910.com/1112.jpg"}]],
|
46
|
+
["eat your veggies", "michelle obama", "michelleobamaveggies", [{:text => "no, really, eat your veggies", :photo_url => "http://www.91011.com/1213.jpg"}]],
|
47
|
+
["wakka wakka", "ellen degeneres", "ellendegenereswakka", [{:text => "no, really, wakka wakka", :photo_url => "http://www.101112.com/1314.jpg"}]],
|
48
|
+
["peas and carrots", "the hulk", "thehulkcarrots", [{:text => "no, really, peas and carrots", :photo_url => "http://www.abc.com/def.jpg"}]],
|
49
|
+
["juan valdez", "camaro", "camaravaldez", [{:text => "no, really, juan valdez", :photo_url => "http://www.bcd.com/efg.jpg"}]],
|
50
|
+
["fish fish fish", "alaska", "alaskafish", [{:text => "no, really, fish fish fish", :photo_url => "http://www.cde.com/fgh.jpg"}]],
|
51
|
+
["tracks", "sir toppem hat", "sirtoppemhattracks", [{:text => "no, really, tracks", :photo_url => "http://www.def.com/ghi.jpg"}]],
|
52
|
+
["smoking stacked", "thomas", "thomasstacked", [{:text => "no, really, smoking stacked", :photo_url => "http://www.efg.com/hij.jpg"}]],
|
53
|
+
["cannes", "alpo", "alpocannes", [{:text => "no, really, cannes", :photo_url => "http://www.fgh.com/ijk.jpg"}]],
|
54
|
+
].collect { |a| MyNestedClass.new(a[0], a[1], a[2], a[3]) }
|
55
|
+
end
|
56
|
+
end
|
data/test/test_column.rb
CHANGED
@@ -1,10 +1,30 @@
|
|
1
1
|
require 'helper'
|
2
2
|
|
3
3
|
class TablePrint
|
4
|
-
class
|
4
|
+
class ColumnHelper
|
5
5
|
def _truncate(field_value)
|
6
6
|
truncate(field_value)
|
7
7
|
end
|
8
|
+
|
9
|
+
def _get_current_method(method_chain)
|
10
|
+
get_current_method(method_chain)
|
11
|
+
end
|
12
|
+
|
13
|
+
def _add_to_stack?(method_chain, method_hash)
|
14
|
+
add_to_stack?(method_chain, method_hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
def _formatted_cell_value(data_obj, method_chain)
|
18
|
+
formatted_cell_value(data_obj, method_chain)
|
19
|
+
end
|
20
|
+
|
21
|
+
def _find_data_length(data, method_chain, start)
|
22
|
+
find_data_length(data, method_chain, start)
|
23
|
+
end
|
24
|
+
|
25
|
+
def _add_stack_objects(stack, data_obj, method_chain, start)
|
26
|
+
add_stack_objects(stack, data_obj, method_chain, start)
|
27
|
+
end
|
8
28
|
end
|
9
29
|
end
|
10
30
|
|
@@ -12,15 +32,35 @@ class TestTablePrint < Test::Unit::TestCase
|
|
12
32
|
|
13
33
|
# TODO: active record tests if defined?(ActiveRecord)
|
14
34
|
|
15
|
-
#
|
35
|
+
# Ordered to match method order in the Column class
|
36
|
+
|
37
|
+
# attr_accessor :field_length, :max_field_length, :method, :name, :options
|
38
|
+
context 'A column object' do
|
39
|
+
setup do
|
40
|
+
@column = TablePrint::ColumnHelper.new([], "to_s")
|
41
|
+
@column.field_length = 0
|
42
|
+
@column.max_field_length = 0
|
43
|
+
@column.method = 0
|
44
|
+
@column.name = 0
|
45
|
+
@column.options = 0
|
46
|
+
end
|
47
|
+
should 'allow writes to and reads from its attributes' do
|
48
|
+
assert_equal 0, @column.field_length
|
49
|
+
assert_equal 0, @column.max_field_length
|
50
|
+
assert_equal 0, @column.method
|
51
|
+
assert_equal 0, @column.name
|
52
|
+
assert_equal 0, @column.options
|
53
|
+
end
|
54
|
+
end
|
16
55
|
|
56
|
+
# def initialize(data, method, options = {})
|
17
57
|
context 'Instantiating a Column' do
|
18
58
|
context 'with a display_method' do
|
19
59
|
setup do
|
20
|
-
@column = TablePrint::
|
60
|
+
@column = TablePrint::ColumnHelper.new([], "to_s")
|
21
61
|
end
|
22
62
|
should 'remember the display method' do
|
23
|
-
assert_equal "to_s", @column.
|
63
|
+
assert_equal "to_s", @column.method
|
24
64
|
end
|
25
65
|
should 'set the name' do
|
26
66
|
assert_equal "to s", @column.name
|
@@ -30,18 +70,27 @@ class TestTablePrint < Test::Unit::TestCase
|
|
30
70
|
context 'with options including' do
|
31
71
|
context 'name' do
|
32
72
|
setup do
|
33
|
-
@column = TablePrint::
|
73
|
+
@column = TablePrint::ColumnHelper.new([], "first", {:name => "test_tube"})
|
34
74
|
end
|
35
75
|
|
36
76
|
should 'set the name according to the options' do
|
37
77
|
assert_equal "test_tube", @column.name
|
38
78
|
end
|
79
|
+
|
80
|
+
context 'when the method name contains dots' do
|
81
|
+
setup do
|
82
|
+
@column = TablePrint::ColumnHelper.new(["short"], "method1.method2", {:name => "test_tube"})
|
83
|
+
end
|
84
|
+
should 'use the option' do
|
85
|
+
assert_equal "TEST_TUBE", @column.formatted_header
|
86
|
+
end
|
87
|
+
end
|
39
88
|
end
|
40
89
|
|
41
90
|
context 'a field_length' do
|
42
91
|
context 'that is valid' do
|
43
92
|
setup do
|
44
|
-
@column = TablePrint::
|
93
|
+
@column = TablePrint::ColumnHelper.new(["short"], "first", {:field_length => 20})
|
45
94
|
end
|
46
95
|
|
47
96
|
should 'set the field length according to the options' do
|
@@ -51,7 +100,7 @@ class TestTablePrint < Test::Unit::TestCase
|
|
51
100
|
|
52
101
|
context 'that is less than 1' do
|
53
102
|
setup do
|
54
|
-
@column = TablePrint::
|
103
|
+
@column = TablePrint::ColumnHelper.new(["short"], "first", {:field_length => 0})
|
55
104
|
end
|
56
105
|
|
57
106
|
should 'ignore the field_length' do
|
@@ -61,7 +110,7 @@ class TestTablePrint < Test::Unit::TestCase
|
|
61
110
|
|
62
111
|
context 'that is bigger than the max' do
|
63
112
|
setup do
|
64
|
-
@column = TablePrint::
|
113
|
+
@column = TablePrint::ColumnHelper.new(["short"], "first", {:field_length => 20, :max_field_length => 10})
|
65
114
|
end
|
66
115
|
|
67
116
|
should 'respect the max_field_length' do
|
@@ -72,64 +121,259 @@ class TestTablePrint < Test::Unit::TestCase
|
|
72
121
|
end
|
73
122
|
end
|
74
123
|
|
124
|
+
# def formatted_header
|
125
|
+
context 'The formatted header function' do
|
126
|
+
context 'when the method name contains no special characters and is shorter than the max field length' do
|
127
|
+
setup do
|
128
|
+
@column = TablePrint::ColumnHelper.new(["short"], "first", {:field_length => 10})
|
129
|
+
end
|
130
|
+
should 'uppercase the method name and pad with spaces' do
|
131
|
+
assert_equal "FIRST ", @column.formatted_header
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'when the method name contains no special characters and is longer than the max field length' do
|
136
|
+
setup do
|
137
|
+
@column = TablePrint::ColumnHelper.new(["short"], "longMethodName", {:field_length => 10})
|
138
|
+
end
|
139
|
+
should 'uppercase and truncate the method name' do
|
140
|
+
assert_equal "LONGMET...", @column.formatted_header
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'when the method name contains underscores' do
|
145
|
+
setup do
|
146
|
+
@column = TablePrint::ColumnHelper.new(["short"], "method_name")
|
147
|
+
end
|
148
|
+
should 'replace underscores with spaces' do
|
149
|
+
assert_equal "METHOD NAME", @column.formatted_header
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'when the method name contains dots' do
|
154
|
+
setup do
|
155
|
+
@column = TablePrint::ColumnHelper.new(["short"], "method1.method2")
|
156
|
+
end
|
157
|
+
should 'replace dots with greater-thans' do
|
158
|
+
assert_equal "METHOD1 > METHOD2", @column.formatted_header
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
context 'when the user passes a name in the column options' do
|
163
|
+
setup do
|
164
|
+
@column = TablePrint::ColumnHelper.new(["short"], "method1.method2", {:name => "whoop"})
|
165
|
+
end
|
166
|
+
should 'use that name instead of the method name' do
|
167
|
+
assert_equal "WHOOP", @column.formatted_header
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# def formatted_cell_value(data_obj, method_chain)
|
173
|
+
context 'The formatted_cell_value method' do
|
174
|
+
should 'return whitespace if the method chain does not exactly match the column definition method' do
|
175
|
+
assert_equal " ", TablePrint::ColumnHelper.new([], "captions.text")._formatted_cell_value("test", "id")
|
176
|
+
assert_equal " ", TablePrint::ColumnHelper.new([], "captions.text")._formatted_cell_value("test", "captions")
|
177
|
+
end
|
178
|
+
|
179
|
+
should 'return whitespace if the method chain begins the ' do
|
180
|
+
assert_equal "no, really, ...", TablePrint::ColumnHelper.new([], "captions.text")._formatted_cell_value(MyNestedClass.setup.first.captions.first, "captions")
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# def add_stack_objects(stack, data_obj, method_chain, method_hash)
|
185
|
+
context 'The add_stack_objects method' do
|
186
|
+
context 'when objects need to be added to the stack' do
|
187
|
+
setup do
|
188
|
+
@tp = TablePrint::ColumnHelper.new(MyNestedClass.setup, "captions.text")
|
189
|
+
@stack = [1, 2, 3]
|
190
|
+
@tp._add_stack_objects(@stack, MyNestedClass.setup.first, "", {})
|
191
|
+
end
|
192
|
+
should 'increase stack size' do
|
193
|
+
assert_equal 4, @stack.size
|
194
|
+
end
|
195
|
+
should 'push the new objects on the front of the stack' do
|
196
|
+
assert_equal MyNestedClass::Caption, @stack.first.first.class
|
197
|
+
end
|
198
|
+
should 'include the updated method_chain in the stack' do
|
199
|
+
assert_equal "captions", @stack.first.last
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
|
205
|
+
# def add_to_stack?(method_chain, method_hash = {})
|
206
|
+
context 'The add_to_stack? method' do
|
207
|
+
should 'appropriately respond to its arguments' do
|
208
|
+
|
209
|
+
# our first method produces an array, so yes, stack 'em up
|
210
|
+
assert TablePrint::ColumnHelper.new(MyNestedClass.setup, "captions.text")._add_to_stack?("", {})
|
211
|
+
|
212
|
+
# captions has already been called, so it gets popped off our method chain. text is the final method, so no, don't stack
|
213
|
+
assert !TablePrint::ColumnHelper.new(MyNestedClass.setup, "captions.text")._add_to_stack?("captions", {})
|
214
|
+
|
215
|
+
# the method isn't one of ours, so there's nothing for us to do
|
216
|
+
assert !TablePrint::ColumnHelper.new(MyNestedClass.setup, "captions.text")._add_to_stack?("id", {})
|
217
|
+
|
218
|
+
# another column has already added the captions to the stack, so there's no need for us to do it
|
219
|
+
assert !TablePrint::ColumnHelper.new(MyNestedClass.setup, "captions.text")._add_to_stack?("", {"captions" => {}})
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# def wrap(object)
|
224
|
+
|
225
|
+
# def truncate(field_value)
|
75
226
|
context 'The truncate function' do
|
76
227
|
should 'let short strings pass through' do
|
77
|
-
assert_equal "asdf", TablePrint::
|
228
|
+
assert_equal "asdf", TablePrint::ColumnHelper.new(["a long long long string"], "first")._truncate("asdf")
|
78
229
|
end
|
79
230
|
|
80
231
|
should 'truncate long strings with ellipses' do
|
81
|
-
|
232
|
+
# have to put long data in the data set to field_length is pushed out to the default max_field_length
|
233
|
+
assert_equal "123456789012345678901234567...", TablePrint::ColumnHelper.new(["1234567890123456789012345678901234567890"], "first")._truncate("1234567890123456789012345678901234567890")
|
82
234
|
end
|
83
235
|
|
84
|
-
context '
|
236
|
+
context 'with a non-default field length' do
|
85
237
|
should 'truncate long strings with ellipses' do
|
86
|
-
|
238
|
+
tp = TablePrint::ColumnHelper.new([], "")
|
239
|
+
tp.field_length = 10
|
240
|
+
assert_equal "1234567...", tp._truncate("1234567890123456789012345678901234567890")
|
87
241
|
end
|
88
242
|
end
|
89
243
|
|
90
244
|
context 'when the max length is tiny' do
|
91
245
|
should 'truncate long strings without ellipses' do
|
92
|
-
assert_equal "
|
93
|
-
assert_equal "
|
94
|
-
assert_equal "1", TablePrint::
|
95
|
-
assert_equal "12", TablePrint::
|
96
|
-
assert_equal "123", TablePrint::
|
97
|
-
assert_equal "1...", TablePrint::
|
246
|
+
assert_equal "123456789012345678901234567...", TablePrint::ColumnHelper.new(["1234567890123456789012345678901234567890"], "first", :field_length => -10)._truncate("1234567890123456789012345678901234567890")
|
247
|
+
assert_equal "123456789012345678901234567...", TablePrint::ColumnHelper.new(["1234567890123456789012345678901234567890"], "first", :field_length => 0)._truncate("1234567890123456789012345678901234567890")
|
248
|
+
assert_equal "1", TablePrint::ColumnHelper.new([], "", :field_length => 1)._truncate("1234567890123456789012345678901234567890")
|
249
|
+
assert_equal "12", TablePrint::ColumnHelper.new([], "", :field_length => 2)._truncate("1234567890123456789012345678901234567890")
|
250
|
+
assert_equal "123", TablePrint::ColumnHelper.new([], "", :field_length => 3)._truncate("1234567890123456789012345678901234567890")
|
251
|
+
assert_equal "1...", TablePrint::ColumnHelper.new([], "", :field_length => 4)._truncate("1234567890123456789012345678901234567890")
|
98
252
|
end
|
99
253
|
end
|
100
254
|
end
|
101
255
|
|
256
|
+
# def initialize_field_length(data)
|
102
257
|
context 'The field length function' do
|
258
|
+
should 'honor the field_length options' do
|
259
|
+
assert_equal 5, TablePrint::ColumnHelper.new(["hello there madam"], "to_s", :field_length => 5).field_length
|
260
|
+
assert_equal 30, TablePrint::ColumnHelper.new(["hello there madam"], "to_s", :field_length => 5).max_field_length
|
261
|
+
end
|
262
|
+
|
263
|
+
should 'honor the max_length option' do
|
264
|
+
assert_equal 5, TablePrint::ColumnHelper.new(["hello there madam"], "to_s", :max_field_length => 5).field_length
|
265
|
+
assert_equal 5, TablePrint::ColumnHelper.new(["hello there madam"], "to_s", :max_field_length => 5).max_field_length
|
266
|
+
end
|
267
|
+
|
268
|
+
should 'honor the max_length option over the field_length option' do
|
269
|
+
assert_equal 5, TablePrint::ColumnHelper.new(["hello there madam"], "to_s", :max_field_length => 5, :field_length => 10).field_length
|
270
|
+
assert_equal 5, TablePrint::ColumnHelper.new(["hello there madam"], "to_s", :max_field_length => 5, :field_length => 10).max_field_length
|
271
|
+
end
|
272
|
+
|
103
273
|
should 'find the maximum width of the data' do
|
104
|
-
assert_equal 11, TablePrint::
|
274
|
+
assert_equal 11, TablePrint::ColumnHelper.new(["hello there"], "to_s").field_length
|
105
275
|
end
|
106
276
|
|
107
277
|
context 'when the data is longer than the max_field_length' do
|
108
278
|
should 'equal the max field length' do
|
109
|
-
assert_equal 5, TablePrint::
|
279
|
+
assert_equal 5, TablePrint::ColumnHelper.new(["hello there"], "to_s", :max_field_length => 5).field_length
|
110
280
|
end
|
111
281
|
end
|
112
282
|
|
113
283
|
context 'when the column name is longer than the data' do
|
114
284
|
should 'reflect the column name length' do
|
115
|
-
assert_equal 4, TablePrint::
|
116
|
-
assert_equal 12, TablePrint::
|
285
|
+
assert_equal 4, TablePrint::ColumnHelper.new(["he"], "to_s", :max_field_length => 5).field_length
|
286
|
+
assert_equal 12, TablePrint::ColumnHelper.new(["hello"], "to_s", :name => "foobar THIS!").field_length
|
117
287
|
end
|
118
288
|
end
|
119
289
|
|
120
|
-
context 'when the
|
121
|
-
should '
|
122
|
-
assert_equal
|
123
|
-
assert_equal 5, TablePrint::Column.new([[false]], "first", :name => "dur").field_length
|
290
|
+
context 'when the method is recursive' do
|
291
|
+
should 'find the maximum width of the data' do
|
292
|
+
assert_equal 30, TablePrint::ColumnHelper.new(MyNestedClass.setup, "captions.photo_url", :max_field_length => 50).field_length
|
124
293
|
end
|
125
294
|
end
|
295
|
+
end
|
126
296
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
297
|
+
# def find_data_length(data, method, start)
|
298
|
+
context 'The find_data_length method' do
|
299
|
+
context 'when method_chain is a top level method' do
|
300
|
+
setup do
|
301
|
+
@tp = TablePrint::ColumnHelper.new([], "")
|
302
|
+
@tp._find_data_length(MyNestedClass.setup, "title", Time.now)
|
303
|
+
end
|
304
|
+
should 'set field_length to the longest value in the data set' do
|
305
|
+
assert_equal 16, @tp.field_length
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
context 'when method_chain is not a top level method' do
|
310
|
+
setup do
|
311
|
+
@tp = TablePrint::ColumnHelper.new([], "")
|
312
|
+
@tp._find_data_length(MyNestedClass.setup, "captions.text", Time.now)
|
313
|
+
end
|
314
|
+
should 'set field_length to the longest value in the data set' do
|
315
|
+
assert_equal 28, @tp.field_length
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
context 'when the data value is longer than max_field_length' do
|
320
|
+
setup do
|
321
|
+
@tp = TablePrint::ColumnHelper.new([], "", :max_field_length => 10)
|
322
|
+
@tp._find_data_length(MyNestedClass.setup, "captions.photo_url", Time.now)
|
323
|
+
end
|
324
|
+
should 'ignore max_field_length (initialize_field_length handles that - this method is just about the data)' do
|
325
|
+
assert_equal 26, @tp.field_length
|
131
326
|
end
|
132
327
|
end
|
133
328
|
end
|
134
329
|
|
330
|
+
# def get_current_method(method_chain)
|
331
|
+
context 'get_current_method' do
|
332
|
+
context 'with a simple method signature' do
|
333
|
+
context 'and no method chain' do
|
334
|
+
should 'return the method itself' do
|
335
|
+
assert_equal "m1", TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1")._get_current_method("")
|
336
|
+
end
|
337
|
+
end
|
338
|
+
context 'and a method chain that does not match' do
|
339
|
+
should 'return nil' do
|
340
|
+
assert_equal nil, TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1")._get_current_method("m2")
|
341
|
+
end
|
342
|
+
end
|
343
|
+
context 'and an overly long method chain' do
|
344
|
+
should 'return nil' do
|
345
|
+
assert_equal nil, TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1")._get_current_method("m1.m2")
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
context 'with a compound method signature' do
|
351
|
+
context 'and no method chain' do
|
352
|
+
should 'return the first method in the chain' do
|
353
|
+
assert_equal "m1", TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1.m2.m3")._get_current_method("")
|
354
|
+
end
|
355
|
+
end
|
356
|
+
context 'and a method chain that does not match' do
|
357
|
+
should 'return nil' do
|
358
|
+
assert_equal nil, TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1.m2.m3")._get_current_method("m2")
|
359
|
+
end
|
360
|
+
end
|
361
|
+
context 'and a valid method chain' do
|
362
|
+
should 'return the next method in the chain' do
|
363
|
+
assert_equal "m2", TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1.m2.m3")._get_current_method("m1")
|
364
|
+
assert_equal "m3", TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1.m2.m3")._get_current_method("m1.m2")
|
365
|
+
end
|
366
|
+
end
|
367
|
+
context 'and an overly long method chain' do
|
368
|
+
should 'return nil' do
|
369
|
+
assert_equal nil, TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1.m2.m3")._get_current_method("m1.m2.m3.m4")
|
370
|
+
end
|
371
|
+
end
|
372
|
+
context 'and a method chain matching our method signature' do
|
373
|
+
should 'return nil' do
|
374
|
+
assert_equal nil, TablePrint::ColumnHelper.new(MyNestedClass.setup, "m1.m2.m3")._get_current_method("m1.m2.m3")
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
135
379
|
end
|
data/test/test_table_print.rb
CHANGED
@@ -28,6 +28,7 @@ class MyClass
|
|
28
28
|
end
|
29
29
|
end
|
30
30
|
|
31
|
+
|
31
32
|
class TablePrint
|
32
33
|
def _get_display_methods(data_obj, options)
|
33
34
|
get_display_methods(data_obj, options)
|
@@ -37,8 +38,8 @@ class TablePrint
|
|
37
38
|
get_default_display_methods(data_obj)
|
38
39
|
end
|
39
40
|
|
40
|
-
def
|
41
|
-
|
41
|
+
def _sort_display_methods(display_methods)
|
42
|
+
sort_display_methods(display_methods)
|
42
43
|
end
|
43
44
|
end
|
44
45
|
|
@@ -93,19 +94,6 @@ class TestTablePrint < Test::Unit::TestCase
|
|
93
94
|
end
|
94
95
|
end
|
95
96
|
|
96
|
-
context 'The clean_display_methods function' do
|
97
|
-
should 'only give back valid methods' do
|
98
|
-
assert_equal [], @tp._clean_display_methods(ManyMethods.new, [""])
|
99
|
-
assert_equal [], @tp._clean_display_methods(ManyMethods.new, [nil])
|
100
|
-
assert_equal ["title"], @tp._clean_display_methods(ManyMethods.new, ["title"])
|
101
|
-
assert_equal ["title"], @tp._clean_display_methods(ManyMethods.new, [:title])
|
102
|
-
assert_equal ["title"], @tp._clean_display_methods(ManyMethods.new, ["title", "title"])
|
103
|
-
assert_equal ["title"], @tp._clean_display_methods(ManyMethods.new, ["title", :title])
|
104
|
-
assert_equal ["title"], @tp._clean_display_methods(ManyMethods.new, [:title, :title])
|
105
|
-
assert_equal ["title"], @tp._clean_display_methods(ManyMethods.new, ["title", "january1985"])
|
106
|
-
end
|
107
|
-
end
|
108
|
-
|
109
97
|
context 'The default display methods for a custom class with one method' do
|
110
98
|
should 'be that method' do
|
111
99
|
assert_equal ["title"], @tp._get_default_display_methods(OneMethod.new)
|
@@ -171,3 +159,4 @@ class TestTablePrint < Test::Unit::TestCase
|
|
171
159
|
end
|
172
160
|
end
|
173
161
|
end
|
162
|
+
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: table_print
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 23
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
- 1
|
9
8
|
- 2
|
10
|
-
|
9
|
+
- 0
|
10
|
+
version: 0.2.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Chris Doyle
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-04-
|
18
|
+
date: 2011-04-26 00:00:00 -07:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|