ruport 1.0.2 → 1.2.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.
@@ -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