ruport 0.7.2 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. data/AUTHORS +7 -3
  2. data/Rakefile +8 -9
  3. data/TODO +16 -0
  4. data/examples/RWEmerson.jpg +0 -0
  5. data/examples/centered_pdf_text_box.rb +66 -0
  6. data/examples/invoice.rb +35 -25
  7. data/examples/invoice_report.rb +1 -1
  8. data/examples/line_plotter.rb +1 -1
  9. data/examples/pdf_table_with_title.rb +42 -0
  10. data/lib/ruport.rb +5 -7
  11. data/lib/ruport.rb.rej +41 -0
  12. data/lib/ruport.rb~ +85 -0
  13. data/lib/ruport/attempt.rb +59 -59
  14. data/lib/ruport/config.rb +15 -4
  15. data/lib/ruport/data.rb +0 -2
  16. data/lib/ruport/data/groupable.rb +25 -16
  17. data/lib/ruport/data/record.rb +128 -102
  18. data/lib/ruport/data/table.rb +352 -199
  19. data/lib/ruport/data/taggable.rb +18 -7
  20. data/lib/ruport/format/html.rb +3 -1
  21. data/lib/ruport/format/latex.rb +1 -1
  22. data/lib/ruport/format/latex.rb.rej +26 -0
  23. data/lib/ruport/format/latex.rb~ +47 -0
  24. data/lib/ruport/format/pdf.rb +111 -28
  25. data/lib/ruport/format/pdf.rb.rej +168 -0
  26. data/lib/ruport/format/pdf.rb~ +189 -0
  27. data/lib/ruport/format/plugin.rb +0 -5
  28. data/lib/ruport/format/svg.rb +4 -4
  29. data/lib/ruport/format/xml.rb +3 -3
  30. data/lib/ruport/generator.rb +66 -27
  31. data/lib/ruport/mailer.rb +4 -1
  32. data/lib/ruport/query.rb +13 -1
  33. data/lib/ruport/renderer.rb +89 -17
  34. data/lib/ruport/renderer/graph.rb +5 -5
  35. data/lib/ruport/renderer/table.rb +8 -9
  36. data/lib/ruport/report.rb +2 -6
  37. data/test/test_config.rb +88 -76
  38. data/test/{test_text_table.rb → test_format_text.rb} +4 -2
  39. data/test/test_groupable.rb +15 -13
  40. data/test/test_query.rb +6 -3
  41. data/test/test_record.rb +57 -33
  42. data/test/test_renderer.rb +77 -0
  43. data/test/test_report.rb +188 -181
  44. data/test/test_ruport.rb +5 -6
  45. data/test/test_table.rb +290 -190
  46. data/test/test_table_renderer.rb +56 -8
  47. data/test/test_taggable.rb +7 -8
  48. data/test/unit.log +259 -7
  49. metadata +22 -19
  50. data/lib/ruport/data/collection.rb +0 -65
  51. data/lib/ruport/data/set.rb +0 -148
  52. data/test/test_collection.rb +0 -30
  53. data/test/test_set.rb +0 -118
@@ -4,9 +4,42 @@
4
4
  # This is Free Software. For details, see LICENSE and COPYING
5
5
  # Copyright 2006 by respective content owners, all rights reserved.
6
6
 
7
+ module Ruport::Data
8
+
9
+ # === Overview
10
+ #
11
+ # This class implements some base features for Ruport::Data::Table,
12
+ # and may be used to make interaction with Data::Table like classes
13
+ # easier
14
+ #
15
+ module Collection
16
+ include Enumerable
17
+ include Taggable
7
18
 
8
- module Ruport::Data
19
+ # A simple formatting tool which allows you to quickly generate a formatted
20
+ # table from a <tt>Collection</tt> object.
21
+ #
22
+ # If a block is given, the Renderer::Table object will be yielded
23
+ #
24
+ # Example:
25
+ # my_collection.as(:csv) #=> "1,2,3\n4,5,6"
26
+ #
27
+ # my_collection.as(:csv) { |e| e.layout.show_table_headers = false }
28
+ #
29
+ def as(type)
30
+ Ruport::Renderer::Table.render(type) do |rend|
31
+ rend.data = self
32
+ yield(rend) if block_given?
33
+ end
34
+ end
35
+
36
+ # Converts a <tt>Collection</tt> object to a <tt>Data::Table</tt>.
37
+ def to_table(options={})
38
+ Table.new({:data => data.map { |r| r.to_a }}.merge(options))
39
+ end
9
40
 
