ruport 0.8.14 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (102) hide show
  1. data/README +42 -107
  2. data/Rakefile +29 -32
  3. data/examples/centered_pdf_text_box.rb +13 -19
  4. data/examples/example.csv +3 -0
  5. data/examples/line_plotter.rb +15 -15
  6. data/examples/pdf_complex_report.rb +10 -23
  7. data/examples/pdf_table_with_title.rb +12 -12
  8. data/examples/rope_examples/itunes/Rakefile +22 -1
  9. data/examples/rope_examples/itunes/config/environment.rb +4 -0
  10. data/examples/rope_examples/itunes/lib/init.rb +32 -2
  11. data/examples/rope_examples/itunes/util/build +50 -16
  12. data/examples/rope_examples/sales_report/README +1 -1
  13. data/examples/rope_examples/sales_report/Rakefile +22 -1
  14. data/examples/rope_examples/sales_report/config/environment.rb +4 -0
  15. data/examples/rope_examples/sales_report/lib/init.rb +32 -2
  16. data/examples/rope_examples/sales_report/lib/reports/sales.rb +10 -16
  17. data/examples/rope_examples/sales_report/util/build +50 -16
  18. data/examples/row_renderer.rb +39 -0
  19. data/examples/ruport_list/png_embed.rb +61 -0
  20. data/examples/ruport_list/roadmap.png +0 -0
  21. data/examples/sample.rb +16 -0
  22. data/examples/simple_pdf_lines.rb +24 -0
  23. data/lib/ruport.rb +143 -57
  24. data/lib/ruport/acts_as_reportable.rb +246 -0
  25. data/lib/ruport/data.rb +1 -2
  26. data/lib/ruport/data/grouping.rb +311 -0
  27. data/lib/ruport/data/record.rb +113 -84
  28. data/lib/ruport/data/table.rb +275 -174
  29. data/lib/ruport/formatter.rb +149 -0
  30. data/lib/ruport/formatter/csv.rb +87 -0
  31. data/lib/ruport/formatter/html.rb +89 -0
  32. data/lib/ruport/formatter/pdf.rb +357 -0
  33. data/lib/ruport/formatter/text.rb +151 -0
  34. data/lib/ruport/generator.rb +127 -30
  35. data/lib/ruport/query.rb +46 -99
  36. data/lib/ruport/renderer.rb +238 -194
  37. data/lib/ruport/renderer/grouping.rb +67 -0
  38. data/lib/ruport/renderer/table.rb +25 -98
  39. data/lib/ruport/report.rb +45 -96
  40. data/test/acts_as_reportable_test.rb +229 -0
  41. data/test/csv_formatter_test.rb +97 -0
  42. data/test/{_test_database.rb → database_test_.rb} +0 -0
  43. data/test/grouping_test.rb +305 -0
  44. data/test/html_formatter_test.rb +104 -0
  45. data/test/pdf_formatter_test.rb +25 -0
  46. data/test/{test_query.rb → query_test.rb} +32 -121
  47. data/test/{test_record.rb → record_test.rb} +40 -23
  48. data/test/renderer_test.rb +344 -0
  49. data/test/{test_report.rb → report_test.rb} +74 -44
  50. data/test/samples/ticket_count.csv +124 -0
  51. data/test/{test_sql_split.rb → sql_split_test.rb} +0 -0
  52. data/test/{test_table.rb → table_test.rb} +255 -44
  53. data/test/text_formatter_test.rb +144 -0
  54. data/util/bench/data/record/bench_as_vs_to.rb +17 -0
  55. data/util/bench/data/record/bench_constructor.rb +46 -0
  56. data/util/bench/data/record/bench_indexing.rb +65 -0
  57. data/util/bench/data/record/bench_reorder.rb +35 -0
  58. data/util/bench/data/record/bench_to_a.rb +19 -0
  59. data/util/bench/data/table/bench_column_manip.rb +103 -0
  60. data/util/bench/data/table/bench_dup.rb +24 -0
  61. data/util/bench/data/table/bench_init.rb +67 -0
  62. data/util/bench/data/table/bench_manip.rb +125 -0
  63. data/util/bench/formatter/bench_csv.rb +14 -0
  64. data/util/bench/formatter/bench_html.rb +14 -0
  65. data/util/bench/formatter/bench_pdf.rb +14 -0
  66. data/util/bench/formatter/bench_text.rb +14 -0
  67. data/util/bench/samples/tattle.csv +1237 -0
  68. metadata +121 -143
  69. data/TODO +0 -21
  70. data/examples/invoice.rb +0 -142
  71. data/examples/invoice_report.rb +0 -29
  72. data/examples/line_graph.rb +0 -38
  73. data/examples/rope_examples/itunes/config/ruport_config.rb +0 -8
  74. data/examples/rope_examples/sales_report/config/ruport_config.rb +0 -8
  75. data/lib/ruport/attempt.rb +0 -63
  76. data/lib/ruport/config.rb +0 -204
  77. data/lib/ruport/data/groupable.rb +0 -93
  78. data/lib/ruport/data/taggable.rb +0 -80
  79. data/lib/ruport/format.rb +0 -1
  80. data/lib/ruport/format/csv.rb +0 -29
  81. data/lib/ruport/format/html.rb +0 -42
  82. data/lib/ruport/format/latex.rb +0 -47
  83. data/lib/ruport/format/pdf.rb +0 -233
  84. data/lib/ruport/format/plugin.rb +0 -31
  85. data/lib/ruport/format/svg.rb +0 -60
  86. data/lib/ruport/format/text.rb +0 -103
  87. data/lib/ruport/format/xml.rb +0 -32
  88. data/lib/ruport/layout.rb +0 -1
  89. data/lib/ruport/layout/component.rb +0 -7
  90. data/lib/ruport/mailer.rb +0 -99
  91. data/lib/ruport/renderer/graph.rb +0 -46
  92. data/lib/ruport/report/graph.rb +0 -14
  93. data/lib/ruport/system_extensions.rb +0 -71
  94. data/test/test_config.rb +0 -88
  95. data/test/test_format_text.rb +0 -63
  96. data/test/test_graph_renderer.rb +0 -97
  97. data/test/test_groupable.rb +0 -56
  98. data/test/test_mailer.rb +0 -170
  99. data/test/test_renderer.rb +0 -151
  100. data/test/test_ruport.rb +0 -58
  101. data/test/test_table_renderer.rb +0 -141
  102. data/test/test_taggable.rb +0 -52
