ruport 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. data/AUTHORS +13 -4
  2. data/CHANGELOG +15 -1
  3. data/Rakefile +1 -1
  4. data/bin/rope +5 -1
  5. data/examples/basic_grouping.rb +13 -3
  6. data/examples/latex_table.rb +17 -0
  7. data/examples/line_graph_report.rb +23 -0
  8. data/examples/line_graph_report.rb.rej +15 -0
  9. data/examples/line_graph_report.rb~ +23 -0
  10. data/examples/line_plotter.rb +1 -0
  11. data/examples/sql_erb.rb +20 -0
  12. data/examples/template.rb +1 -1
  13. data/examples/text_processors.rb +3 -9
  14. data/lib/ruport.rb +2 -3
  15. data/lib/ruport.rb~ +69 -0
  16. data/lib/ruport/attempt.rb +63 -0
  17. data/lib/ruport/data.rb +1 -1
  18. data/lib/ruport/data.rb.rej +5 -0
  19. data/lib/ruport/data.rb~ +1 -0
  20. data/lib/ruport/data/groupable.rb +20 -0
  21. data/lib/ruport/data/record.rb +18 -13
  22. data/lib/ruport/data/table.rb +92 -26
  23. data/lib/ruport/data/table.rb~ +329 -0
  24. data/lib/ruport/format.rb +7 -1
  25. data/lib/ruport/format/engine/table.rb +2 -1
  26. data/lib/ruport/format/plugin.rb +8 -8
  27. data/lib/ruport/format/plugin/csv_plugin.rb +5 -1
  28. data/lib/ruport/format/plugin/html_plugin.rb +12 -8
  29. data/lib/ruport/format/plugin/latex_plugin.rb +50 -0
  30. data/lib/ruport/format/plugin/text_plugin.rb +38 -33
  31. data/lib/ruport/meta_tools.rb +3 -3
  32. data/lib/ruport/query.rb +21 -9
  33. data/lib/ruport/report.rb +35 -20
  34. data/lib/ruport/report/graph.rb +14 -0
  35. data/lib/ruport/system_extensions.rb +9 -8
  36. data/test/_test_groupable.rb +0 -0
  37. data/test/samples/data.tsv +3 -0
  38. data/test/samples/erb_test.sql +1 -0
  39. data/test/samples/query_test.sql +1 -0
  40. data/test/test_collection.rb +1 -1
  41. data/test/test_format.rb +1 -1
  42. data/test/test_format_engine.rb +17 -0
  43. data/test/test_groupable.rb +41 -0
  44. data/test/test_invoice.rb +1 -1
  45. data/test/test_latex.rb +20 -0
  46. data/test/test_plugin.rb +59 -29
  47. data/test/test_query.rb +12 -6
  48. data/test/test_record.rb +23 -4
  49. data/test/test_record.rb.rej +46 -0
  50. data/test/test_report.rb +32 -7
  51. data/test/test_table.rb +152 -4
  52. data/test/ts_all.rb +21 -0
  53. data/test/unit.log +61 -154
  54. metadata +32 -12
  55. data/lib/ruport/rails.rb +0 -2
  56. data/lib/ruport/rails/reportable.rb +0 -58
@@ -33,33 +33,33 @@ module Ruport::Data
33
33
  #
34
34
  # For building a table using ActiveRecord, have a look at Ruport::Reportable.
35
35
  class Table < Collection
36
-
36
+ include Groupable
37
37
  # Creates a new table based on the supplied options.
38
38
  # Valid options are :data and :column_names.
39
39
  #
40
40
  # table = Table.new({:data => [1,2,3], [3,4,5],
41
41
  # :column_names => %w[a b c]})
42
42
  def initialize(options={})
43
- @column_names = options[:column_names].dup if options[:column_names]
43
+ @column_names = options[:column_names] ? options[:column_names].dup : []
44
44
  @data = []
