ruport 1.0.2 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -51,16 +51,7 @@ module Ruport::Data
51
51
  base.renders_as_group
52
52
  end
53
53
 
54
- # Create a copy of the Group. Records will be copied as well.
55
- #
56
- # Example:
57
- #
58
- # one = Group.new :name => 'test',
59
- # :data => [[1,2], [3,4]],
60
- # :column_names => %w[a b]
61
- # two = one.dup
62
- #
63
- def initialize_copy(from)
54
+ def initialize_copy(from) #:nodoc:
64
55
  super
65
56
  @name = from.name
66
57
  @subgroups = from.subgroups.inject({}) { |h,d|
@@ -149,20 +140,34 @@ module Ruport::Data
149
140
  #
150
141
  # Valid options:
151
142
  # <b><tt>:by</tt></b>:: A column name or array of column names that the
152
- # data will be grouped on.
143
+ # data will be grouped on.
144
+ # <b><tt>:order</tt></b>:: Determines the iteration and presentation order
145
+ # of a Grouping object. Set to :name to order by
146
+ # Group names. You can also provide a lambda which
147
+ # will be passed Group objects, and use semantics
148
+ # similar to Enumerable#group_by
153
149
  #
154
- # Example:
155
- #
156
- # table = [[1,2,3],[4,5,6]].to_table(%w[a b c])
150
+ # Examples:
157
151
  #
152
+ # table = [[1,2,3],[4,5,6],[1,1,2]].to_table(%w[a b c])
153
+ #
154
+ # # unordered
158
155
  # grouping = Grouping.new(table, :by => "a")
156
+ #
157
+ # # ordered by group name
158
+ # grouping = Grouping.new(table, :by => "a", :order => :name)
159
159
  #
160
+ # # ordered by group size
161
+ # grouping = Grouping.new(table, :by => "a",
162
+ # :order => lambda { |g| g.size } )
160
163
  def initialize(data={},options={})
161
164
  if data.kind_of?(Hash)
162
- @grouped_by = data[:by]
165
+ @grouped_by = data[:by]
166
+ @order = data[:order]
163
167
  @data = {}
164
168
  else
165
- @grouped_by = options[:by]
169
+ @grouped_by = options[:by]
170
+ @order = options[:order]
166
171
  cols = Array(options[:by]).dup
167
172
  @data = data.to_group.send(:grouped_data, cols.shift)
168
173
  cols.each do |col|
@@ -192,9 +197,35 @@ module Ruport::Data
192
197
 
193
198
  # Iterates through the Grouping, yielding each group name and Group object
194
199
  #
195
- def each
196
- @data.each { |name,group| yield(name,group) }
197
- end
200
+ def each
201
+ if @order.respond_to?(:call)
202
+ @data.sort_by { |n,g| @order[g] }.each { |n,g| yield(n,g) }
203
+ elsif @order == :name
204
+ @data.sort_by { |n,g| n }.each { |name,group| yield(name,group) }
205
+ else
206
+ @data.each { |name,group| yield(name,group) }
207
+ end
208
+ end
209
+
210
+
211
+ # Returns a new grouping with the specified sort order.
212
+ # You can sort by Group name or an arbitrary block
213
+ #
214
+ # by_name = grouping.sort_grouping_by(:name)
215
+ # by_size = grouping.sort_grouping_by { |g| g.size }
216
+ def sort_grouping_by(type=nil,&block)
217
+ a = Grouping.new(:by => @grouped_by, :order => type || block)
218
+ each { |n,g| a << g }
219
+ return a
220
+ end
221
+
222
+ # Applies the specified sort order to an existing Grouping object.
223
+ #
224
+ # grouping.sort_grouping_by!(:name)
225
+ # grouping.sort_grouping_by! { |g| g.size }
226
+ def sort_grouping_by!(type=nil,&block)
227
+ @order = type || block
228
+ end
198
229
 
199
230
  # Used to add extra data to the Grouping. <tt>group</tt> should be a Group.
200
231
  #
@@ -217,7 +248,7 @@ module Ruport::Data
217
248
  @data.merge!({ group.name => group })
218
249
  end
219
250
 
220
- alias_method :append, :<<
251
+ alias_method :append, :<<
221
252
 
222
253
  # Provides access to the subgroups of a particular group in the Grouping.
223
254
  # Supply the name of a group and it returns a Grouping created from the
@@ -259,7 +290,7 @@ module Ruport::Data
259
290
  s.merge(r[0] => r[1].call(group))
260
291
  end
261
292
  end
262
- t.reorder(cols)
293
+ t.data.reorder(cols)
263
294
  }
