jsanders-ruport 1.7.1
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.
- data/AUTHORS +48 -0
- data/LICENSE +59 -0
- data/README +114 -0
- data/Rakefile +93 -0
- data/examples/RWEmerson.jpg +0 -0
- data/examples/anon.rb +43 -0
- data/examples/btree/commaleon/commaleon.rb +263 -0
- data/examples/btree/commaleon/sample_data/ticket_count.csv +124 -0
- data/examples/btree/commaleon/sample_data/ticket_count2.csv +119 -0
- data/examples/centered_pdf_text_box.rb +83 -0
- data/examples/data/tattle.dump +82 -0
- data/examples/example.csv +3 -0
- data/examples/line_plotter.rb +61 -0
- data/examples/pdf_report_with_common_base.rb +72 -0
- data/examples/png_embed.rb +54 -0
- data/examples/roadmap.png +0 -0
- data/examples/row_renderer.rb +39 -0
- data/examples/simple_pdf_lines.rb +25 -0
- data/examples/simple_templating_example.rb +34 -0
- data/examples/tattle_ruby_version.rb +39 -0
- data/examples/tattle_rubygems_version.rb +37 -0
- data/examples/trac_ticket_status.rb +59 -0
- data/lib/ruport.rb +127 -0
- data/lib/ruport/controller.rb +616 -0
- data/lib/ruport/controller/grouping.rb +71 -0
- data/lib/ruport/controller/table.rb +54 -0
- data/lib/ruport/data.rb +4 -0
- data/lib/ruport/data/feeder.rb +111 -0
- data/lib/ruport/data/grouping.rb +399 -0
- data/lib/ruport/data/record.rb +297 -0
- data/lib/ruport/data/table.rb +950 -0
- data/lib/ruport/extensions.rb +4 -0
- data/lib/ruport/formatter.rb +254 -0
- data/lib/ruport/formatter/csv.rb +149 -0
- data/lib/ruport/formatter/html.rb +161 -0
- data/lib/ruport/formatter/pdf.rb +591 -0
- data/lib/ruport/formatter/template.rb +187 -0
- data/lib/ruport/formatter/text.rb +231 -0
- data/lib/uport.rb +1 -0
- data/test/controller_test.rb +743 -0
- data/test/csv_formatter_test.rb +164 -0
- data/test/data_feeder_test.rb +88 -0
- data/test/grouping_test.rb +410 -0
- data/test/helpers.rb +11 -0
- data/test/html_formatter_test.rb +201 -0
- data/test/pdf_formatter_test.rb +354 -0
- data/test/record_test.rb +332 -0
- data/test/samples/addressbook.csv +6 -0
- data/test/samples/data.csv +3 -0
- data/test/samples/data.tsv +3 -0
- data/test/samples/dates.csv +1409 -0
- data/test/samples/erb_test.sql +1 -0
- data/test/samples/query_test.sql +1 -0
- data/test/samples/ruport_test.sql +8 -0
- data/test/samples/test.sql +2 -0
- data/test/samples/test.yaml +3 -0
- data/test/samples/ticket_count.csv +124 -0
- data/test/table_pivot_test.rb +134 -0
- data/test/table_test.rb +838 -0
- data/test/template_test.rb +48 -0
- data/test/text_formatter_test.rb +258 -0
- data/util/bench/data/record/bench_as_vs_to.rb +18 -0
- data/util/bench/data/record/bench_constructor.rb +46 -0
- data/util/bench/data/record/bench_indexing.rb +65 -0
- data/util/bench/data/record/bench_reorder.rb +35 -0
- data/util/bench/data/record/bench_to_a.rb +19 -0
- data/util/bench/data/table/bench_column_manip.rb +103 -0
- data/util/bench/data/table/bench_dup.rb +24 -0
- data/util/bench/data/table/bench_init.rb +67 -0
- data/util/bench/data/table/bench_manip.rb +125 -0
- data/util/bench/formatter/bench_csv.rb +14 -0
- data/util/bench/formatter/bench_html.rb +14 -0
- data/util/bench/formatter/bench_pdf.rb +14 -0
- data/util/bench/formatter/bench_text.rb +14 -0
- data/util/bench/samples/tattle.csv +1237 -0
- 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
|