41
+ end
42
+
10
43
  # === Overview
11
44
  #
12
45
  # This class is one of the core classes for building and working with data
@@ -20,9 +53,13 @@ module Ruport::Data
20
53
  # Once your data is in a Ruport::Data::Table object, it can be manipulated
21
54
  # to suit your needs, then used to build a report.
22
55
  #
23
- class Table < Collection
24
- include Groupable
56
+ class Table
57
+ include Collection
58
+ include Groupable
25
59
 
60
+ require "forwardable"
61
+ extend Forwardable
62
+
26
63
  # Creates a new table based on the supplied options.
27
64
  # Valid options:
28
65
  # <b><tt>:data</tt></b>:: An Array of Arrays representing the
@@ -36,6 +73,8 @@ module Ruport::Data
36
73
  #
37
74
  def initialize(options={})
38
75
  @column_names = options[:column_names] ? options[:column_names].dup : []
76
+ @record_class = options[:record_class] &&
77
+ options[:record_class].name || "Ruport::Data::Record"
39
78
  @data = []
40
79
  if options[:data]
41
80
  if options[:data].all? { |r| r.kind_of? Record }
@@ -49,22 +88,39 @@ module Ruport::Data
49
88
 
50
89
  # This Table's column names.
51
90
  attr_reader :column_names
52
- def_delegator :@data, :[]
91
+
92
+ attr_reader :data
93
+ def_delegators :@data, :each, :length, :size, :empty?, :[]
53
94
 
95
+
54
96
  # Sets the column names for this table. <tt>new_column_names</tt> should
55
97
  # be an array listing the names of the columns.
56
98
  #
57
99
  # Example:
58
- #
100
+ #
59
101
  # table = Table.new :data => [1,2,3], [3,4,5],
60
102
  # :column_names => %w[a b c]
61
103
  #
62
104
  # table.column_names = %w[e f g]
63
105
  #
64
106
  def column_names=(new_column_names)
107
+ columns = new_column_names.zip(@column_names)
65
108
  @column_names.replace(new_column_names.dup)
109
+ unless @data.empty?
110
+ each { |r|
111
+ columns.each_with_index { |x,i|
112
+ if x[1].nil?
113
+ r.rename_attribute(i,x[0])
114
+ elsif x[1] != x[0]
115
+ r.rename_attribute(x[1],x[0],false)
116
+ end
117
+ }
118
+ r.send(:reindex, @column_names)
119
+ }
120
+ end
66
121
  end
67
122
 
123
+
68
124
  # Compares this Table to another Table and returns <tt>true</tt> if
69
125
  # both the <tt>data</tt> and <tt>column_names</tt> are equal.
70
126
  #
@@ -84,18 +140,6 @@ module Ruport::Data
84
140
 
85
141
  alias_method :==, :eql?
86
142
 
87
- # Uses Ruport's built-in text plugin to render this Table into a String
88
- #
89
- # Example:
90
- #
91
- # data = Table.new :data => [1,2], [3,4],
92
- # :column_names => %w[a b]
93
- # puts data.to_s
94
- #
95
- def to_s
96
- as(:text)
97
- end
98
-
99
143
  # Used to add extra data to the Table. <tt>other</tt> can be an Array,
100
144
  # Hash or Record.
101
145
  #
@@ -110,23 +154,28 @@ module Ruport::Data
110
154
  def <<(other)
111
155
  case other
112
156
  when Array
113
- @data << Record.new(other, :attributes => @column_names)
157
+ attributes = @column_names.empty? ? nil : @column_names
158
+ @data << record_class.new(other, :attributes => attributes)
114
159
  when Hash
115
160
  raise ArgumentError unless @column_names
116
161
  arr = @column_names.map { |k| other[k] }