@@ -5,40 +5,7 @@
5
5
  # Copyright 2006 by respective content owners, all rights reserved.
6
6
 
7
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
18
8
 
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
40
-
41
- end
42
9
 
43
10
  # === Overview
44
11
  #
@@ -53,12 +20,100 @@ module Ruport::Data
53
20
  # to suit your needs, then used to build a report.
54
21
  #
55
22
  class Table
56
- include Collection
57
- include Groupable
58
-
59
- require "forwardable"
60
- extend Forwardable
61
23
 
24
+ module FromCSV
25
+ # Loads a CSV file directly into a Table using the FasterCSV library.
26
+ #
27
+ # Example:
28
+ #
29
+ # # treat first row as column_names
30
+ # table = Table.load('mydata.csv')
31
+ #
32
+ # # do not assume the data has column_names
33
+ # table = Table.load('mydata.csv',:has_names => false)
34
+ #
35
+ # # pass in FasterCSV options, such as column separators
36
+ # table = Table.load('mydata.csv',:csv_opts => { :col_sep => "\t" })
37
+ #
38
+ def load(csv_file, options={},&block)
39
+ get_table_from_csv(:foreach, csv_file, options,&block)
40
+ end
41
+
42
+ # Creates a Table from a CSV string using FasterCSV. See Table.load for
43
+ # additional examples.
44
+ #
45
+ # table = Table.parse("a,b,c\n1,2,3\n4,5,6\n")
46
+ #
47
+ def parse(string, options={},&block)
48
+ get_table_from_csv(:parse,string,options,&block)
49
+ end
50
+
51
+ private
52
+
53
+ def get_table_from_csv(msg,param,options={},&block) #:nodoc:
54
+ require "fastercsv"
55
+
56
+ options = {:has_names => true,
57
+ :csv_options => {} }.merge(options)
58
+
59
+ # if people want to use FCSV's header support, let them
60
+ adjust_options_for_fcsv_headers(options)
61
+
62
+ loaded_data = self.new(options)
63
+
64
+ first_line = true
65
+ FasterCSV.send(msg,param,options[:csv_options]) do |row|
66
+
67
+ if first_line
68
+ adjust_for_headers(loaded_data,row,options)
69
+ first_line = false
70
+ next if options[:has_names]
71
+ end
72
+
73
+ if block
74
+ handle_csv_row_proc(loaded_data,row,options,block)
75
+ else
76
+ loaded_data << row
77
+ end
78
+
79
+ end
80
+
81
+ return loaded_data
82
+ end
83
+
84
+ def handle_csv_row_proc(data,row,options,block)
85
+ if options[:records]
86
+ row = Record.new(row, :attributes => data.column_names)
87
+ end
88
+
89
+ block[data,row]
90
+ end
91
+
92
+ def adjust_options_for_fcsv_headers(options)
93
+ options[:has_names] = false if options[:csv_options][:headers]
94
+ end
95
+
96
+ def adjust_for_headers(loaded,row,options)
97
+ if options[:has_names]
98
+ loaded.column_names = row
99
+ elsif options[:csv_options][:headers]
100
+ loaded.column_names = row.headers
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+
107
+ include Enumerable
108
+ extend FromCSV
109
+
110
+ include Ruport::Renderer::Hooks
111
+ renders_as_table
112
+
113
+ def self.inherited(base)
114
+ base.renders_as_table
115
+ end
116
+
62
117
  # Creates a new table based on the supplied options.