45
- options[:data].each { |e| self << e } if options[:data]
45
+ if options[:data]
46
+ if options[:data].all? { |r| r.kind_of? Record }
47
+ record_tags = options[:data].map { |r| r.tags }
48
+ options[:data] = options[:data].map { |r| r.to_a }
49
+ end
50
+ options[:data].each { |e| self << e }
51
+ each { |r| r.tags = record_tags.shift } if record_tags
52
+ end
46
53
  end
47
54
 
48
55
  attr_reader :column_names
49
-
50
56
  def_delegator :@data, :[]
51
-
52
57
  # Sets the column names for this table. The single parameter should be
53
58
  # an array listing the names of the columns.
54
59
  #
55
60
  # tbl = Table.new({:data => [1,2,3], [3,4,5], :column_names => %w[a b c]})
56
61
  # tbl.column_names = %w[e f g]
57
62
  def column_names=(other)
58
- #FIXME: when column_names changes, we'll need to remove this
59
- unless @column_names
60
- @column_names = other.dup
61
- each { |r| r.attributes = @column_names }
62
- end
63
63
  @column_names.replace(other.dup)
64
64
  end
65
65
 
@@ -72,6 +72,7 @@ module Ruport::Data
72
72
  def eql?(other)
73
73
  data.eql?(other.data) && column_names.eql?(other.column_names)
74
74
  end
75
+
75
76
  alias_method :==, :eql?
76
77
 
77
78
  # Uses Ruport's built-in text plugin to render this table into a string
@@ -107,22 +108,32 @@ module Ruport::Data
107
108
  self
108
109
  end
109
110
 
111
+ # Used to combine two tables. Throws an ArgumentError if the tables don't
112
+ # have identical columns.
113
+ #
114
+ # inky = Table.new(:data => [[1,2], [3,4]], :column_names => %w[a b])
115
+ # blinky = Table.new(:data => [[5,6]], :column_names => %w[a b])
116
+ # sue = inky + blinky
117
+ # sue.data #=> [[1,2],[3,4],[5,6]]
118
+
119
+ def +(other)
120
+ raise ArgumentError unless other.column_names == @column_names
121
+ Table.new(:column_names => @column_names, :data => @data + other.data)
122
+ end
123
+
110
124
  # Reorders the columns that exist in the table. Operates directly
111
125
  # on this table.
112
126
  #
113
127
  # data = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
114
128
  # data.reorder!([1,0])
115
- #
116
129
  def reorder!(*indices)
117
130
  indices = indices[0] if indices[0].kind_of? Array
118
- if @column_names
131
+ if @column_names && !@column_names.empty?
119
132
  x = if indices.all? { |i| i.kind_of? Integer }
120
133
  indices.map { |i| @column_names[i] }
121
134
  else
122
135
  indices
123
136
  end
124
- # FIXME: @column_names.replace should and allow us to avoid the
125
- # r.attributes hack below. This means this might be buggy.
126
137
  @column_names = x
127
138
  end
