jsanders-ruport 1.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. data/AUTHORS +48 -0
  2. data/LICENSE +59 -0
  3. data/README +114 -0
  4. data/Rakefile +93 -0
  5. data/examples/RWEmerson.jpg +0 -0
  6. data/examples/anon.rb +43 -0
  7. data/examples/btree/commaleon/commaleon.rb +263 -0
  8. data/examples/btree/commaleon/sample_data/ticket_count.csv +124 -0
  9. data/examples/btree/commaleon/sample_data/ticket_count2.csv +119 -0
  10. data/examples/centered_pdf_text_box.rb +83 -0
  11. data/examples/data/tattle.dump +82 -0
  12. data/examples/example.csv +3 -0
  13. data/examples/line_plotter.rb +61 -0
  14. data/examples/pdf_report_with_common_base.rb +72 -0
  15. data/examples/png_embed.rb +54 -0
  16. data/examples/roadmap.png +0 -0
  17. data/examples/row_renderer.rb +39 -0
  18. data/examples/simple_pdf_lines.rb +25 -0
  19. data/examples/simple_templating_example.rb +34 -0
  20. data/examples/tattle_ruby_version.rb +39 -0
  21. data/examples/tattle_rubygems_version.rb +37 -0
  22. data/examples/trac_ticket_status.rb +59 -0
  23. data/lib/ruport.rb +127 -0
  24. data/lib/ruport/controller.rb +616 -0
  25. data/lib/ruport/controller/grouping.rb +71 -0
  26. data/lib/ruport/controller/table.rb +54 -0
  27. data/lib/ruport/data.rb +4 -0
  28. data/lib/ruport/data/feeder.rb +111 -0
  29. data/lib/ruport/data/grouping.rb +399 -0
  30. data/lib/ruport/data/record.rb +297 -0
  31. data/lib/ruport/data/table.rb +950 -0
  32. data/lib/ruport/extensions.rb +4 -0
  33. data/lib/ruport/formatter.rb +254 -0
  34. data/lib/ruport/formatter/csv.rb +149 -0
  35. data/lib/ruport/formatter/html.rb +161 -0
  36. data/lib/ruport/formatter/pdf.rb +591 -0
  37. data/lib/ruport/formatter/template.rb +187 -0
  38. data/lib/ruport/formatter/text.rb +231 -0
  39. data/lib/uport.rb +1 -0
  40. data/test/controller_test.rb +743 -0
  41. data/test/csv_formatter_test.rb +164 -0
  42. data/test/data_feeder_test.rb +88 -0
  43. data/test/grouping_test.rb +410 -0
  44. data/test/helpers.rb +11 -0
  45. data/test/html_formatter_test.rb +201 -0
  46. data/test/pdf_formatter_test.rb +354 -0
  47. data/test/record_test.rb +332 -0
  48. data/test/samples/addressbook.csv +6 -0
  49. data/test/samples/data.csv +3 -0
  50. data/test/samples/data.tsv +3 -0
  51. data/test/samples/dates.csv +1409 -0
  52. data/test/samples/erb_test.sql +1 -0
  53. data/test/samples/query_test.sql +1 -0
  54. data/test/samples/ruport_test.sql +8 -0
  55. data/test/samples/test.sql +2 -0
  56. data/test/samples/test.yaml +3 -0
  57. data/test/samples/ticket_count.csv +124 -0
  58. data/test/table_pivot_test.rb +134 -0
  59. data/test/table_test.rb +838 -0
  60. data/test/template_test.rb +48 -0
  61. data/test/text_formatter_test.rb +258 -0
  62. data/util/bench/data/record/bench_as_vs_to.rb +18 -0
  63. data/util/bench/data/record/bench_constructor.rb +46 -0
  64. data/util/bench/data/record/bench_indexing.rb +65 -0
  65. data/util/bench/data/record/bench_reorder.rb +35 -0
  66. data/util/bench/data/record/bench_to_a.rb +19 -0
  67. data/util/bench/data/table/bench_column_manip.rb +103 -0
  68. data/util/bench/data/table/bench_dup.rb +24 -0
  69. data/util/bench/data/table/bench_init.rb +67 -0
  70. data/util/bench/data/table/bench_manip.rb +125 -0
  71. data/util/bench/formatter/bench_csv.rb +14 -0
  72. data/util/bench/formatter/bench_html.rb +14 -0
  73. data/util/bench/formatter/bench_pdf.rb +14 -0
  74. data/util/bench/formatter/bench_text.rb +14 -0
  75. data/util/bench/samples/tattle.csv +1237 -0
  76. metadata +176 -0