264
295
  end
265
296
 
@@ -277,6 +308,37 @@ module Ruport::Data
277
308
  as(:text)
278
309
  end
279
310
 
311
+ # Calculates sums. If a column name or index is given, it will try to
312
+ # convert each element of that column to an integer or float
313
+ # and add them together. The sum is calculated across all groups in
314
+ # the grouping.
315
+ #
316
+ # If a block is given, it yields each Record in each Group so that you can
317
+ # do your own calculation.
318
+ #
319
+ # Example:
320
+ #
321
+ # table = [[1,2,3],[3,4,5],[5,6,7]].to_table(%w[col1 col2 col3])
322
+ # grouping = Grouping(table, :by => "col1")
323
+ # grouping.sigma("col2") #=> 12
324
+ # grouping.sigma(0) #=> 12
325
+ # grouping.sigma { |r| r.col2 + r.col3 } #=> 27
326
+ # grouping.sigma { |r| r.col2 + 1 } #=> 15
327
+ #
328
+ def sigma(column=nil)
329
+ inject(0) do |s, (group_name, group)|
330
+ if column
331
+ s + group.sigma(column)
332
+ else
333
+ s + group.sigma do |r|
334
+ yield(r)
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ alias_method :sum, :sigma
341
+
280
342
  include Ruport::Renderer::Hooks
281
343
  renders_as_grouping
282
344
 
@@ -293,7 +355,7 @@ module Ruport::Data
293
355
  #
294
356
  # two = one.dup
295
357
  #
296
- def initialize_copy(from)
358
+ def initialize_copy(from) #:nodoc:
297
359
  @grouped_by = from.grouped_by
298
360
  @data = from.data.inject({}) { |h,d| h.merge!({ d[0] => d[1].dup }) }
299
361
  end
@@ -252,7 +252,7 @@ module Ruport::Data
252
252
  # one = Record.new([1,2,3,4],:attributes => %w[a b c d])
253
253
  # two = one.dup
254
254
  #
255
- def initialize_copy(from)
255
+ def initialize_copy(from) #:nodoc:
256
256
  @data = from.data.dup
257
257
  @attributes = from.attributes.dup
258
258
  end
@@ -22,7 +22,7 @@ module Ruport::Data
22
22
  # Once your data is in a Table object, it can be manipulated
23
23
  # to suit your needs, then used to build a report.
24
24
  #
25
- class Table
25
+ class Table
26
26
 
27
27
  # === Overview
28
28
  #
@@ -51,7 +51,7 @@ module Ruport::Data
51
51
  #
52
52
  # table = Table.parse("a,b,c\n1,2,3\n4,5,6\n")
53
53
  #
54
- def parse(string, options={},&block)
54
+ def parse(string, options={},&block)
55
55
  get_table_from_csv(:parse,string,options,&block)
56
56
  end
57
57
 
@@ -66,35 +66,33 @@ module Ruport::Data
66
66
  # if people want to use FCSV's header support, let them
67
67
  adjust_options_for_fcsv_headers(options)
68
68
 
69
- loaded_data = self.new(options)
70
-
71
- first_line = true
72
- FasterCSV.send(msg,param,options[:csv_options]) do |row|
73
-
74
- if first_line
75
- adjust_for_headers(loaded_data,row,options)
76
- first_line = false
77
- next if options[:has_names]
78
- end
79
-
80
- if block
81
- handle_csv_row_proc(loaded_data,row,options,block)
82
- else
83
- loaded_data << row
69
+ table = self.new(options) do |feeder|
70
+ first_line = true
71
+ FasterCSV.send(msg,param,options[:csv_options]) do |row|
72
+ if first_line
73
+ adjust_for_headers(feeder.data,row,options)
74
+ first_line = false
75
+ next if options[:has_names]
76
+ end
77
+
78
+ if block
79
+ handle_csv_row_proc(feeder,row,options,block)
80
+ else
81
+ feeder << row
82
+ end
84
83
  end