117
- @data << Record.new(arr, :attributes => @column_names)
118
- when Record
119
- raise ArgumentError unless column_names.eql? other.attributes
120
- @data << Record.new(other.data, :attributes => @column_names)
162
+ @data << record_class.new(arr, :attributes => @column_names)
163
+ when record_class
164
+ self << other.to_h
121
165
  @data.last.tags = other.tags.dup
122
166
  else
123
167
  raise ArgumentError
124
168
  end
125
169
  self
126
170
  end
171
+
172
+ def record_class
173
+ @record_class.split("::").inject(Class) { |c,el| c.send(:const_get,el) }
174
+ end
127
175
 
128
- # Used to combine two Tables. Throws an ArgumentError if the Tables don't
129
- # have identical columns.
176
+
177
+ # Used to merge two Tables by rows.
178
+ # Throws an ArgumentError if the Tables don't have identical columns.
130
179
  #
131
180
  # Example:
132
181
  #
@@ -144,34 +193,8 @@ module Ruport::Data
144
193
  Table.new(:column_names => @column_names, :data => @data + other.data)
145
194
  end
146
195
 
147
- # Reorders the columns that exist in the Table. Modifies this Table
148
- # in-place.
149
- #
150
- # Example:
151
- #
152
- # data = Table.new :data => [1,2], [3,4],
153
- # :column_names => %w[a b]
154
- #
155
- # data.reorder!([1,0])
156
- #
157
- def reorder!(*indices)
158
- indices = indices[0] if indices[0].kind_of? Array
159
-
160
- if @column_names && !@column_names.empty?
161
- x = if indices.all? { |i| i.kind_of? Integer }
162
- indices.map { |i| @column_names[i] }
163
- else
164
- indices
165
- end
166
- @column_names = x
167
- end
168
- @data.each { |r|
169
- r.reorder_data!(*indices)
170
- r.attributes = @column_names
171
- }; self
172
- end
173
-
174
- # Returns a copy of the Table with its columns in the requested order.
196
+
197
+ # Reorders the table's columns.
175
198
  #
176
199
  # Example:
177
200
  #
@@ -181,177 +204,212 @@ module Ruport::Data
181
204
  # two = one.reorder!([1,0])
182
205
  #
183
206
  def reorder(*indices)
184
- dup.reorder!(*indices)
185
- end
207
+ indices = indices[0] if indices[0].kind_of? Array
208
+
209
+ if indices.all? { |i| i.kind_of? Integer }
210
+ indices.map! { |i| @column_names[i] }
211
+ end
212
+
213
+ @column_names = indices
214
+
215
+ @data.each { |r|
216
+ r.attributes = @column_names
217
+ }; self
218
+ end
186
219
 
220
+
187
221
  # Adds an extra column to the Table. Available Options:
188
222
  #
189
- # <b><tt>:name</tt></b>:: The new column's name (required)
190
223
  # <b><tt>:fill</tt></b>:: The default value to use for the column in
191
224
  # existing rows. Set to nil if not specified.
225
+ #
226
+ # <b><tt>:position</tt></b>:: Inserts the column at the indicated position
227
+ # number.
228
+ #
229
+ # <b><tt>:before</tt></b>:: Inserts the new column before the column
230
+ # indicated. (by name)
231
+ #
232
+ # <b><tt>:after</tt></b>:: Inserts the new column after the column
233
+ # indicated. (by name)
234
+ #
235
+ # If a block is provided, it will be used to build up the column.
236
+ #
192
237
  #
193
238
  # Example:
194
239
  #
195
- # data = Table.new :data => [1,2], [3,4],
196
- # :column_names => %w[a b]
240
+ # data = Table("a","b") { |t| t << [1,2] << [3,4] }
241
+ #
242
+ # #basic usage, column full of 1's
243
+ # data.add_column 'new_column', :default => 1
244
+ #
245
+ # #new empty column before new_column
246
+ # data.add_column 'new_col2', :before => 'new_column'
197
247
  #
198
- # data.append_column :name => 'new_column', :fill => 1
248
+ # # new column placed just after column a
249
+ # data.add_column 'new_col3', :position => 1
199
250
  #