63
118
  # Valid options:
64
119
  # <b><tt>:data</tt></b>:: An Array of Arrays representing the
@@ -76,22 +131,28 @@ module Ruport::Data
76
131
  options[:record_class].name || "Ruport::Data::Record"
77
132
  @data = []
78
133
  if options[:data]
79
- if options[:data].all? { |r| r.kind_of? Record }
80
- record_tags = options[:data].map { |r| r.tags.dup }
81
- options[:data] = options[:data].map { |r| r.to_a }
134
+ if options[:data].all? { |r| r.kind_of? Record }
135
+ options[:data] = options[:data].map { |r|
136
+ if @column_names.empty? or
137
+ r.attributes.all? { |a| a.kind_of?(Numeric) }
138
+ r.to_a
139
+ else
140
+ r.to_hash
141
+ end
142
+ }
82
143
  end
83
144
  options[:data].each { |e| self << e }
84
- each { |r| r.tags = record_tags.shift } if record_tags
85
145
  end
86
146
  end
87
147
 
88
148
  # This Table's column names.
89
149
  attr_reader :column_names
90
-
91
- attr_reader :data
150
+ attr_reader :data
151
+
152
+ require "forwardable"
153
+ extend Forwardable
92
154
  def_delegators :@data, :each, :length, :size, :empty?, :[]
93
155
 
94
-
95
156
  # Sets the column names for this table. <tt>new_column_names</tt> should
96
157
  # be an array listing the names of the columns.
97
158
  #
@@ -115,11 +176,10 @@ module Ruport::Data
115
176
  end
116
177
  }
117
178
  r.send(:reindex, @column_names)
118
- }
119
- end
179
+ }
180
+ end
120
181
  end
121
182
 
122
-
123
183
  # Compares this Table to another Table and returns <tt>true</tt> if
124
184
  # both the <tt>data</tt> and <tt>column_names</tt> are equal.
125
185
  #
@@ -140,7 +200,8 @@ module Ruport::Data
140
200
  alias_method :==, :eql?
141
201
 
142
202
  # Used to add extra data to the Table. <tt>other</tt> can be an Array,
143
- # Hash or Record.
203
+ # Hash or Record. It also can be anything that implements a meaningful
204
+ # to_hash or to_ary
144
205
  #
145
206
  # Example:
146
207
  #
@@ -150,28 +211,21 @@ module Ruport::Data
150
211
  # data << { :a => 4, :b => 5}
151
212
  # data << Record.new [5,6], :attributes => %w[a b]
152
213
  #
153
- def <<(other)
154
- case other
214
+ def <<(row)
215
+ case row
155
216
  when Array
156
- attributes = @column_names.empty? ? nil : @column_names
157
- @data << record_class.new(other, :attributes => attributes)
217
+ append_array(row)
158
218
  when Hash
159
- raise ArgumentError unless @column_names
160
- arr = @column_names.map { |k| other[k] }
161
- @data << record_class.new(arr, :attributes => @column_names)
219
+ append_hash(row)
162
220
  when record_class