85
-
86
84
  end
87
-
88
- return loaded_data
85
+
86
+ return table
89
87
  end
90
88
 
91
- def handle_csv_row_proc(data,row,options,block)
89
+ def handle_csv_row_proc(feeder,row,options,block)
92
90
  if options[:records]
93
91
  rc = options[:record_class] || Record
94
- row = rc.new(row, :attributes => data.column_names)
92
+ row = rc.new(row, :attributes => feeder.data.column_names)
95
93
  end
96
94
 
97
- block[data,row]
95
+ block[feeder,row]
98
96
  end
99
97
 
100
98
  def adjust_options_for_fcsv_headers(options)
@@ -127,6 +125,15 @@ module Ruport::Data
127
125
  # records in this Table.
128
126
  # <b><tt>:column_names</tt></b>:: An Array containing the column names
129
127
  # for this Table.
128
+ # <b><tt>:filters</tt></b>:: A proc or array of procs that set up
129
+ # conditions to filter the data being
130
+ # added to the table.
131
+ # <b><tt>:transforms</tt></b>:: A proc or array of procs that perform
132
+ # transformations on the data being added
133
+ # to the table.
134
+ # <b><tt>:record_class</tt></b>:: Specify the class of the table's
135
+ # records.
136
+ #
130
137
  # Example:
131
138
  #
132
139
  # table = Table.new :data => [[1,2,3], [3,4,5]],
@@ -136,20 +143,30 @@ module Ruport::Data
136
143
  @column_names = options[:column_names] ? options[:column_names].dup : []
137
144
  @record_class = options[:record_class] &&
138
145
  options[:record_class].name || "Ruport::Data::Record"
139
- @data = []
146
+ @data = []
147
+
148
+ feeder = Feeder.new(self)
149
+
150
+ Array(options[:filters]).each { |f| feeder.filter(&f) }
151
+ Array(options[:transforms]).each { |t| feeder.transform(&t) }
152
+
140
153
  if options[:data]
141
- if options[:data].all? { |r| r.kind_of? Record }
142
- options[:data] = options[:data].map { |r|
143
- if @column_names.empty? or
144
- r.attributes.all? { |a| a.kind_of?(Numeric) }
145
- r.to_a
154
+ options[:data].each do |e|
155
+ if e.kind_of?(Record)
156
+ e = if @column_names.empty? or
157
+ e.attributes.all? { |a| a.kind_of?(Numeric) }
158
+ e.to_a
146
159
  else
147
- r.to_hash.values_at(*@column_names)
160
+ e.to_hash.values_at(*@column_names)
148
161
  end
149
- }
150
- end
151
- options[:data].each { |e| self << e }
152
- end
162
+ end
163
+ r = recordize(e)
164
+
165
+ feeder << r
166
+ end
167
+ end
168
+
169
+ yield(feeder) if block_given?
153
170
  end
154
171
 
155
172
  # This Table's column names
@@ -221,17 +238,8 @@ module Ruport::Data
221
238
  # data << Record.new [5,6], :attributes => %w[a b]
222
239
  #
223
240
  def <<(row)
224
- case row
225
- when Array
226
- append_array(row)
227
- when Hash
228
- append_hash(row)
229
- when record_class
230
- append_record(row)
231
- else
232
- append_hash(row) rescue append_array(row)
233
- end
234
- return self
241
+ @data << recordize(row)
242
+ return self
235
243
  end
236
244
 
237
245
  # Returns the record class constant being used by the table.
@@ -323,7 +331,7 @@ module Ruport::Data
323
331
  # # new column built via a block, added at the end of the table
324
332
  # data.add_column("new_col4") { |r| r.a + r.b }
325
333
  #
326
- def add_column(name,options={})
334
+ def add_column(name,options={})
327
335
  if pos = options[:position]
328
336
  column_names.insert(pos,name)
329
337
  elsif pos = options[:after]
@@ -352,7 +360,7 @@ module Ruport::Data
352
360
  #
353
361
  def add_columns(names,options={})
354
362
  raise "Greg isn't smart enough to figure this out.\n"+
355
- "Send ideas in at http://list.rubyreports.org" if block_given?
363
+ "Send ideas in at http://list.rubyreports.org" if block_given?
356
364
  need_reverse = !!(options[:after] || options[:position])