200
- def append_column(options={})
201
- self.column_names += [options[:name]] if options[:name]
251
+ # # new column built via a block, added at the end of the table
252
+ # data.add_column("new_col4") { |r| r.a + r.b }
253
+ #
254
+ def add_column(name,options={})
255
+ if pos = options[:position]
256
+ column_names.insert(pos,name)
257
+ elsif pos = options[:after]
258
+ column_names.insert(column_names.index(pos)+1,name)
259
+ elsif pos = options[:before]
260
+ column_names.insert(column_names.index(pos),name)
261
+ else
262
+ column_names << name
263
+ end
264
+
202
265
  if block_given?
203
- each { |r| r.data << yield(r) || options[:fill] }
266
+ each { |r| r[name] = yield(r) || options[:default] }
204
267
  else
205
- each { |r| r.data << options[:fill] }
268
+ each { |r| r[name] = options[:default] }
206
269
  end; self
207
270
  end
208
-
209
- # Removes a column from the Table. Any values in the specified column are
210
- # lost.
271
+
272
+ # Removes the given column from the table. May use name or position.
211
273
  #
212
274
  # Example:
213
275
  #
214
- # data = Table.new :data => [[1,2], [3,4]], :column_names => %w[a b]
215
- # data.append_column :name => 'new_column', :fill => 1
216
- # data.remove_column :name => 'new_column'
217
- # data == Table.new :data => [[1,2], [3,4]],
218
- # :column_names => %w[a b] #=> true
219
- # data = [[1,2],[3,4]].to_table
220
- # data.remove_column(1)
221
- # data.eql? [[1],[3]].to_table %w[a] #=> true
222
- #
223
- def remove_column(options={})
224
- if options.kind_of? Integer
225
- return reorder!((0...data[0].length).to_a - [options])
226
- elsif options.kind_of? Hash
227
- name = options[:name]
228
- else
229
- name = options
230
- end
231
-
232
- raise ArgumentError unless column_names.include? name
233
- reorder! column_names - [name]
276
+ # table.remove_column(0) #=> removes the first column
277
+ # table.remove_column("apple") #=> removes column named apple
278
+ def remove_column(col)
279
+ col = column_names[col] if col.kind_of? Fixnum
280
+ column_names.delete(col)
281
+ each { |r| r.send(:delete,col) }
234
282
  end
235
-
236
- # Create a copy of the Table: records will be copied as well.
283
+
284
+ # Removes multiple columns from the table. May use name or position
285
+ # Will autosplat arrays.
237
286
  #
238
287
  # Example:
239
- #
240
- # one = Table.new :data => [1,2], [3,4],
241
- # :column_names => %w[a b]
242
- # two = one.dup
243
- #
244
- def dup
245
- a = self.class.new(:data => @data, :column_names => @column_names)
246
- a.tags = tags.dup
247
- return a
288
+ # table.remove_columns('a','b','c')
289
+ # table.remove_columns([0,1])
290
+ def remove_columns(*cols)
291
+ cols = cols[0] if cols[0].kind_of? Array
292
+ cols.each { |col| remove_column(col) }
248
293
  end
249
-
250
- # Loads a CSV file directly into a Table using the FasterCSV library.
251
- #
294
+
295
+ # Renames a column. Will update Record attributes as well
296
+ #
252
297
  # Example:
253
- #
254
- # # treat first row as column_names
255
- # table = Table.load('mydata.csv')
256
298
  #
257
- # # do not assume the data has column_names
258
- # table = Table.load('mydata.csv',:has_names => false)
259
- #
260
- # # pass in FasterCSV options, such as column separators
261
- # table = Table.load('mydata.csv',:csv_opts => { :col_sep => "\t" })
262
- #
263
- def self.load(csv_file, options={},&block)
264
- get_table_from_csv(:foreach, csv_file, options,&block)
299
+ # old_values = table.map { |r| r.a }
300
+ # table.rename_column("a","zanzibar")
301
+ # new_values = table.map { |r| r.zanzibar }
302
+ # old_values == new_values #=> true
303
+ # table.column_names.include?("a") #=> false
304
+ def rename_column(old_name,new_name)
305
+ self.column_names[column_names.index(old_name)] = new_name
306
+ each { |r| r.rename_attribute(old_name,new_name,false)}
265
307
  end