@@ -0,0 +1,297 @@
1
+ # Ruport : Extensible Reporting System
2
+ #
3
+ # data/record.rb provides a record data structure for Ruport.
4
+ #
5
+ # Created by Gregory Brown / Dudley Flanders, 2006
6
+ # Copyright (C) 2006 Gregory Brown / Dudley Flanders, All Rights Reserved.
7
+ #
8
+ # This is free software distributed under the same terms as Ruby 1.8
9
+ # See LICENSE and COPYING for details.
10
+ #
11
+ module Ruport::Data
12
+
13
+ # === Overview
14
+ #
15
+ # Data::Records are the work-horse of Ruport's data model. These can behave
16
+ # as Array-like, Hash-like, or Struct-like objects. They are used as the
17
+ # base element for Data::Table
18
+ #
19
+ class Record
20
+
21
+ if RUBY_VERSION < "1.9"
22
+ private :id
23
+ end
24
+
25
+ include Enumerable
26
+
27
+ # Creates a new Record object. If the <tt>:attributes</tt>
28
+ # keyword is specified, Hash-like and Struct-like
29
+ # access will be enabled. Otherwise, Record elements may be
30
+ # accessed ordinally, like an Array.
31
+ #
32
+ # A Record can accept either a Hash or an Array as its <tt>data</tt>.
33
+ #
34
+ # Examples:
35
+ # a = Record.new [1,2,3]
36
+ # a[1] #=> 2
37
+ #
38
+ # b = Record.new [1,2,3], :attributes => %w[a b c]
39
+ # b[1] #=> 2
40
+ # b['a'] #=> 1
41
+ # b.c #=> 3
42
+ #
43
+ # c = Record.new {"a" => 1, "c" => 3, "b" => 2}, :attributes => %w[a b c]
44
+ # c[1] #=> 2
45
+ # c['a'] #=> 1
46
+ # c.c #=> 3
47
+ #
48
+ # d = Record.new { "a" => 1, "c" => 3, "b" => 2 }
49
+ # d[1] #=> ? (without attributes, you cannot rely on order)
50
+ # d['a'] #=> 1
51
+ # d.c #=> 3
52
+ #
53
+ def initialize(data,options={})
54
+ data = data.dup
55
+ case(data)
56
+ when Array
57
+ @attributes = options[:attributes] || (0...data.length).to_a
58
+ @data = @attributes.inject({}) { |h,a| h.merge(a => data.shift) }
59
+ when Hash
60
+ @data = data.dup
61
+ @attributes = options[:attributes] || data.keys
62
+ end
63
+ end
64
+
65
+ ##############
66
+ # Delegators #
67
+ ##############
68
+
69
+ # Returns a copy of the <tt>attributes</tt> from this Record.
70
+ #
71
+ # Example:
72
+ #
73
+ # a = Data::Record.new([1,2],:attributes => %w[a b])
74
+ # a.attributes #=> ["a","b"]
75
+ #
76
+ def attributes
77
+ @attributes.dup
78
+ end
79
+
80
+ # Sets the <tt>attribute</tt> list for this Record.
81
+ # (Dangerous when used within Table objects!)
82
+ attr_writer :attributes
83
+
84
+ # The data for the record
85
+ attr_reader :data
86
+
87
+ # The size of the record (the number of items in the record's data).
88
+ def size; @data.size; end
89
+ alias_method :length, :size
90
+
91
+ ##################
92
+ # Access Methods #
93
+ ##################
94
+
95
+ # Allows either Array or Hash-like indexing.
96
+ #
97
+ # Examples:
98
+ #
99
+ # my_record[1]
100
+ # my_record["foo"]
101
+ #
102
+ def [](index)
103
+ case(index)
104
+ when Integer
105
+ @data[@attributes[index]]
106
+ else
107
+ @data[index]
108
+ end
109
+ end
110
+
111
+ # Allows setting a <tt>value</tt> at an <tt>index</tt>.
112
+ #
113
+ # Examples:
114
+ #
115
+ # my_record[1] = "foo"
116
+ # my_record["bar"] = "baz"
117
+ #
118
+ def []=(index,value)
119
+ case(index)
120
+ when Integer
121
+ @data[@attributes[index]] = value
122
+ else
123
+ @data[index] = value
124
+ @attributes << index unless @attributes.include? index
125
+ end
126
+ end
127
+
128
+ # Indifferent access to attributes.
129
+ #
130
+ # Examples:
131
+ #
132
+ # record.get(:foo) # looks for an attribute "foo" or :foo,
133
+ # or calls the method <tt>foo</tt>
134
+ #
135
+ # record.get("foo") # looks for an attribute "foo" or :foo
136
+ #
137
+ # record.get(0) # Gets the first element
138
+ #
139
+ def get(name)
140
+ case name
141
+ when String,Symbol
142
+ self[name] || send(name)
143
+ when Fixnum
144
+ self[name]
145
+ else
146
+ raise ArgumentError, "Whatchu Talkin' Bout, Willis?"
147
+ end
148
+ end
149
+
150
+ ################
151
+ # Conversions #
152
+ ################
153
+
154
+ # Converts a Record into an Array.
155
+ #
156
+ # Example:
157
+ #
158
+ # a = Data::Record.new([1,2],:attributes => %w[a b])
159
+ # a.to_a #=> [1,2]
160
+ #
161
+ def to_a
162
+ @attributes.map { |a| @data[a] }
163
+ end
164
+
165
+ # Converts a Record into a Hash.
166
+ #
167
+ # Example:
168
+ #
169
+ # a = Data::Record.new([1,2],:attributes => %w[a b])
170
+ # a.to_hash #=> {"a" => 1, "b" => 2}
171
+ #
172
+ def to_hash
173
+ @data.dup
174
+ end
175
+
176
+ ################
177
+ # Comparisons #
178
+ ################
179
+
180
+ # If <tt>attributes</tt> and <tt>to_a</tt> are equivalent, then
181
+ # <tt>==</tt> evaluates to true. Otherwise, <tt>==</tt> returns false.
182
+ #
183
+ def ==(other)
184
+ @attributes.eql?(other.attributes) &&
185
+ to_a == other.to_a
186
+ end
187
+
188
+ alias_method :eql?, :==
189
+
190
+ #############
191
+ # Iterators #
192
+ #############
193
+
194
+ # Yields each element of the Record. Does not provide attribute names.
195
+ def each
196
+ to_a.each { |e| yield(e) }
197
+ end
198
+
199
+ #################
200
+ # Manipulations #
201
+ #################
202
+
203
+ # Takes an old name and a new name and renames an attribute.
204
+ #
205
+ # The third option, update_index is for internal use.
206
+ def rename_attribute(old_name,new_name,update_index=true)
207
+ @attributes[@attributes.index(old_name)] = new_name if update_index
208
+ @data[new_name] = @data.delete(old_name)
209
+ end
210
+
211
+ # Allows you to change the order of or reduce the number of columns in a
212
+ # Record.
213
+ #
214
+ # Example:
215
+ #
216
+ # a = Data::Record.new([1,2,3,4],:attributes => %w[a b c d])
217
+ # a.reorder("a","d","b")
218
+ # a.attributes #=> ["a","d","b"]
219
+ # a.data #=> [1,4,2]
220
+ def reorder(*indices)
221
+ indices[0].kind_of?(Array) && indices.flatten!
222
+ if indices.all? { |i| i.kind_of? Integer }
223
+ raise ArgumentError unless indices.all? { |i| @attributes[i] }
224
+ self.attributes = indices.map { |i| @attributes[i] }
225
+ else
226
+ raise ArgumentError unless (indices - @attributes).empty?
227
+ self.attributes = indices
228
+ end
229
+ self
230
+ end
231
+
232
+ #######################
233
+ # Internals / Helpers #
234
+ #######################
235
+
236
+ include Ruport::Controller::Hooks
237
+ renders_as_row
238
+
239
+ def self.inherited(base) #:nodoc:
240
+ base.renders_as_row
241
+ end
242
+
243
+ # Provides a unique hash value. If a Record contains the same data and
244
+ # attributes as another Record, they will hash to the same value, even if
245
+ # they are not the same object. This is similar to the way Array works,
246
+ # but different from Hash and other objects.
247
+ #
248
+ def hash
249
+ @attributes.hash + to_a.hash
250
+ end
251
+
252
+ # Create a copy of the Record.
253
+ #
254
+ # Example:
255
+ #
256
+ # one = Record.new([1,2,3,4],:attributes => %w[a b c d])
257
+ # two = one.dup
258
+ #
259
+ def initialize_copy(from) #:nodoc:
260
+ @data = from.data.dup
261
+ @attributes = from.attributes.dup
262
+ end
263
+
264
+ # Provides accessor style methods for attribute access.
265
+ #
266
+ # Example:
267
+ #
268
+ # my_record.foo = 2
269
+ # my_record.foo #=> 2
270
+ #
271
+ # Also provides a shortcut for the <tt>as()</tt> method by converting a
272
+ # call to <tt>to_format_name</tt> into a call to <tt>as(:format_name)</tt>
273
+ #
274
+ def method_missing(id,*args,&block)
275
+ k = id.to_s.gsub(/=$/,"")
276
+ key_index = @attributes.index(k) || @attributes.index(k.to_sym)
277
+
278
+ if key_index
279
+ args[0] ? self[key_index] = args[0] : self[key_index]
280
+ else
281
+ return as($1.to_sym,*args,&block) if id.to_s =~ /^to_(.*)/
282
+ super
283
+ end
284
+ end
285
+
286
+ private
287
+
288
+ def delete(key)
289
+ @data.delete(key)
290
+ @attributes.delete(key)
291
+ end
292
+
293
+ def reindex(new_attributes)
294
+ @attributes = new_attributes
295
+ end
296
+ end
297
+ end
@@ -0,0 +1,950 @@
1
+ # Ruport : Extensible Reporting System
2
+ #
3
+ # data/table.rb provides a table data structure for Ruport.
4
+ #
5
+ # Created by Gregory Brown / Dudley Flanders, 2006
6
+ # Copyright (C) 2006 Gregory Brown / Dudley Flanders, All Rights Reserved.
7
+ #
8
+ # This is free software distributed under the same terms as Ruby 1.8
9
+ # See LICENSE and COPYING for details.
10
+ #
11
+ module Ruport::Data
12
+
13
+ # === Overview
14
+ #
15
+ # This class is one of the core classes for building and working with data
16
+ # in Ruport. The idea is to get your data into a standard form, regardless
17
+ # of its source (a database, manual arrays, ActiveRecord, CSVs, etc.).
18
+ #
19
+ # Table is intended to be used as the data store for structured, tabular
20
+ # data.
21
+ #
22
+ # Once your data is in a Table object, it can be manipulated
23
+ # to suit your needs, then used to build a report.
24
+ #
25
+ class Table
26
+
27
+ class Pivot #:nodoc:
28
+
29
+ def initialize(table, group_col, pivot_col, summary_col, options = {})
30
+ @table = table
31
+ @group_column = group_col
32
+ @pivot_column = pivot_col
33
+ @summary_column = summary_col
34
+ @pivot_order = options[:pivot_order]
35
+ end
36
+
37
+ def convert_row_order_to_group_order(row_order_spec)
38
+ case row_order_spec
39
+ when Array
40
+ proc {|group|
41
+ row_order_spec.map {|e| group[0][e].to_s }
42
+ }
43
+ when Proc
44
+ proc {|group|
45
+ if row_order_spec.arity == 2
46
+ row_order_spec.call(group[0], group.name)
47
+ else
48
+ row_order_spec.call(group[0])
49
+ end
50
+ }
51
+ when NilClass
52
+ nil
53
+ else
54
+ proc {|group| group[0][row_order_spec].to_s }
55
+ end
56
+ end
57
+
58
+ def columns_from_pivot
59
+ ordering = convert_row_order_to_group_order(@pivot_order)
60
+ pivot_column_grouping = Grouping(@table, :by => @pivot_column)
61
+ pivot_column_grouping.each {|n,g| g.add_column(n) { n }}
62
+ pivot_column_grouping.sort_grouping_by!(ordering) if ordering
63
+ result = []
64
+ pivot_column_grouping.each {|name,_| result << name }
65
+ result
66
+ end
67
+
68
+ def group_column_entries
69
+ @table.map {|row| row[@group_column]}.uniq
70
+ end
71
+
72
+ def to_table
73
+ result = Table()
74
+ result.add_column(@group_column)
75
+ pivoted_columns = columns_from_pivot
76
+ pivoted_columns.each { |name| result.add_column(name) }
77
+ outer_grouping = Grouping(@table, :by => @group_column)
78
+ group_column_entries.each {|outer_group_name|
79
+ outer_group = outer_grouping[outer_group_name]
80
+ pivot_values = pivoted_columns.inject({}) do |hsh, e|
81
+ matching_rows = outer_group.rows_with(@pivot_column => e)
82
+ hsh[e] = matching_rows.first && matching_rows.first[@summary_column]
83
+ hsh
84
+ end
85
+ result << [outer_group_name] + pivoted_columns.map {|e|
86
+ pivot_values[e]
87
+ }
88
+ }
89
+ result
90
+ end
91
+
92
+ end
93
+
94
+ # Creates a new table with values from the specified pivot column
95
+ # transformed into columns.
96
+ #
97
+ # Required options:
98
+ # <b><tt>:group_by</tt></b>:: The name of a column whose unique
99
+ # values should become rows in the new
100
+ # table.
101
+ #
102
+ # <b><tt>:values</tt></b>:: The name of a column that should supply
103
+ # the values for the pivoted columns.
104
+ #
105
+ # Optional:
106
+ # <b><tt>:pivot_order</tt></b>:: An ordering specification for the
107
+ # pivoted columns, in terms of the source
108
+ # rows. If this is a Proc there is an
109
+ # optional second argument that receives
110
+ # the name of the pivot column, which due
111
+ # to implementation oddity currently is
112
+ # removed from the row provided in the
113
+ # first argument. This wart will likely
114
+ # be fixed in a future version.
115
+ #
116
+ # Example:
117
+ #
118
+ # Given a table <em>my_table</em>:
119
+ # +-------------------------+
120
+ # | Group | Segment | Value |
121
+ # +-------------------------+
122
+ # | A | 1 | 0 |
123
+ # | A | 2 | 1 |
124
+ # | B | 1 | 2 |
125
+ # | B | 2 | 3 |
126
+ # +-------------------------+
127
+ #
128
+ # Pivoting the table on the Segment column:
129
+ #
130
+ # my_table.pivot('Segment', :group_by => 'Group', :values => 'Value',
131
+ # :pivot_order => proc {|row, name| name})
132
+ #
133
+ # Yields a new table like this:
134
+ # +---------------+
135
+ # | Group | 1 | 2 |
136
+ # +---------------+
137
+ # | A | 0 | 1 |
138
+ # | B | 2 | 3 |
139
+ # +---------------+
140
+ #
141
+ def pivot(pivot_column, options = {})
142
+ group_column = options[:group_by] ||
143
+ raise(ArgumentError, ":group_by option required")
144
+ value_column = options[:values] ||
145
+ raise(ArgumentError, ":values option required")
146
+ Pivot.new(
147
+ self, group_column, pivot_column, value_column, options
148
+ ).to_table
149
+ end
150
+
151
+ # === Overview
152
+ #
153
+ # This module provides facilities for creating tables from csv data.
154
+ #
155
+ module FromCSV
156
+ # Loads a CSV file directly into a Table using the FasterCSV library.
157
+ #
158
+ # Example:
159
+ #
160
+ # # treat first row as column_names
161
+ # table = Table.load('mydata.csv')
162
+ #
163
+ # # do not assume the data has column_names
164
+ # table = Table.load('mydata.csv',:has_names => false)
165
+ #
166
+ # # pass in FasterCSV options, such as column separators
167
+ # table = Table.load('mydata.csv',:csv_options => { :col_sep => "\t" })
168
+ #
169
+ def load(csv_file, options={},&block)
170
+ get_table_from_csv(:foreach, csv_file, options,&block)
171
+ end
172
+
173
+ # Creates a Table from a CSV string using FasterCSV. See Table.load for
174
+ # additional examples.
175
+ #
176
+ # table = Table.parse("a,b,c\n1,2,3\n4,5,6\n")
177
+ #
178
+ def parse(string, options={},&block)
179
+ get_table_from_csv(:parse,string,options,&block)
180
+ end
181
+
182
+ private
183
+
184
+ def get_table_from_csv(msg,param,options={},&block) #:nodoc:
185
+ require "fastercsv"
186
+
187
+ options = {:has_names => true,
188
+ :csv_options => {} }.merge(options)
189
+
190
+ adjust_options_for_fcsv_headers(options)
191
+
192
+ table = self.new(options) do |feeder|
193
+ first_line = true
194
+ FasterCSV.send(msg,param,options[:csv_options]) do |row|
195
+ if first_line
196
+ adjust_for_headers(feeder.data,row,options)
197
+ first_line = false
198
+ next if options[:has_names]
199
+ end
200
+
201
+ if block
202
+ handle_csv_row_proc(feeder,row,options,block)
203
+ else
204
+ feeder << row
205
+ end
206
+ end
207
+ end
208
+
209
+ return table
210
+ end
211
+
212
+ def handle_csv_row_proc(feeder,row,options,block)
213
+ if options[:records]
214
+ rc = options[:record_class] || Record
215
+ row = rc.new(row, :attributes => feeder.data.column_names)
216
+ end
217
+
218
+ block[feeder,row]
219
+ end
220
+
221
+ def adjust_options_for_fcsv_headers(options)
222
+ options[:has_names] = false if options[:csv_options][:headers]
223
+ end
224
+
225
+ def adjust_for_headers(loaded,row,options)
226
+ if options[:has_names]
227
+ loaded.column_names = row
228
+ elsif options[:csv_options][:headers]
229
+ loaded.column_names = row.headers
230
+ end
231
+ end
232
+ end
233
+
234
+ include Enumerable
235
+ extend FromCSV
236
+
237
+ include Ruport::Controller::Hooks
238
+ renders_as_table
239
+
240
+ def self.inherited(base) #:nodoc:
241
+ base.renders_as_table
242
+ end
243
+
244
+ # Creates a new table based on the supplied options.
245
+ #
246
+ # Valid options:
247
+ # <b><tt>:data</tt></b>:: An Array of Arrays representing the
248
+ # records in this Table.
249
+ # <b><tt>:column_names</tt></b>:: An Array containing the column names
250
+ # for this Table.
251
+ # <b><tt>:filters</tt></b>:: A proc or array of procs that set up
252
+ # conditions to filter the data being
253
+ # added to the table.
254
+ # <b><tt>:transforms</tt></b>:: A proc or array of procs that perform
255
+ # transformations on the data being added
256
+ # to the table.
257
+ # <b><tt>:record_class</tt></b>:: Specify the class of the table's
258
+ # records.
259
+ #
260
+ # Example:
261
+ #
262
+ # table = Table.new :data => [[1,2,3], [3,4,5]],
263
+ # :column_names => %w[a b c]
264
+ #
265
+ def initialize(options={})
266
+ @column_names = options[:column_names] ? options[:column_names].dup : []
267
+ @record_class = options[:record_class] &&
268
+ options[:record_class].name || "Ruport::Data::Record"
269
+ @data = []
270
+
271
+ feeder = Feeder.new(self)
272
+
273
+ Array(options[:filters]).each { |f| feeder.filter(&f) }
274
+ Array(options[:transforms]).each { |t| feeder.transform(&t) }
275
+
276
+ if options[:data]
277
+ options[:data].each do |e|
278
+ if e.kind_of?(Record)
279
+ e = if @column_names.empty? or
280
+ e.attributes.all? { |a| a.kind_of?(Numeric) }
281
+ e.to_a
282
+ else
283
+ e.to_hash.values_at(*@column_names)
284
+ end
285
+ end
286
+ r = recordize(e)
287
+
288
+ feeder << r
289
+ end
290
+ end
291
+
292
+ yield(feeder) if block_given?
293
+ end
294
+
295
+ # This Table's column names
296
+ attr_reader :column_names
297
+
298
+ # This Table's data
299
+ attr_reader :data
300
+
301
+ require "forwardable"
302
+ extend Forwardable
303
+ def_delegators :@data, :each, :length, :size, :empty?, :[]
304
+
305
+ # Sets the column names for this table. <tt>new_column_names</tt> should
306
+ # be an array listing the names of the columns.
307
+ #
308
+ # Example:
309
+ #
310
+ # table = Table.new :data => [[1,2,3], [3,4,5]],
311
+ # :column_names => %w[a b c]
312
+ #
313
+ # table.column_names = %w[e f g]
314
+ #
315
+ def column_names=(new_column_names)
316
+ columns = new_column_names.zip(@column_names)
317
+ @column_names.replace(new_column_names.dup)
318
+ unless @data.empty?
319
+ each { |r|
320
+ columns.each_with_index { |x,i|
321
+ if x[1].nil?
322
+ r.rename_attribute(i,x[0])
323
+ elsif x[1] != x[0]
324
+ r.rename_attribute(x[1],x[0],false)
325
+ end
326
+ }
327
+ r.send(:reindex, @column_names)
328
+ }
329
+ end
330
+ end
331
+
332
+ # Compares this Table to another Table and returns <tt>true</tt> if
333
+ # both the <tt>data</tt> and <tt>column_names</tt> are equal.
334
+ #
335
+ # Example:
336
+ #
337
+ # one = Table.new :data => [[1,2], [3,4]],
338
+ # :column_names => %w[a b]
339
+ #
340
+ # two = Table.new :data => [[1,2], [3,4]],
341
+ # :column_names => %w[a b]
342
+ #
343
+ # one.eql?(two) #=> true
344
+ #
345
+ def eql?(other)
346
+ data.eql?(other.data) && column_names.eql?(other.column_names)
347
+ end
348
+
349
+ alias_method :==, :eql?
350
+
351
+ # Used to add extra data to the Table. <tt>row</tt> can be an Array,
352
+ # Hash or Record. It also can be anything that implements a meaningful
353
+ # to_hash or to_ary.
354
+ #
355
+ # Example:
356
+ #
357
+ # data = Table.new :data => [[1,2], [3,4]],
358
+ # :column_names => %w[a b]
359
+ # data << [8,9]
360
+ # data << { :a => 4, :b => 5}
361
+ # data << Record.new [5,6], :attributes => %w[a b]
362
+ #
363
+ def <<(row)
364
+ @data << recordize(row)
365
+ return self
366
+ end
367
+
368
+ # Returns the record class constant being used by the table.
369
+ def record_class
370
+ @record_class.split("::").inject(Class) { |c,el| c.send(:const_get,el) }
371
+ end
372
+
373
+ # Used to merge two Tables by rows.
374
+ # Raises an ArgumentError if the Tables don't have identical columns.
375
+ #
376
+ # Example:
377
+ #
378
+ # inky = Table.new :data => [[1,2], [3,4]],
379
+ # :column_names => %w[a b]
380
+ #
381
+ # blinky = Table.new :data => [[5,6]],
382
+ # :column_names => %w[a b]
383
+ #
384
+ # sue = inky + blinky
385
+ # sue.data #=> [[1,2],[3,4],[5,6]]
386
+ #
387
+ def +(other)
388
+ raise ArgumentError unless other.column_names == @column_names
389
+ self.class.new( :column_names => @column_names,
390
+ :data => @data + other.data,
391
+ :record_class => record_class )
392
+ end
393
+
394
+ # Allows you to change the order of, or reduce the number of columns in a
395
+ # Table.
396
+ #
397
+ # Example:
398
+ #
399
+ # a = Table.new :data => [[1,2,3],[4,5,6]], :column_names => %w[a b c]
400
+ # a.reorder("b","c","a")
401
+ # a.column_names #=> ["b","c","a"]
402
+ #
403
+ # a = Table.new :data => [[1,2,3],[4,5,6]], :column_names => %w[a b c]
404
+ # a.reorder(1,2,0)
405
+ # a.column_names #=> ["b","c","a"]
406
+ #
407
+ # a = Table.new :data => [[1,2,3],[4,5,6]], :column_names => %w[a b c]
408
+ # a.reorder(0,2)
409
+ # a.column_names #=> ["a","c"]
410
+ #
411
+ def reorder(*indices)
412
+ raise(ArgumentError,"Can't reorder without column names set!") if
413
+ @column_names.empty?
414
+
415
+ indices = indices[0] if indices[0].kind_of? Array
416
+
417
+ if indices.all? { |i| i.kind_of? Integer }
418
+ indices.map! { |i| @column_names[i] }
419
+ end
420
+
421
+ reduce(indices)
422
+ end
423
+
424
+ # Adds an extra column to the Table.
425
+ #
426
+ # Available Options:
427
+ # <b><tt>:default</tt></b>:: The default value to use for the column in
428
+ # existing rows. Set to nil if not specified.
429
+ #
430
+ # <b><tt>:position</tt></b>:: Inserts the column at the indicated position
431
+ # number.
432
+ #
433
+ # <b><tt>:before</tt></b>:: Inserts the new column before the column
434
+ # indicated (by name).
435
+ #
436
+ # <b><tt>:after</tt></b>:: Inserts the new column after the column
437
+ # indicated (by name).
438
+ #
439
+ # If a block is provided, it will be used to build up the column.
440
+ #
441
+ # Example:
442
+ #
443
+ # data = Table("a","b") { |t| t << [1,2] << [3,4] }
444
+ #
445
+ # # basic usage, column full of 1's
446
+ # data.add_column 'new_column', :default => 1
447
+ #
448
+ # # new empty column before new_column
449
+ # data.add_column 'new_col2', :before => 'new_column'
450
+ #
451
+ # # new column placed just after column a
452
+ # data.add_column 'new_col3', :position => 1
453
+ #
454
+ # # new column built via a block, added at the end of the table
455
+ # data.add_column("new_col4") { |r| r.a + r.b }
456
+ #
457
+ def add_column(name,options={})
458
+ if pos = options[:position]
459
+ column_names.insert(pos,name)
460
+ elsif pos = options[:after]
461
+ column_names.insert(column_names.index(pos)+1,name)
462
+ elsif pos = options[:before]
463
+ column_names.insert(column_names.index(pos),name)
464
+ else
465
+ column_names << name
466
+ end
467
+
468
+ if block_given?
469
+ each { |r| r[name] = yield(r) || options[:default] }
470
+ else
471
+ each { |r| r[name] = options[:default] }
472
+ end; self
473
+ end
474
+
475
+ # Add multiple extra columns to the Table. See <tt>add_column</tt> for
476
+ # a list of available options.
477
+ #
478
+ # Example:
479
+ #
480
+ # data = Table("a","b") { |t| t << [1,2] << [3,4] }
481
+ #
482
+ # data.add_columns ['new_column_1','new_column_2'], :default => 1
483
+ #
484
+ def add_columns(names,options={})
485
+ raise "Greg isn't smart enough to figure this out.\n"+
486
+ "Send ideas in at http://list.rubyreports.org" if block_given?
487
+ need_reverse = !!(options[:after] || options[:position])
488
+ names = names.reverse if need_reverse
489
+ names.each { |n| add_column(n,options) }
490
+ self
491
+ end
492
+
493
+ # Removes the given column from the table. May use name or position.
494
+ #
495
+ # Example:
496
+ #
497
+ # table.remove_column(0) #=> removes the first column
498
+ # table.remove_column("apple") #=> removes column named apple
499
+ #
500
+ def remove_column(col)
501
+ col = column_names[col] if col.kind_of? Fixnum
502
+ column_names.delete(col)
503
+ each { |r| r.send(:delete,col) }
504
+ end
505
+
506
+ # Removes multiple columns from the table. May use name or position
507
+ # Will autosplat arrays.
508
+ #
509
+ # Example:
510
+ # table.remove_columns('a','b','c')
511
+ # table.remove_columns([0,1])
512
+ #
513
+ def remove_columns(*cols)
514
+ cols = cols[0] if cols[0].kind_of? Array
515
+ cols.each { |col| remove_column(col) }
516
+ end
517
+
518
+ # Renames a column. Will update Record attributes as well.
519
+ #
520
+ # Example:
521
+ #
522
+ # old_values = table.map { |r| r.a }
523
+ # table.rename_column("a","zanzibar")
524
+ # new_values = table.map { |r| r.zanzibar }
525
+ # old_values == new_values #=> true
526
+ # table.column_names.include?("a") #=> false
527
+ #
528
+ def rename_column(old_name,new_name)
529
+ index = column_names.index(old_name) or return
530
+ self.column_names[index] = new_name
531
+ each { |r| r.rename_attribute(old_name,new_name,false)}
532
+ end
533
+
534
+ # Renames multiple columns. Takes either a hash of "old" => "new"
535
+ # names or two arrays of names %w[old names],%w[new names].
536
+ #
537
+ # Example:
538
+ #
539
+ # table.column_names #=> ["a", "b"]
540
+ # table.rename_columns ["a", "b"], ["c", "d"]
541
+ # table.column_names #=> ["c", "d"]
542
+ #
543
+ # table.column_names #=> ["a", "b"]
544
+ # table.rename_columns {"a" => "c", "b" => "d"}
545
+ # table.column_names #=> ["c", "d"]
546
+ #
547
+ def rename_columns(old_cols=nil,new_cols=nil)
548
+ if block_given?
549
+ if old_cols
550
+ old_cols.each { |c| rename_column(c,yield(c)) }
551
+ else
552
+ column_names.each { |c| rename_column(c,yield(c)) }
553
+ end
554
+ return
555
+ end
556
+
557
+ raise ArgumentError unless old_cols
558
+
559
+ if new_cols
560
+ raise ArgumentError,
561
+ "odd number of arguments" unless old_cols.size == new_cols.size
562
+ h = Hash[*old_cols.zip(new_cols).flatten]
563
+ else
564
+ h = old_cols
565
+ end
566
+ h.each {|old,new| rename_column(old,new) }
567
+ end
568
+
569
+ # Exchanges one column with another.
570
+ #
571
+ # Example:
572
+ #
573
+ # >> a = Table(%w[a b c]) { |t| t << [1,2,3] << [4,5,6] }
574
+ # >> puts a
575
+ # +-----------+
576
+ # | a | b | c |
577
+ # +-----------+
578
+ # | 1 | 2 | 3 |
579
+ # | 4 | 5 | 6 |
580
+ # +-----------+
581
+ # >> a.swap_column("a","c")
582
+ # >> puts a
583
+ # +-----------+
584
+ # | c | b | a |
585
+ # +-----------+
586
+ # | 3 | 2 | 1 |
587
+ # | 6 | 5 | 4 |
588
+ # +-----------+
589
+ #
590
+ def swap_column(a,b)
591
+ if [a,b].all? { |r| r.kind_of? Fixnum }
592
+ col_a,col_b = column_names[a],column_names[b]
593
+ column_names[a] = col_b
594
+ column_names[b] = col_a
595
+ else
596
+ a_ind, b_ind = [column_names.index(a), column_names.index(b)]
597
+ column_names[b_ind] = a
598
+ column_names[a_ind] = b
599
+ end
600
+ end
601
+
602
+ # Allows you to specify a new column to replace an existing column
603
+ # in your table via a block.
604
+ #
605
+ # Example:
606
+ #
607
+ # >> a = Table(%w[a b c]) { |t| t << [1,2,3] << [4,5,6] }
608
+ # >> a.replace_column("c","c2") { |r| r.c * 2 + r.a }
609
+ #
610
+ # >> puts a
611
+ # +------------+
612
+ # | a | b | c2 |
613
+ # +------------+
614
+ # | 1 | 2 | 7 |
615
+ # | 4 | 5 | 16 |
616
+ # +------------+
617
+ #
618
+ def replace_column(old_col,new_col=nil,&block)
619
+ if new_col
620
+ add_column(new_col,:after => old_col,&block)
621
+ remove_column(old_col)
622
+ else
623
+ each { |r| r[old_col] = yield(r) }
624
+ end
625
+ end
626
+
627
+ # Generates a sub table
628
+ #
629
+ # Examples:
630
+ #
631
+ # table = [[1,2,3,4],[5,6,7,8],[9,10,11,12]].to_table(%w[a b c d])
632
+ #
633
+ # Using column_names and a range:
634
+ #
635
+ # sub_table = table.sub_table(%w[a b],1..-1)
636
+ # sub_table == [[5,6],[9,10]].to_table(%w[a b]) #=> true
637
+ #
638
+ # Using just column_names:
639
+ #
640
+ # sub_table = table.sub_table(%w[a d])
641
+ # sub_table == [[1,4],[5,8],[9,12]].to_table(%w[a d]) #=> true
642
+ #
643
+ # Using column_names and a block:
644
+ #
645
+ # sub_table = table.sub_table(%w[d b]) { |r| r.a < 6 }
646
+ # sub_table == [[4,2],[8,6]].to_table(%w[d b]) #=> true
647
+ #
648
+ # Using a range for row reduction:
649
+ # sub_table = table.sub_table(1..-1)
650
+ # sub_table == [[5,6,7,8],[9,10,11,12]].to_table(%w[a b c d]) #=> true
651
+ #
652
+ # Using just a block:
653
+ #
654
+ # sub_table = table.sub_table { |r| r.c > 10 }
655
+ # sub_table == [[9,10,11,12]].to_table(%w[a b c d]) #=> true
656
+ #
657
+ def sub_table(cor=column_names,range=nil,&block)
658
+ if range
659
+ self.class.new(:column_names => cor,:data => data[range])
660
+ elsif cor.kind_of?(Range)
661
+ self.class.new(:column_names => column_names,:data => data[cor])
662
+ elsif block
663
+ self.class.new( :column_names => cor, :data => data.select(&block))
664
+ else
665
+ self.class.new( :column_names => cor, :data => data)
666
+ end
667
+ end
668
+
669
+ # Generates a sub table in place, modifying the receiver. See documentation
670
+ # for <tt>sub_table</tt>.
671
+ #
672
+ def reduce(columns=column_names,range=nil,&block)
673
+ t = sub_table(columns,range,&block)
674
+ @data = t.data
675
+ @column_names = t.column_names
676
+ self
677
+ end
678
+
679
+ alias_method :sub_table!, :reduce
680
+
681
+ # Returns an array of values for the given column name.
682
+ #
683
+ # Example:
684
+ #
685
+ # table = [[1,2],[3,4],[5,6]].to_table(%w[col1 col2])
686
+ # table.column("col1") #=> [1,3,5]
687
+ #
688
+ def column(name)
689
+ case(name)
690
+ when Integer
691
+ unless column_names.empty?
692
+ raise ArgumentError if name > column_names.length
693
+ end
694
+ else
695
+ raise ArgumentError unless column_names.include?(name)
696
+ end
697
+
698
+ map { |r| r[name] }
699
+ end
700
+
701
+ # Calculates sums. If a column name or index is given, it will try to
702
+ # convert each element of that column to an integer or float
703
+ # and add them together.
704
+ #
705
+ # If a block is given, it yields each Record so that you can do your own
706
+ # calculation.
707
+ #
708
+ # Example:
709
+ #
710
+ # table = [[1,2],[3,4],[5,6]].to_table(%w[col1 col2])
711
+ # table.sigma("col1") #=> 9
712
+ # table.sigma(0) #=> 9
713
+ # table.sigma { |r| r.col1 + r.col2 } #=> 21
714
+ # table.sigma { |r| r.col2 + 1 } #=> 15
715
+ #
716
+ def sigma(column=nil)
717
+ inject(0) { |s,r|
718
+ if column
719
+ s + if r.get(column).kind_of? Numeric
720
+ r.get(column)
721
+ else
722
+ r.get(column) =~ /\./ ? r.get(column).to_f : r.get(column).to_i
723
+ end
724
+ else
725
+ s + yield(r)
726
+ end
727
+ }
728
+ end
729
+
730
+ alias_method :sum, :sigma
731
+
732
+ # Returns a sorted table. If col_names is specified,
733
+ # the block is ignored and the table is sorted by the named columns.
734
+ #
735
+ # The second argument specifies sorting options. Currently only
736
+ # :order is supported. Default order is ascending, to sort decending
737
+ # use :order => :descending
738
+ #
739
+ # Example:
740
+ #
741
+ # table = [[4, 3], [2, 5], [7, 1]].to_table(%w[col1 col2 ])
742
+ #
743
+ # # returns a new table sorted by col1
744
+ # table.sort_rows_by {|r| r["col1"]}
745
+ #
746
+ # # returns a new table sorted by col1, in descending order
747
+ # table.sort_rows_by(nil, :order => :descending) {|r| r["col1"]}
748
+ #
749
+ # # returns a new table sorted by col2
750
+ # table.sort_rows_by(["col2"])
751
+ #
752
+ # # returns a new table sorted by col2, descending order
753
+ # table.sort_rows_by("col2", :order => :descending)
754
+ #
755
+ # # returns a new table sorted by col1, then col2
756
+ # table.sort_rows_by(["col1", "col2"])
757
+ #
758
+ # # returns a new table sorted by col1, then col2, in descending order
759
+ # table.sort_rows_by(["col1", "col2"], :order => descending)
760
+ #
761
+ def sort_rows_by(col_names=nil, options={}, &block)
762
+ # stabilizer is needed because of
763
+ # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/170565
764
+ stabilizer = 0
765
+
766
+ nil_rows, sortable = partition do |r|
767
+ Array(col_names).any? { |c| r[c].nil? }
768
+ end
769
+
770
+ data_array =
771
+ if col_names
772
+ sortable.sort_by do |r|
773
+ stabilizer += 1
774
+ [Array(col_names).map {|col| r[col]}, stabilizer]
775
+ end
776
+ else
777
+ sortable.sort_by(&block)
778
+ end
779
+
780
+ data_array += nil_rows
781
+ data_array.reverse! if options[:order] == :descending
782
+
783
+ table = self.class.new( :data => data_array,
784
+ :column_names => @column_names,
785
+ :record_class => record_class )
786
+
787
+ return table
788
+ end
789
+
790
+ # Same as Table#sort_rows_by, but self modifying.
791
+ # See <tt>sort_rows_by</tt> for documentation.
792
+ #
793
+ def sort_rows_by!(col_names=nil,options={},&block)
794
+ table = sort_rows_by(col_names,options,&block)
795
+ @data = table.data
796
+ end
797
+
798
+ # Get an array of records from the Table limited by the criteria specified.
799
+ #
800
+ # Example:
801
+ #
802
+ # table = Table.new :data => [[1,2,3], [1,4,6], [4,5,6]],
803
+ # :column_names => %w[a b c]
804
+ # table.rows_with(:a => 1) #=> [[1,2,3], [1,4,6]]
805
+ # table.rows_with(:a => 1, :b => 4) #=> [[1,4,6]]
806
+ # table.rows_with_a(1) #=> [[1,2,3], [1,4,6]]
807
+ # table.rows_with(%w[a b]) {|a,b| [a,b] == [1,4] } #=> [[1,4,6]]
808
+ #
809
+ def rows_with(columns,&block)
810
+ select { |r|
811
+ if block
812
+ block[*(columns.map { |c| r.get(c) })]
813
+ else
814
+ columns.all? { |k,v| r.get(k) == v }
815
+ end
816
+ }
817
+ end
818
+
819
+ # Create a copy of the Table. Records will be copied as well.
820
+ #
821
+ # Example:
822
+ #
823
+ # one = Table.new :data => [[1,2], [3,4]],
824
+ # :column_names => %w[a b]
825
+ # two = one.dup
826
+ #
827
+ def initialize_copy(from)
828
+ @record_class = from.record_class.name
829
+ @column_names = from.column_names.dup
830
+ @data = []
831
+ from.data.each { |r| self << r.dup }
832
+ end
833
+
834
+ # Uses Ruport's built-in text formatter to render this Table into a String.
835
+ #
836
+ # Example:
837
+ #
838
+ # data = Table.new :data => [[1,2], [3,4]],
839
+ # :column_names => %w[a b]
840
+ # puts data.to_s
841
+ #
842
+ def to_s
843
+ as(:text)
844
+ end
845
+
846
+ # Convert the Table into a Group using the supplied group name.
847
+ #
848
+ # data = Table.new :data => [[1,2], [3,4]],
849
+ # :column_names => %w[a b]
850
+ # group = data.to_group("my_group")
851
+ #
852
+ def to_group(name=nil)
853
+ Group.new( :data => data,
854
+ :column_names => column_names,
855
+ :name => name,
856
+ :record_class => record_class )
857
+ end
858
+
859
+ # NOTE: does not respect tainted status
860
+ alias_method :clone, :dup
861
+
862
+ # Provides a shortcut for the <tt>as()</tt> method by converting a call to
863
+ # <tt>as(:format_name)</tt> into a call to <tt>to_format_name</tt>
864
+ #
865
+ # Also converts a call to <tt>rows_with_columnname</tt> to a call to
866
+ # <tt>rows_with(:columnname => args[0])</tt>.
867
+ #
868
+ def method_missing(id,*args,&block)
869
+ return as($1.to_sym,*args,&block) if id.to_s =~ /^to_(.*)/
870
+ return rows_with($1.to_sym => args[0]) if id.to_s =~ /^rows_with_(.*)/
871
+ super
872
+ end
873
+
874
+ def feed_element(row)
875
+ recordize(row)
876
+ end
877
+
878
+ private
879
+
880
+ def recordize(row)
881
+ case row
882
+ when Array
883
+ normalize_array(row)
884
+ when Hash
885
+ normalize_hash(row)
886
+ when record_class
887
+ recordize(normalize_record(row))
888
+ else
889
+ normalize_hash(row) rescue normalize_array(row)
890
+ end
891
+ end
892
+
893
+ def normalize_hash(hash_obj)
894
+ hash_obj = hash_obj.to_hash
895
+ raise ArgumentError unless @column_names
896
+ record_class.new(hash_obj, :attributes => @column_names)
897
+ end
898
+
899
+ def normalize_record(record)
900
+ record.send(column_names.empty? ? :to_a : :to_hash)
901
+ end
902
+
903
+ def normalize_array(array)
904
+ attributes = @column_names.empty? ? nil : @column_names
905
+ record_class.new(array.to_ary, :attributes => attributes)
906
+ end
907
+ end
908
+ end
909
+
910
+
911
+ module Kernel
912
+
913
+ # Shortcut interface for creating Data::Tables
914
+ #
915
+ # Examples:
916
+ #
917
+ # t = Table(%w[a b c]) #=> creates a new empty table w. cols a,b,c
918
+ # t = Table("a","b","c") #=> creates a new empty table w. cols a,b,c
919
+ #
920
+ # # allows building table inside of block, returns table object
921
+ # t = Table(%w[a b c]) { |t| t << [1,2,3] }
922
+ #
923
+ # # allows loading table from CSV
924
+ # # accepts all Data::Table.load options, including block (yields table,row)
925
+ #
926
+ # t = Table("foo.csv")
927
+ # t = Table("bar.csv", :has_names => false)
928
+ def Table(*args,&block)
929
+ table=
930
+ case(args[0])
931
+ when Array
932
+ opts = args[1] || {}
933
+ Ruport::Data::Table.new(f={:column_names => args[0]}.merge(opts),&block)
934
+ when /\.csv/
935
+ return Ruport::Data::Table.load(*args,&block)
936
+ when Hash
937
+ if file = args[0].delete(:file)
938
+ return Ruport::Data::Table.load(file,args[0],&block)
939
+ elsif string = args[0].delete(:string)
940
+ return Ruport::Data::Table.parse(string,args[0],&block)
941
+ else
942
+ return Ruport::Data::Table.new(args[0],&block)
943
+ end
944
+ else
945
+ Ruport::Data::Table.new(:data => [], :column_names => args,&block)
946
+ end
947
+
948
+ return table
949
+ end
950
+ end