163
- if column_names.empty?
164
- self << other.to_a
165
- else
166
- self << other.to_h
167
- end
168
- @data.last.tags = other.tags.dup
221
+ append_record(row)
169
222
  else
170
- raise ArgumentError
171
- end
172
- self
173
- end
223
+ append_hash(row) rescue append_array(row)
224
+ end
225
+ return self
226
+ end
174
227
 
228
+ # returns the record class constant being used by the table
175
229
  def record_class
176
230
  @record_class.split("::").inject(Class) { |c,el| c.send(:const_get,el) }
177
231
  end
@@ -199,34 +253,24 @@ module Ruport::Data
199
253
  end
200
254
 
201
255
 
202
- # Reorders the table's columns.
203
- #
204
- # Example:
205
- #
206
- # one = Table.new :data => [[1,2], [3,4]],
207
- # :column_names => %w[a b]
208
- #
209
- # two = one.reorder([1,0])
210
- #
211
256
  def reorder(*indices)
257
+ raise(ArgumentError,"Can't reorder without column names set!") if
258
+ @column_names.empty?
259
+
212
260
  indices = indices[0] if indices[0].kind_of? Array
213
261
 
214
262
  if indices.all? { |i| i.kind_of? Integer }
215
263
  indices.map! { |i| @column_names[i] }
216
- end
264
+ end
217
265
 
218
- @column_names = indices
219
-
220
- @data.each { |r|
221
- r.attributes = @column_names
222
- }; self
266
+ reduce(indices)
223
267
  end
224
268
 
225
269
 
226
270
  # Adds an extra column to the Table. Available Options:
227
271
  #
228
- # <b><tt>:fill</tt></b>:: The default value to use for the column in
229
- # existing rows. Set to nil if not specified.
272
+ # <b><tt>:default</tt></b>:: The default value to use for the column in
273
+ # existing rows. Set to nil if not specified.
230
274
  #
231
275
  # <b><tt>:position</tt></b>:: Inserts the column at the indicated position
232
276
  # number.
@@ -272,6 +316,15 @@ module Ruport::Data
272
316
  else
273
317
  each { |r| r[name] = options[:default] }
274
318
  end; self
319
+ end
320
+
321
+ def add_columns(names,options={})
322
+ raise "Greg isn't smart enough to figure this out.\n"+
323
+ "Send ideas in at http://list.rubyreports.org" if block_given?
324
+ need_reverse = !!(options[:after] || options[:position])
325
+ names = names.reverse if need_reverse
326
+ names.each { |n| add_column(n,options) }
327
+ self
275
328
  end
276
329
 
277
330
  # Removes the given column from the table. May use name or position.
@@ -280,6 +333,7 @@ module Ruport::Data
280
333
  #
281
334
  # table.remove_column(0) #=> removes the first column
282
335
  # table.remove_column("apple") #=> removes column named apple
336
+ #
283
337
  def remove_column(col)
284
338
  col = column_names[col] if col.kind_of? Fixnum
285
339
  column_names.delete(col)
@@ -292,6 +346,7 @@ module Ruport::Data
292
346
  # Example:
293
347
  # table.remove_columns('a','b','c')
294
348
  # table.remove_columns([0,1])
349
+ #
295
350
  def remove_columns(*cols)
296
351
  cols = cols[0] if cols[0].kind_of? Array
297
352
  cols.each { |col| remove_column(col) }
@@ -306,11 +361,46 @@ module Ruport::Data
306
361
  # new_values = table.map { |r| r.zanzibar }
307
362
  # old_values == new_values #=> true
308
363
  # table.column_names.include?("a") #=> false
364
+ #
309
365
  def rename_column(old_name,new_name)
310
366
  self.column_names[column_names.index(old_name)] = new_name
311
367
  each { |r| r.rename_attribute(old_name,new_name,false)}
312
368
  end