266
308
 
267
- # Creates a Table from a CSV string using FasterCSV. See Table.load for
268
- # additional examples.
269
- #
270
- # table = Table.parse("a,b,c\n1,2,3\n4,5,6\n")
271
- #
272
- def self.parse(string, options={},&block)
273
- get_table_from_csv(:parse,string,options,&block)
274
- end
275
309
 
276
- def self.get_table_from_csv(msg,param,options={},&block) #:nodoc:
277
- options = {:has_names => true,
278
- :csv_options => {} }.merge(options)
279
- require "fastercsv"
280
- loaded_data = self.new
281
-
282
- first_line = true
283
- FasterCSV.send(msg,param,options[:csv_options]) do |row|
284
- if first_line && options[:has_names]
285
- loaded_data.column_names = row
286
- first_line = false
287
- elsif !block
288
- loaded_data << row
289
- else
290
- block[loaded_data,row]
291
- end
292
- end ; loaded_data
310
+ # Exchanges one column with another.
311
+ #
312
+ # Example:
313
+ #
314
+ # >> a = Table(%w[a b c]) { |t| t << [1,2,3] << [4,5,6] }
315
+ # >> puts a
316
+ # +-----------+
317
+ # | a | b | c |
318
+ # +-----------+
319
+ # | 1 | 2 | 3 |
320
+ # | 4 | 5 | 6 |
321
+ # +-----------+
322
+ # >> a.swap_column("a","c")
323
+ # >> puts a
324
+ # +-----------+
325
+ # | c | b | a |
326
+ # +-----------+
327
+ # | 3 | 2 | 1 |
328
+ # | 6 | 5 | 4 |
329
+ # +-----------+
330
+ def swap_column(a,b)
331
+ if [a,b].all? { |r| r.kind_of? Fixnum }
332
+ col_a,col_b = column_names[a],column_names[b]
333
+ column_names[a] = col_b
334
+ column_names[b] = col_a
335
+ else
336
+ a_ind, b_ind = [column_names.index(a), column_names.index(b)]
337
+ column_names[b_ind] = a
338
+ column_names[a_ind] = b
339
+ end
293
340
  end
294
-
295
- # Allows you to split Tables into multiple Tables for grouping.
341
+
342
+ # Allows you to specify a new column to replace an existing column
343
+ # in your table via a block.
344
+ #
345
+ # Example:
346
+ #
347
+ # >> a = Table(%w[a b c]) { |t| t << [1,2,3] << [4,5,6] }
348
+ # >> a.replace_column("c","c2") { |r| r.c * 2 + r.a }
349
+ #
350
+ # >> puts a
351
+ # +------------+
352
+ # | a | b | c2 |
353
+ # +------------+
354
+ # | 1 | 2 | 7 |
355
+ # | 4 | 5 | 16 |
356
+ # +------------+
357
+ def replace_column(old_col,new_col,&block)
358
+ add_column(new_col,:after => old_col,&block)
359
+ remove_column(old_col)
360
+ end
361
+
362
+ # Generates a sub table
363
+ #
364
+ # Examples:
365
+ #
366
+ # table = [[1,2,3,4],[5,6,7,8],[9,10,11,12]].to_table(%w[a b c d])
296
367
  #
297
- # Example:
368
+ # Using column_names and a range:
298
369
  #
299
- # a = Table.new(:column_name => %w[name a b c])
300
- # a << ["greg",1,2,3]
301
- # a << ["joe", 2,3,4]
302
- # a << ["greg",7,8,9]
303
- # a << ["joe", 1,2,3]
370
+ # sub_table = table.sub_table(%w[a b],1..-2)
371
+ # sub_table == [[1,2],[3,4]].to_table(%w[a b]) #=> true
304
372
  #
305
- # b = a.split :group => "name"
373
+ # Using just column_names:
306
374
  #
307
- # b.greg.eql? [[1,2,3],[7,8,9]].to_table(%w[a b c]) #=> true
308
- # b["joe"].eql? [[2,3,4],[1,2,3]].to_table(%w[a b c]) #=> true
375
+ # sub_table = table.sub_table(%w[a d])
376
+ # sub_table == [[1,4],[5,8],[9,12]].to_table(%w[a d]) #=> true
309
377
  #