357
365
  names = names.reverse if need_reverse
358
366
  names.each { |n| add_column(n,options) }
@@ -366,8 +374,8 @@ module Ruport::Data
366
374
  # table.remove_column(0) #=> removes the first column
367
375
  # table.remove_column("apple") #=> removes column named apple
368
376
  #
369
- def remove_column(col)
370
- col = column_names[col] if col.kind_of? Fixnum
377
+ def remove_column(col)
378
+ col = column_names[col] if col.kind_of? Fixnum
371
379
  column_names.delete(col)
372
380
  each { |r| r.send(:delete,col) }
373
381
  end
@@ -522,12 +530,12 @@ module Ruport::Data
522
530
  # sub_table = table.sub_table { |r| r.c > 10 }
523
531
  # sub_table == [[9,10,11,12]].to_table(%w[a b c d]) #=> true
524
532
  #
525
- def sub_table(cor=column_names,range=nil,&block)
526
- if range
533
+ def sub_table(cor=column_names,range=nil,&block)
534
+ if range
527
535
  self.class.new(:column_names => cor,:data => data[range])
528
- elsif cor.kind_of?(Range)
529
- self.class.new(:column_names => column_names,:data => data[cor])
530
- elsif block
536
+ elsif cor.kind_of?(Range)
537
+ self.class.new(:column_names => column_names,:data => data[cor])
538
+ elsif block
531
539
  self.class.new( :column_names => cor, :data => data.select(&block))
532
540
  else
533
541
  self.class.new( :column_names => cor, :data => data)
@@ -590,7 +598,7 @@ module Ruport::Data
590
598
  r.get(column) =~ /\./ ? r.get(column).to_f : r.get(column).to_i
591
599
  end
592
600
  else
593
- s + yield(r)
601
+ s + yield(r)
594
602
  end
595
603
  }
596
604
  end
@@ -598,9 +606,11 @@ module Ruport::Data
598
606
  alias_method :sum, :sigma
599
607
 
600
608
  # Returns a sorted table. If col_names is specified,
601
- # the block is ignored and the table is sorted by the named columns. All
602
- # options are used in constructing the new Table (see Array#to_table
603
- # for details).
609
+ # the block is ignored and the table is sorted by the named columns.
610
+ #
611
+ # The second argument specifies sorting options. Currently only
612
+ # :order is supported. Default order is ascending, to sort decending
613
+ # use :order => :descending
604
614
  #
605
615
  # Example:
606
616
  #
@@ -609,26 +619,42 @@ module Ruport::Data
609
619
  # # returns a new table sorted by col1
610
620
  # table.sort_rows_by {|r| r["col1"]}
611
621
  #
622
+ # # returns a new table sorted by col1, in descending order
623
+ # table.sort_rows_by(nil, :order => :descending) {|r| r["col1"]}
624
+ #
612
625
  # # returns a new table sorted by col2
613
- # table.sort_rows_by ["col2"]
626
+ # table.sort_rows_by(["col2"])
627
+ #
628
+ # # returns a new table sorted by col2, descending order
629
+ # table.sort_rows_by("col2", :order => :descending)
614
630
  #
615
631
  # # returns a new table sorted by col1, then col2
616
- # table.sort_rows_by ["col1", "col2"]
632
+ # table.sort_rows_by(["col1", "col2"])
633
+ #
634
+ # # returns a new table sorted by col1, then col2, in descending order
635
+ # table.sort_rows_by(["col1", "col2"], :order => descending)
617
636
  #
618
- def sort_rows_by(col_names=nil, &block)
637
+ def sort_rows_by(col_names=nil, options={}, &block)
619
638
  # stabilizer is needed because of
620
639
  # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/170565
621
640
  stabilizer = 0
641
+
642
+ nil_rows, sortable = partition do |r|
643
+ Array(col_names).any? { |c| r[c].nil? }
644
+ end
622
645
 
623
646
  data_array =
624
647
  if col_names
625
- sort_by do |r|
648
+ sortable.sort_by do |r|
626
649
  stabilizer += 1
627
650
  [Array(col_names).map {|col| r[col]}, stabilizer]
628
651
  end
629
652
  else
630
- sort_by(&block)
631
- end
653
+ sortable.sort_by(&block)
654
+ end
655
+
656
+ data_array += nil_rows
657
+ data_array.reverse! if options[:order] == :descending
632
658
 