313
-
369
+
370
+ # Renames multiple columns. Takes either a hash of "old" => "new"
371
+ # names or two arrays of names %w[old names],%w[new names].
372
+ #
373
+ # Example:
374
+ #
375
+ # table.column_names #=> ["a", "b"]
376
+ # table.rename_columns ["a", "b"], ["c", "d"]
377
+ # table.column_names #=> ["c", "d"]
378
+ #
379
+ # table.column_names #=> ["a", "b"]
380
+ # table.rename_columns {"a" => "c", "b" => "d"}
381
+ # table.column_names #=> ["c", "d"]
382
+ #
383
+ def rename_columns(old_cols=nil,new_cols=nil)
384
+ if block_given?
385
+ if old_cols
386
+ old_cols.each { |c| rename_column(c,yield(c)) }
387
+ else
388
+ column_names.each { |c| rename_column(c,yield(c)) }
389
+ end
390
+ return
391
+ end
392
+
393
+ raise ArgumentError unless old_cols
394
+
395
+ if new_cols
396
+ raise ArgumentError,
397
+ "odd number of arguments" unless old_cols.size == new_cols.size
398
+ h = Hash[*old_cols.zip(new_cols).flatten]
399
+ else
400
+ h = old_cols
401
+ end
402
+ h.each {|old,new| rename_column(old,new) }
403
+ end
314
404
 
315
405
  # Exchanges one column with another.
316
406
  #
@@ -332,6 +422,7 @@ module Ruport::Data
332
422
  # | 3 | 2 | 1 |
333
423
  # | 6 | 5 | 4 |
334
424
  # +-----------+
425
+ #
335
426
  def swap_column(a,b)
336
427
  if [a,b].all? { |r| r.kind_of? Fixnum }
337
428
  col_a,col_b = column_names[a],column_names[b]
@@ -359,9 +450,14 @@ module Ruport::Data
359
450
  # | 1 | 2 | 7 |
360
451
  # | 4 | 5 | 16 |
361
452
  # +------------+
362
- def replace_column(old_col,new_col,&block)
363
- add_column(new_col,:after => old_col,&block)
364
- remove_column(old_col)
453
+ #
454
+ def replace_column(old_col,new_col=nil,&block)
455
+ if new_col
456
+ add_column(new_col,:after => old_col,&block)
457
+ remove_column(old_col)
458
+ else
459
+ each { |r| r[old_col] = yield(r) }
460
+ end
365
461
  end
366
462
 
367
463
  # Generates a sub table
@@ -383,39 +479,54 @@ module Ruport::Data
383
479
  # Using column_names and a block:
384
480
  #
385
481
  # sub_table = table.sub_table(%w[d b]) { |r| r.a < 6 }
386
- # sub_table == [[4,2],[8,6]].to_table(%w[d b]) #=> true
482
+ # sub_table == [[4,2],[8,6]].to_table(%w[d b]) #=> true
483
+ #
484
+ # Using a range for row reduction:
485
+ # sub_table = table.sub_table(1..-1)
486
+ # sub_table == [[5,6,7,8],[9,10,11,12]].to_table(%w[a b c d]) #=> true
387
487
  #
388
488
  # Using just a block:
389
489
  #
390
490
  # sub_table = table.sub_table { |r| r.c > 10 }
391
491
  # sub_table == [[9,10,11,12]].to_table(%w[a b c d]) #=> true
392
492
  #
393
- # FIXME: loses tags
394
- def sub_table(columns=column_names,range=nil)
395
- t = self.class.new(:column_names => columns,
396
- :record_class => record_class)
397
- if range
398
- data[range].each { |r| t << r }
399
- elsif block_given?
400
- data.each { |r| t << r if yield(r) }
401
- else
402
- data.each { |r| t << r }
403
- end
404
- return t
493
+ def sub_table(cor=column_names,range=nil,&block)
494
+ if range
495
+ self.class.new(:column_names => cor,:data => data[range])
496
+ elsif cor.kind_of?(Range)
497
+ self.class.new(:column_names => column_names,:data => data[cor])
498
+ elsif block
499
+ self.class.new( :column_names => cor, :data => data.select(&block))
500
+ else
501
+ self.class.new( :column_names => cor, :data => data)
502
+ end
503
+ end
504
+
505
+ # Generates a sub table in place, modifying the receiver. See documentation
506
+ # for <tt>sub_table</tt>.
507
+ #
508
+ def reduce(columns=column_names,range=nil,&block)
509
+ t = sub_table(columns,range,&block)
510
+ @data = t.data
511
+ @column_names = t.column_names
512
+ self
405
513
  end
514
+
515
+ alias_method :sub_table!, :reduce
406
516
 