310
- # You can also pass an Array to <tt>:group</tt>, and the resulting
311
- # attributes in the group will be joined by an underscore.
378
+ # Using column_names and a block:
312
379
  #
313
- # Example:
314
- #
315
- # a = Table.new(:column_names => %w[first_name last_name x]
316
- # a << %w[greg brown foo]
317
- # a << %w[greg gibson bar]
318
- # a << %w[greg brown baz]
319
- #
320
- # b = a.split :group => %w[first_name last_name]
321
- # a.greg_brown.length #=> 2
322
- # a["greg_gibson"].length #=> 1
323
- # a.greg_brown[0].x #=> "foo"
324
- #
325
- def split(options={})
326
- if options[:group].kind_of? Array
327
- group = map { |r| options[:group].map { |e| r[e] } }.uniq
328
- data = group.inject([]) { |s,g|
329
- s + [select { |r| options[:group].map { |e| r[e] }.eql?(g) }]
330
- }
331
- c = column_names - options[:group]
332
- else
333
- group = map { |r| r[options[:group]] }.uniq
334
- data = group.inject([]) { |s,g|
335
- s + [select { |r| r[options[:group]].eql?(g) }]
336
- }
337
- c = column_names - [options[:group]]
338
-
339
- end
340
- data.map! { |g|
341
- Ruport::Data::Table.new(
342
- :data => g.map { |x| x.reorder(*c) },
343
- :column_names => c
344
- )
345
- }
346
- rec = if options[:group].kind_of? Array
347
- Ruport::Data::Record.new(data,
348
- :attributes => group.map { |e| e.join("_") } )
380
+ # sub_table = table.sub_table(%w[d b]) { |r| r.a < 6 }
381
+ # sub_table == [[4,2],[8,6]].to_table(%w[b d]) #=> true
382
+ #
383
+ # Using just a block:
384
+ #
385
+ # sub_table = table.sub_table { |r| r.c > 10 }
386
+ # sub_table == [[9,10,11,12]].to_table(%w[a b c d]) #=> true
387
+ #
388
+ # FIXME: loses tags
389
+ def sub_table(columns=column_names,range=nil)
390
+ Table(columns) do |t|
391
+ if range
392
+ data[range].each { |r| t << r }
393
+ elsif block_given?
394
+ data.each { |r| t << r if yield(r) }
395
+ else
396
+ data.each { |r| t << r }
397
+ end
398
+ end
399
+ end
400
+
401
+ # returns an array of values for the given column_name
402
+ def column(name)
403
+ case(name)
404
+ when Integer
405
+ unless column_names.empty?
406
+ raise ArgumentError if name > column_names.length
407
+ end
349
408
  else
350
- Ruport::Data::Record.new data, :attributes => group
409
+ raise ArgumentError unless column_names.include?(name)
351
410
  end
352
- class << rec
353
- def each_group; attributes.each { |a| yield(a) }; end
354
- end; rec
411
+
412
+ map { |r| r[name] }
355
413
  end
356
414
 
357
415
  # Calculates sums. If a column name or index is given, it will try to
@@ -380,11 +438,12 @@ module Ruport::Data
380
438
  else
381
439
  s + yield(r)
382
440
  end
383
- }
441
+ }
384
442
  end
385
443
 
386
444
  alias_method :sum, :sigma
387
-
445
+
446
+ #
388
447
  # Returns a sorted table. If col_names is specified,
389
448
  # the block is ignored and the table is sorted by the named columns. All
390
449
  # options are used in constructing the new Table (see Array#to_table
@@ -424,9 +483,104 @@ module Ruport::Data
424
483
  table.tags = self.tags
425
484
  return table
426
485
  end
427
-
428
- end
429
486
 