633
659
  table = self.class.new( :data => data_array,
634
660
  :column_names => @column_names,
@@ -640,8 +666,8 @@ module Ruport::Data
640
666
  # Same as Table#sort_rows_by, but self modifying.
641
667
  # See <tt>sort_rows_by</tt> for documentation.
642
668
  #
643
- def sort_rows_by!(col_names=nil,&block)
644
- table = sort_rows_by(col_names,&block)
669
+ def sort_rows_by!(col_names=nil,options={},&block)
670
+ table = sort_rows_by(col_names,options,&block)
645
671
  @data = table.data
646
672
  end
647
673
 
@@ -665,7 +691,7 @@ module Ruport::Data
665
691
  end
666
692
  }
667
693
  end
668
-
694
+
669
695
  # Create a copy of the Table. Records will be copied as well.
670
696
  #
671
697
  # Example:
@@ -721,24 +747,39 @@ module Ruport::Data
721
747
  super
722
748
  end
723
749
 
724
- # Appends an array as a record in the Table.
725
- def append_array(array)
726
- attributes = @column_names.empty? ? nil : @column_names
727
- @data << record_class.new(array.to_ary, :attributes => attributes)
728
- end
750
+ def feed_element(row)
751
+ recordize(row)
752
+ end
753
+
754
+ private
729
755
 
730
- # Appends a hash as a record in the Table.
731
- def append_hash(hash_obj)
756
+ def recordize(row)
757
+ case row
758
+ when Array
759
+ normalize_array(row)
760
+ when Hash
761
+ normalize_hash(row)
762
+ when record_class
763
+ recordize(normalize_record(row))
764
+ else
765
+ normalize_hash(row) rescue normalize_array(row)
766
+ end
767
+ end
768
+
769
+ def normalize_hash(hash_obj)
732
770
  hash_obj = hash_obj.to_hash
733
771
  raise ArgumentError unless @column_names
734
- @data << record_class.new(hash_obj, :attributes => @column_names)
735
- end
772
+ record_class.new(hash_obj, :attributes => @column_names)
773
+ end
736
774
 
737
- # Appends a record to the Table.
738
- def append_record(record)
739
- self << record.send(column_names.empty? ? :to_a : :to_hash)
740
- end
741
-
775
+ def normalize_record(record)
776
+ record.send(column_names.empty? ? :to_a : :to_hash)
777
+ end
778
+
779
+ def normalize_array(array)
780
+ attributes = @column_names.empty? ? nil : @column_names
781
+ record_class.new(array.to_ary, :attributes => attributes)
782
+ end
742
783
  end
743
784
  end
744
785
 
@@ -765,7 +806,7 @@ module Kernel
765
806
  case(args[0])
766
807
  when Array
767
808
  opts = args[1] || {}
768
- Ruport::Data::Table.new(f={:column_names => args[0]}.merge(opts))
809
+ Ruport::Data::Table.new(f={:column_names => args[0]}.merge(opts),&block)
769
810
  when /\.csv/
770
811
  return Ruport::Data::Table.load(*args,&block)
771
812
  when Hash
@@ -774,13 +815,12 @@ module Kernel
774
815
  elsif string = args[0].delete(:string)
775
816
  return Ruport::Data::Table.parse(string,args[0],&block)
776
817
  else
777
- return Ruport::Data::Table.new(args[0])
818
+ return Ruport::Data::Table.new(args[0],&block)
778
819
  end
779
820
  else
780
- Ruport::Data::Table.new(:data => [], :column_names => args)
821
+ Ruport::Data::Table.new(:data => [], :column_names => args,&block)
781
822
  end
782
823
 
783
- block[table] if block
784
824
  return table
785
825
  end
786
826
  end
@@ -793,8 +833,8 @@ class Array
793
833
  # Example:
794
834
  # [[1,2],[3,4]].to_table(%w[a b])
795
835
  #
796
- def to_table(column_names=nil)
797
- Ruport::Data::Table.new({:data => self, :column_names => column_names})
836
+ def to_table(column_names=nil,&b)
837
+ Ruport::Data::Table.new({:data => self, :column_names => column_names},&b)
798
838
  end
799
839
 
800
840
  end