407
- # returns an array of values for the given column name
517
+ # Returns an array of values for the given column name.
518
+ #
408
519
  def column(name)
409
520
  case(name)
410
521
  when Integer
411
- unless column_names.empty?
522
+ unless column_names.empty?
412
523
  raise ArgumentError if name > column_names.length
413
524
  end
414
525
  else
415
526
  raise ArgumentError unless column_names.include?(name)
416
527
  end
417
528
 
418
- map { |r| r[name] }
529
+ map { |r| r[name] }
419
530
  end
420
531
 
421
532
  # Calculates sums. If a column name or index is given, it will try to
@@ -436,10 +547,10 @@ module Ruport::Data
436
547
  def sigma(column=nil)
437
548
  inject(0) { |s,r|
438
549
  if column
439
- s + if r[column].kind_of? Numeric
440
- r[column]
550
+ s + if r.get(column).kind_of? Numeric
551
+ r.get(column)
441
552
  else
442
- r[column] =~ /\./ ? r[column].to_f : r[column].to_i
553
+ r.get(column) =~ /\./ ? r.get(column).to_f : r.get(column).to_i
443
554
  end
444
555
  else
445
556
  s + yield(r)
@@ -449,7 +560,6 @@ module Ruport::Data
449
560
 
450
561
  alias_method :sum, :sigma
451
562
 
452
- #
453
563
  # Returns a sorted table. If col_names is specified,
454
564
  # the block is ignored and the table is sorted by the named columns. All
455
565
  # options are used in constructing the new Table (see Array#to_table
@@ -477,7 +587,7 @@ module Ruport::Data
477
587
  if col_names
478
588
  sort_by do |r|
479
589
  stabilizer += 1
480
- [col_names.map {|col| r[col]}, stabilizer]
590
+ [Array(col_names).map {|col| r[col]}, stabilizer]
481
591
  end
482
592
  else
483
593
  sort_by(&block)
@@ -485,12 +595,17 @@ module Ruport::Data
485
595
 
486
596
  table = self.class.new( :data => data_array,
487
597
  :column_names => @column_names,
488
- :record_class => record_class )
598
+ :record_class => record_class )
489
599
 
490
- table.tags = self.tags
491
600
  return table
601
+ end
602
+
603
+ # same as Table#sort_rows_by, but self modifiying
604
+ def sort_rows_by!(col_names=nil,&block)
605
+ table = sort_rows_by(col_names,&block)
606
+ @data = table.data
492
607
  end
493
-
608
+
494
609
  def rows_with(columns,&block)