487
+ def rows_with(columns,&block)
488
+ select { |r|
489
+ if block
490
+ block[*(columns.map { |c| r.get(c) })]
491
+ else
492
+ columns.all? { |k,v| r.get(k) == v }
493
+ end
494
+ }
495
+ end
496
+
497
+ # Create a copy of the Table: records will be copied as well.
498
+ #
499
+ # Example:
500
+ #
501
+ # one = Table.new :data => [1,2], [3,4],
502
+ # :column_names => %w[a b]
503
+ # two = one.dup
504
+ #
505
+ def dup
506
+ a = self.class.new(:data => @data, :column_names => @column_names)
507
+ a.tags = tags.dup
508
+ return a
509
+ end
510
+
511
+ #
512
+ # Uses Ruport's built-in text plugin to render this Table into a String
513
+ #
514
+ # Example:
515
+ #
516
+ # data = Table.new :data => [1,2], [3,4],
517
+ # :column_names => %w[a b]
518
+ # puts data.to_s
519
+ #
520
+ def to_s
521
+ as(:text)
522
+ end
523
+
524
+ # NOTE: does not respect tainted status
525
+ alias_method :clone, :dup
526
+
527
+
528
+ # Provides a shortcut for the <tt>as()</tt> method by converting a call to
529
+ # <tt>as(:format_name)</tt> into a call to <tt>to_format_name</tt>
530
+ def method_missing(id,*args)
531
+ return as($1.to_sym) if id.to_s =~ /^to_(.*)/
532
+ return rows_with($1.to_sym => args[0]) if id.to_s =~ /^rows_with_(.*)/
533
+ super
534
+ end
535
+
536
+ # Loads a CSV file directly into a Table using the FasterCSV library.
537
+ #
538
+ # Example:
539
+ #
540
+ # # treat first row as column_names
541
+ # table = Table.load('mydata.csv')
542
+ #
543
+ # # do not assume the data has column_names
544
+ # table = Table.load('mydata.csv',:has_names => false)
545
+ #
546
+ # # pass in FasterCSV options, such as column separators
547
+ # table = Table.load('mydata.csv',:csv_opts => { :col_sep => "\t" })
548
+ #
549
+ def self.load(csv_file, options={},&block)
550
+ get_table_from_csv(:foreach, csv_file, options,&block)
551
+ end
552
+
553
+ #
554
+ # Creates a Table from a CSV string using FasterCSV. See Table.load for
555
+ # additional examples.
556
+ #
557
+ # table = Table.parse("a,b,c\n1,2,3\n4,5,6\n")
558
+ #
559
+ def self.parse(string, options={},&block)
560
+ get_table_from_csv(:parse,string,options,&block)
561
+ end
562
+
563
+ private
564
+
565
+ def self.get_table_from_csv(msg,param,options={},&block) #:nodoc:
566
+ options = {:has_names => true,
567
+ :csv_options => {} }.merge(options)
568
+ require "fastercsv"
569
+ loaded_data = self.new
570
+
571
+ first_line = true
572
+ FasterCSV.send(msg,param,options[:csv_options]) do |row|
573
+ if first_line && options[:has_names]
574
+ loaded_data.column_names = row
575
+ first_line = false
576
+ elsif !block
577
+ loaded_data << row
578
+ else
579
+ block[loaded_data,row]
580
+ end
581
+ end ; loaded_data
582
+ end
583
+ end
430
584
  end
431
585
 
432
586
 
@@ -452,21 +606,20 @@ module Kernel
452
606
  table=
453
607
  case(args[0])
454
608
  when Array
455
- [].to_table(args[0])
609
+ opts = args[1] || {}
610
+ Ruport::Data::Table.new(f={:column_names => args[0]}.merge(opts))
456
611
  when /\.csv/
457
612
  Ruport::Data::Table.load(*args)
458
613
  else
459
614
  [].to_table(args)
460
- end
461
-
615
+ end
616
+
462
617
  block[table] if block
463
-
464
618
  return table
465
619
  end
466
- end
620
+ end
467
621
 
468
622
  class Array
469
-
470
623
  #
471
624
  # Converts an array to a Ruport::Data::Table object, ready to
472
625
  # use in your reports.
@@ -477,5 +630,5 @@ class Array
477
630
  def to_table(column_names=nil)
478
631
  Ruport::Data::Table.new({:data => self, :column_names => column_names})
479
632
  end
480
- end
481
633
 
634
+ end