128
139
  @data.each { |r|
@@ -145,14 +156,14 @@ module Ruport::Data
145
156
  # use for the column in existing rows.
146
157
  #
147
158
  # data = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
148
- # data.append_coulmn({:name => 'new_column', :fill => 1)
159
+ # data.append_column({:name => 'new_column', :fill => 1)
149
160
  def append_column(options={})
150
161
  self.column_names += [options[:name]] if options[:name]
151
162
  if block_given?
152
163
  each { |r| r.data << yield(r) || options[:fill] }
153
164
  else
154
165
  each { |r| r.data << options[:fill] }
155
- end
166
+ end; self
156
167
  end
157
168
 
158
169
  # Removes a column from the table. Any values in the specified column are
@@ -168,11 +179,15 @@ module Ruport::Data
168
179
  # data.eql? [[1],[3]].to_table %w[a] #=> true
169
180
  def remove_column(options={})
170
181
  if options.kind_of? Integer
171
- reorder!((0...data[0].length).to_a - [options])
182
+ return reorder!((0...data[0].length).to_a - [options])
183
+ elsif options.kind_of? Hash
184
+ name = options[:name]
172
185
  else
173
- raise ArgumentError unless column_names.include? options[:name]
174
- reorder! column_names - [options[:name]]
175
- end
186
+ name = options
187
+ end
188
+
189
+ raise ArgumentError unless column_names.include? name
190
+ reorder! column_names - [name]
176
191
  end
177
192
 
178
193
  # Create a shallow copy of the table: the same data elements are referenced
@@ -189,13 +204,22 @@ module Ruport::Data
189
204
  # Loads a CSV file directly into a table using the fasterCSV library.
190
205
  #
191
206
  # data = Table.load('mydata.csv')
192
- def self.load(csv_file, options = {})
193
- options = {:has_names => true}.merge(options)
207
+ def self.load(csv_file, options={})
208
+ get_table_from_csv(:foreach, csv_file, options)
209
+ end
210
+
211
+ def self.parse(string, options={})
212
+ get_table_from_csv(:parse,string,options)
213
+ end
214
+
215
+ def self.get_table_from_csv(msg,param,options={})
216
+ options = {:has_names => true,
217
+ :csv_options => {} }.merge(options)
194
218
  require "fastercsv"
195
219
  loaded_data = self.new
196
220
 
197
221
  first_line = true
198
- FasterCSV.foreach(csv_file) do |row|
222
+ FasterCSV.send(msg,param,options[:csv_options]) do |row|
199
223
  if first_line && options[:has_names]
200
224
  loaded_data.column_names = row
201
225
  first_line = false
@@ -207,8 +231,6 @@ module Ruport::Data
207
231
  end ; loaded_data
208
232
  end
209
233
 
210
-
211
-
212
234
  # Allows you to split tables into multiple tables for grouping.
213
235
  #
214
236
  # Example:
@@ -259,13 +281,57 @@ module Ruport::Data
259
281
  :column_names => c
260
282
  )
261
283
  }
262
- if options[:group].kind_of? Array
284
+ rec = if options[:group].kind_of? Array
263
285
  Ruport::Data::Record.new(data,
264
286
  :attributes => group.map { |e| e.join("_") } )
265
287
  else
266
288
  Ruport::Data::Record.new data, :attributes => group
267
289
  end
290
+ class << rec
291
+ def each_group; attributes.each { |a| yield(a) }; end
292
+ end; rec
293
+ end
294
+
295
+ # Calculates sums. If a column name or index is given, it will try to
296
+ # convert each element of that column to an integer or float
297
+ # and add it together
298
+ #
299
+ # If a block is given, yields each Record so that you can do a calculation.
300
+ #
301
+ #
302
+ # Example:
303
+ #
304
+ #
305
+ # table = [[1,2],[3,4],[5,6]].to_table(%w[col1 col2])
306
+ # table.sigma("col1") #=> 9
307
+ # table.sigma(0) #=> 9
308
+ # table.sigma { |r| r.col1 + r.col2 } #=> 21
309
+ # table.sigma { |r| r.col2 + 1 } #=> 15
310
+ #
311
+ # For the non-mathy, this has been aliased as Table#sum
312
+ def sigma(column=nil)
313
+ inject(0) { |s,r|
314
+ if column
315
+ s + if r[column].kind_of? Numeric
316
+ r[column]
317
+ else
318
+ r[column] =~ /\./ ? r[column].to_f : r[column].to_i
319
+ end
320
+ else
321
+ s + yield(r)
322
+ end
323
+ }
268
324
  end
325
+
326
+ alias_method :sum, :sigma
269
327
 
270
328
  end
329
+
330
+ end
331
+
332
+ module Ruport::Data::TableHelper
333
+ def table(names=[])
334
+ t = [].to_table(names)
335
+ yield(t) if block_given?; t
336
+ end
271
337
  end