495
610
  select { |r|
496
611
  if block
@@ -509,19 +624,14 @@ module Ruport::Data
509
624
  # :column_names => %w[a b]
510
625
  # two = one.dup
511
626
  #
512
- def dup
513
- a = self.class.new( :data => @data,
514
- :column_names => @column_names,
515
- :record_class => record_class )
516
- a.tags = tags.dup
517
- return a
518
- end
519
-
520
- # NOTE: does not respect tainted status
521
- alias_method :clone, :dup
627
+ def initialize_copy(from)
628
+ @record_class = from.record_class.name
629
+ @column_names = from.column_names.dup
630
+ @data = []
631
+ from.data.each { |r| self << r.dup }
632
+ end
522
633
 
523
- #
524
- # Uses Ruport's built-in text plugin to render this Table into a String
634
+ # Uses Ruport's built-in text formatter to render this Table into a String
525
635
  #
526
636
  # Example:
527
637
  #
@@ -533,61 +643,45 @@ module Ruport::Data
533
643
  as(:text)
534
644
  end
535
645
 
646
+ # Convert the Table into a Group using the supplied group name.
647
+ #
648
+ # data = Table.new :data => [[1,2], [3,4]],
649
+ # :column_names => %w[a b]
650
+ # group = data.to_group("my_group")
651
+ #
652
+ def to_group(name=nil)
653
+ Group.new( :data => data,
654
+ :column_names => column_names,
655
+ :name => name,
656
+ :record_class => record_class )
657
+ end
658
+
659
+ # NOTE: does not respect tainted status
660
+ alias_method :clone, :dup
661
+
536
662
  # Provides a shortcut for the <tt>as()</tt> method by converting a call to
537
663
  # <tt>as(:format_name)</tt> into a call to <tt>to_format_name</tt>
538
- def method_missing(id,*args)
539
- return as($1.to_sym) if id.to_s =~ /^to_(.*)/
664
+ def method_missing(id,*args,&block)
665
+ return as($1.to_sym,*args,&block) if id.to_s =~ /^to_(.*)/
540
666
  return rows_with($1.to_sym => args[0]) if id.to_s =~ /^rows_with_(.*)/
541
667
  super
542
668
  end
543
669
 
544
- # Loads a CSV file directly into a Table using the FasterCSV library.
545
- #
546
- # Example:
547
- #
548
- # # treat first row as column_names
549
- # table = Table.load('mydata.csv')
550
- #
551
- # # do not assume the data has column_names
552
- # table = Table.load('mydata.csv',:has_names => false)
553
- #
554
- # # pass in FasterCSV options, such as column separators
555
- # table = Table.load('mydata.csv',:csv_opts => { :col_sep => "\t" })
556
- #
557
- def self.load(csv_file, options={},&block)
558
- get_table_from_csv(:foreach, csv_file, options,&block)
559
- end
670
+ def append_array(array)
671
+ attributes = @column_names.empty? ? nil : @column_names
672
+ @data << record_class.new(array.to_ary, :attributes => attributes)
673
+ end
560
674
 
561
- #
562
- # Creates a Table from a CSV string using FasterCSV. See Table.load for
563
- # additional examples.
564
- #
565
- # table = Table.parse("a,b,c\n1,2,3\n4,5,6\n")
566
- #
567
- def self.parse(string, options={},&block)
568
- get_table_from_csv(:parse,string,options,&block)
675
+ def append_hash(hash_obj)
676
+ hash_obj = hash_obj.to_hash
677
+ raise ArgumentError unless @column_names
678
+ @data << record_class.new(hash_obj, :attributes => @column_names)
569
679
  end
570
-
571
- private
572
680
 
573
- def self.get_table_from_csv(msg,param,options={},&block) #:nodoc:
574
- options = {:has_names => true,
575
- :csv_options => {} }.merge(options)
576
- require "fastercsv"
577
- loaded_data = self.new
578
-
579
- first_line = true
580
- FasterCSV.send(msg,param,options[:csv_options]) do |row|
581
- if first_line && options[:has_names]
582
- loaded_data.column_names = row
583
- first_line = false
584
- elsif !block
585
- loaded_data << row
586
- else
587
- block[loaded_data,row]
588
- end
589
- end ; loaded_data
590
- end
681
+ def append_record(record)
682
+ self << record.send(column_names.empty? ? :to_a : :to_hash)
683
+ end
684
+
591
685
  end
592
686
  end
593
687
 
@@ -605,8 +699,7 @@ module Kernel
605
699
  # t = Table(%w[a b c]) { |t| t << [1,2,3] }
606
700
  #
607
701
  # # allows loading table from CSV
608
- # # accepts all Data::Table.load options, but block form yields table,
609
- # # not row!
702
+ # # accepts all Data::Table.load options, including block (yields table,row)
610
703
  #
611
704
  # t = Table("foo.csv")
612
705
  # t = Table("bar.csv", :has_names => false)
@@ -617,9 +710,17 @@ module Kernel
617
710
  opts = args[1] || {}
618
711
  Ruport::Data::Table.new(f={:column_names => args[0]}.merge(opts))
619
712
  when /\.csv/
620
- Ruport::Data::Table.load(*args)
713
+ return Ruport::Data::Table.load(*args,&block)
714
+ when Hash
715
+ if file = args[0].delete(:file)
716
+ return Ruport::Data::Table.load(file,args[0],&block)
717
+ elsif string = args[0].delete(:string)
718
+ return Ruport::Data::Table.parse(string,args[0],&block)
719
+ else
720
+ return Ruport::Data::Table.new(args[0])
721
+ end
621
722
  else
622
- Ruport::Data::Table.new(:data => [],:column_names => args)
723
+ Ruport::Data::Table.new(:data => [], :column_names => args)
623
724
  end
624
725
 
625
726
  block[table] if block
@@ -628,7 +729,7 @@ module Kernel
628
729
  end
629
730
 
630
731
  class Array
631
- #
732
+
632
733
  # Converts an array to a Ruport::Data::Table object, ready to
633
734
  # use in your reports.
634
735
  #