ruport 0.5.4 → 0.6.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.
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