@@ -0,0 +1,329 @@
1
+ # The Ruport Data Collections.
2
+ # Authors: Gregory Brown / Dudley Flanders
3
+ #
4
+ # This is Free Software. For details, see LICENSE and COPYING
5
+ # Copyright 2006 by respective content owners, all rights reserved.
6
+
7
+ class Array
8
+ # Converts an array to a Ruport::Data::Table object, ready to
9
+ # use in your reports.
10
+ #
11
+ # [[1,2],[3,4]].to_table(%w[a b])
12
+ def to_table(options={})
13
+ options = { :column_names => options } if options.kind_of? Array
14
+ Ruport::Data::Table.new({:data => self}.merge(options))
15
+ end
16
+ end
17
+
18
+ module Ruport::Data
19
+
20
+ # This class is one of the core classes for building and working with data
21
+ # in Ruport. The idea is to get your data into a standard form, regardless
22
+ # of its source (a database, manual arrays, ActiveRecord, CSVs, etc.).
23
+ #
24
+ # Table is intended to be used as the data store for structured, tabular
25
+ # data - Ruport::Data::Set is an alternate data store intended for less
26
+ # structured data.
27
+ #
28
+ # Once your data is in a Ruport::Data::Table object, it can be manipulated
29
+ # to suit your needs, then used to build a report.
30
+ #
31
+ # Included in this class are methods to create Tables manually and from CSV
32
+ # files.
33
+ #
34
+ # For building a table using ActiveRecord, have a look at Ruport::Reportable.
35
+ class Table < Collection
36
+
37
+ # Creates a new table based on the supplied options.
38
+ # Valid options are :data and :column_names.
39
+ #
40
+ # table = Table.new({:data => [1,2,3], [3,4,5],
41
+ # :column_names => %w[a b c]})
42
+ def initialize(options={})
43
+ @column_names = options[:column_names] ? options[:column_names].dup : []
44
+ @data = []
45
+ if options[:data]
46
+ if options[:data].all? { |r| r.kind_of? Record }
47
+ record_tags = options[:data].map { |r| r.tags }
48
+ options[:data] = options[:data].map { |r| r.to_a }
49
+ end
50
+ options[:data].each { |e| self << e }
51
+ each { |r| r.tags = record_tags.shift } if record_tags
52
+ end
53
+ end
54
+
55
+ attr_reader :column_names
56
+ def_delegator :@data, :[]
57
+ # Sets the column names for this table. The single parameter should be
58
+ # an array listing the names of the columns.
59
+ #
60
+ # tbl = Table.new({:data => [1,2,3], [3,4,5], :column_names => %w[a b c]})
61
+ # tbl.column_names = %w[e f g]
62
+ def column_names=(other)
63
+ @column_names.replace(other.dup)
64
+ end
65
+
66
+ # Compares this table to another table and returns true if
67
+ # both the data and column names are equal
68
+ #
69
+ # one = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
70
+ # two = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
71
+ # one.eql?(two) #=> true
72
+ def eql?(other)
73
+ data.eql?(other.data) && column_names.eql?(other.column_names)
74
+ end
75
+
76
+ alias_method :==, :eql?
77
+
78
+ # Uses Ruport's built-in text plugin to render this table into a string
79
+ #
80
+ # data = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
81
+ # puts data.to_s
82
+ def to_s
83
+ as(:text)
84
+ end
85
+
86
+ # Used to add extra data to the table. The single parameter can be an
87
+ # Array, Hash or Ruport::Data::Record.
88
+ #
89
+ # data = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
90
+ # data << [8,9]
91
+ # data << { :a => 4, :b => 5}
92
+ # data << Ruport::Data::Record.new [5,6], :attributes => %w[a b]
93
+ def <<(other)
94
+ case other
95
+ when Array
96
+ @data << Record.new(other, :attributes => @column_names)
97
+ when Hash
98
+ raise ArgumentError unless @column_names
99
+ arr = @column_names.map { |k| other[k] }
100
+ @data << Record.new(arr, :attributes => @column_names)
101
+ when Record
102
+ raise ArgumentError unless column_names.eql? other.attributes
103
+ @data << Record.new(other.data, :attributes => @column_names)
104
+ @data.last.tags = other.tags.dup
105
+ else
106
+ raise ArgumentError
107
+ end
108
+ self
109
+ end
110
+
111
+ # Used to combine two tables. Throws an ArgumentError if the tables don't
112
+ # have identical columns.
113
+ #
114
+ # inky = Table.new(:data => [[1,2], [3,4]], :column_names => %w[a b])
115
+ # blinky = Table.new(:data => [[5,6]], :column_names => %w[a b])
116
+ # sue = inky + blinky
117
+ # sue.data #=> [[1,2],[3,4],[5,6]]
118
+
119
+ def +(other)
120
+ raise ArgumentError unless other.column_names == @column_names
121
+ Table.new(:column_names => @column_names, :data => @data + other.data)
122
+ end
123
+
124
+ # Reorders the columns that exist in the table. Operates directly
125
+ # on this table.
126
+ #
127
+ # data = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
128
+ # data.reorder!([1,0])
129
+ def reorder!(*indices)
130
+ indices = indices[0] if indices[0].kind_of? Array
131
+ if @column_names && !@column_names.empty?
132
+ x = if indices.all? { |i| i.kind_of? Integer }
133
+ indices.map { |i| @column_names[i] }
134
+ else
135
+ indices
136
+ end
137
+ @column_names = x
138
+ end
139
+ @data.each { |r|
140
+ r.reorder_data!(*indices)
141
+ r.attributes = @column_names
142
+ }; self
143
+ end
144
+
145
+ # Returns a copy of the table with its columns in the requested order.
146
+ #
147
+ # one = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
148
+ # two = one.reorder!([1,0])
149
+ def reorder(*indices)
150
+ dup.reorder!(*indices)
151
+ end
152
+
153
+ # Adds an extra column to the table. Accepts an options Hash as its
154
+ # only parameter which should contain 2 keys - :name and :fill.
155
+ # :name specifies the new columns name, and :fill the default value to
156
+ # use for the column in existing rows.
157
+ #
158
+ # data = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
159
+ # data.append_column({:name => 'new_column', :fill => 1)
160
+ def append_column(options={})
161
+ self.column_names += [options[:name]] if options[:name]
162
+ if block_given?
163
+ each { |r| r.data << yield(r) || options[:fill] }
164
+ else
165
+ each { |r| r.data << options[:fill] }
166
+ end; self
167
+ end
168
+
169
+ # Removes a column from the table. Any values in the specified column are
170
+ # lost.
171
+ # data = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
172
+ # data.append_column({:name => 'new_column', :fill => 1)
173
+ # data.remove_column({:name => 'new_column')
174
+ # data == Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
175
+ # #=> true
176
+ #
177
+ # data = [[1,2],[3,4]].to_table
178
+ # data.remove_column(1)
179
+ # data.eql? [[1],[3]].to_table %w[a] #=> true
180
+ def remove_column(options={})
181
+ if options.kind_of? Integer
182
+ return reorder!((0...data[0].length).to_a - [options])
183
+ elsif options.kind_of? Hash
184
+ name = options[:name]
185
+ else
186
+ name = options
187
+ end
188
+
189
+ raise ArgumentError unless column_names.include? name
190
+ reorder! column_names - [name]
191
+ end
192
+
193
+ # Create a shallow copy of the table: the same data elements are referenced
194
+ # by both the old and new table.
195
+ #
196
+ # one = Table.new({:data => [1,2], [3,4], :column_names => %w[a b]})
197
+ # two = one.dup
198
+ def dup
199
+ a = self.class.new(:data => @data, :column_names => @column_names)
200
+ a.tags = tags.dup
201
+ return a
202
+ end
203
+
204
+ # Loads a CSV file directly into a table using the fasterCSV library.
205
+ #
206
+ # data = Table.load('mydata.csv')
207
+ def self.load(csv_file, options={})
208
+ get_table_from_csv(:foreach, csv_file, options)
209
+ end
210
+
211
+ def self.parse(string, options={})
212
+ get_table_from_csv(:parse,string,options)
213
+ end
214
+
215
+ def self.get_table_from_csv(msg,param,options={})
216
+ options = {:has_names => true,
217
+ :csv_options => {} }.merge(options)
218
+ require "fastercsv"
219
+ loaded_data = self.new
220
+
221
+ first_line = true
222
+ FasterCSV.send(msg,param,options[:csv_options]) do |row|
223
+ if first_line && options[:has_names]
224
+ loaded_data.column_names = row
225
+ first_line = false
226
+ elsif !block_given?
227
+ loaded_data << row
228
+ else
229
+ yield(loaded_data,row)
230
+ end
231
+ end ; loaded_data
232
+ end
233
+
234
+ # Allows you to split tables into multiple tables for grouping.
235
+ #
236
+ # Example:
237
+ #
238
+ # a = Table.new(:column_name => %w[name a b c])
239
+ # a << ["greg",1,2,3]
240
+ # a << ["joe", 2,3,4]
241
+ # a << ["greg",7,8,9]
242
+ # a << ["joe", 1,2,3]
243
+ #
244
+ # b = a.split :group => "name"
245
+ #
246
+ # b.greg.eql? [[1,2,3],[7,8,9]].to_table(%w[a b c]) #=> true
247
+ # b["joe"].eql? [[2,3,4],[1,2,3]].to_table(%w[a b c]) #=> true
248
+ #
249
+ # You can also pass an array to :group, and the resulting attributes in
250
+ # the group will be joined by an underscore.
251
+ #
252
+ # Example:
253
+ #
254
+ # a = Table.new(:column_names => %w[first_name last_name x]
255
+ # a << %w[greg brown foo]
256
+ # a << %w[greg gibson bar]
257
+ # a << %w[greg brown baz]
258
+ #
259
+ # b = a.split :group => %w[first_name last_name]
260
+ # a.greg_brown.length #=> 2
261
+ # a["greg_gibson"].length #=> 1
262
+ # a.greg_brown[0].x #=> "foo"
263
+ def split(options={})
264
+ if options[:group].kind_of? Array
265
+ group = map { |r| options[:group].map { |e| r[e] } }.uniq
266
+ data = group.inject([]) { |s,g|
267
+ s + [select { |r| options[:group].map { |e| r[e] }.eql?(g) }]
268
+ }
269
+ c = column_names - options[:group]
270
+ else
271
+ group = map { |r| r[options[:group]] }.uniq
272
+ data = group.inject([]) { |s,g|
273
+ s + [select { |r| r[options[:group]].eql?(g) }]
274
+ }
275
+ c = column_names - [options[:group]]
276
+
277
+ end
278
+ data.map! { |g|
279
+ Ruport::Data::Table.new(
280
+ :data => g.map { |x| x.reorder(*c) },
281
+ :column_names => c
282
+ )
283
+ }
284
+ rec = if options[:group].kind_of? Array
285
+ Ruport::Data::Record.new(data,
286
+ :attributes => group.map { |e| e.join("_") } )
287
+ else
288
+ Ruport::Data::Record.new data, :attributes => group
289
+ end
290
+ class << rec
291
+ def each_group; attributes.each { |a| yield(a) }; end
292
+ end; rec
293
+ end
294
+
295
+ # Calculates sums. If a column name or index is given, it will try to
296
+ # convert each element of that column to an integer or float
297
+ # and add it together
298
+ #
299
+ # If a block is given, yields each Record so that you can do a calculation.
300
+ #
301
+ #
302
+ # Example:
303
+ #
304
+ #
305
+ # table = [[1,2],[3,4],[5,6]].to_table(%w[col1 col2])
306
+ # table.sigma("col1") #=> 9
307
+ # table.sigma(0) #=> 9
308
+ # table.sigma { |r| r.col1 + r.col2 } #=> 21
309
+ # table.sigma { |r| r.col2 + 1 } #=> 15
310
+ #
311
+ # For the non-mathy, this has been aliased as Table#sum
312
+ def sigma(column=nil)
313
+ inject(0) { |s,r|
314
+ if column
315
+ s + if r[column].kind_of? Numeric
316
+ r[column]
317
+ else
318
+ r[column] =~ /\./ ? r[column].to_f : r[column].to_i
319
+ end
320
+ else
321
+ s + yield(r)
322
+ end
323
+ }
324
+ end
325
+
326
+ alias_method :sum, :sigma
327
+
328
+ end
329
+ end