lean-ruport 0.3.8
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.
Potentially problematic release.
This version of lean-ruport might be problematic. Click here for more details.
- data/ACKNOWLEDGEMENTS +33 -0
- data/AUTHORS +19 -0
- data/CHANGELOG +206 -0
- data/COPYING +340 -0
- data/LICENSE +7 -0
- data/README +209 -0
- data/Rakefile +54 -0
- data/TODO +27 -0
- data/lib/ruport.rb +58 -0
- data/lib/ruport/config.rb +114 -0
- data/lib/ruport/data_row.rb +144 -0
- data/lib/ruport/data_set.rb +221 -0
- data/lib/ruport/format.rb +116 -0
- data/lib/ruport/format/builder.rb +89 -0
- data/lib/ruport/format/document.rb +77 -0
- data/lib/ruport/format/open_node.rb +36 -0
- data/lib/ruport/parser.rb +202 -0
- data/lib/ruport/query.rb +208 -0
- data/lib/ruport/query/sql_split.rb +33 -0
- data/lib/ruport/report.rb +116 -0
- data/lib/ruport/report/mailer.rb +48 -0
- data/test/samples/addressbook.csv +6 -0
- data/test/samples/car_ads.txt +505 -0
- data/test/samples/data.csv +3 -0
- data/test/samples/document.xml +22 -0
- data/test/samples/five_lines.txt +5 -0
- data/test/samples/five_paragraphs.txt +9 -0
- data/test/samples/ross_report.txt +58530 -0
- data/test/samples/ruport_test.sql +8 -0
- data/test/samples/stonecodeblog.sql +279 -0
- data/test/samples/test.sql +2 -0
- data/test/samples/test.yaml +3 -0
- data/test/tc_builder.rb +116 -0
- data/test/tc_config.rb +41 -0
- data/test/tc_data_row.rb +36 -0
- data/test/tc_data_set.rb +141 -0
- data/test/tc_database.rb +25 -0
- data/test/tc_document.rb +42 -0
- data/test/tc_element.rb +18 -0
- data/test/tc_page.rb +42 -0
- data/test/tc_query.rb +55 -0
- data/test/tc_reading.rb +60 -0
- data/test/tc_report.rb +31 -0
- data/test/tc_section.rb +45 -0
- data/test/tc_sql_split.rb +18 -0
- data/test/tc_state.rb +142 -0
- data/test/ts_all.rb +9 -0
- data/test/ts_format.rb +5 -0
- data/test/ts_parser.rb +10 -0
- metadata +102 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
# --
|
2
|
+
# data_row.rb : Ruby Reports row abstraction
|
3
|
+
#
|
4
|
+
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
5
|
+
#
|
6
|
+
# Copyright (c) 2006, All Rights Reserved.
|
7
|
+
#
|
8
|
+
# This is free software. You may modify and redistribute this freely under
|
9
|
+
# your choice of the GNU General Public License or the Ruby License.
|
10
|
+
#
|
11
|
+
# See LICENSE and COPYING for details
|
12
|
+
# ++
|
13
|
+
module Ruport
|
14
|
+
|
15
|
+
# DataRows are Enumerable lists which can be accessed by field name or ordinal
|
16
|
+
# position.
|
17
|
+
#
|
18
|
+
# They feature a tagging system, allowing them to be easily
|
19
|
+
# compared or recalled.
|
20
|
+
#
|
21
|
+
# DataRows form the elements of DataSets
|
22
|
+
#
|
23
|
+
class DataRow
|
24
|
+
|
25
|
+
include Enumerable
|
26
|
+
|
27
|
+
# Takes data and field names as well as some optional parameters and
|
28
|
+
# constructs a DataRow.
|
29
|
+
#
|
30
|
+
#
|
31
|
+
# <tt>data</tt> can be specified in Hash, Array, or DataRow form
|
32
|
+
#
|
33
|
+
# Options:
|
34
|
+
# <tt>:filler</tt>:: this will be used as a default value for empty
|
35
|
+
# <tt>:tags</tt>:: an initial set of tags for the row
|
36
|
+
#
|
37
|
+
#
|
38
|
+
# Examples:
|
39
|
+
# >> Ruport::DataRow.new [1,2,3,4,5], [:a,:b,:c,:d,:e],
|
40
|
+
# :tags => %w[cat dog]
|
41
|
+
# => #<Ruport::DataRow:0xb77e4b04 @fields=[:a, :b, :c, :d, :e],
|
42
|
+
# @data=[1, 2, 3, 4, 5], @tags=["cat", "dog"]>
|
43
|
+
#
|
44
|
+
# >> Ruport::DataRow.new({ :a => 'moo', :c => 'caw'} , [:a,:b,:c,:d,:e],
|
45
|
+
# :tags => %w[cat dog])
|
46
|
+
# => #<Ruport::DataRow:0xb77c298c @fields=[:a, :b, :c, :d, :e],
|
47
|
+
# @data=["moo", nil, "caw", nil, nil], @tags=["cat", "dog"]>
|
48
|
+
#
|
49
|
+
# >> Ruport::DataRow.new [1,2,3], [:a,:b,:c,:d,:e], :tags => %w[cat dog],
|
50
|
+
# :filler => 0
|
51
|
+
# => #<Ruport::DataRow:0xb77bb4d4 @fields=[:a, :b, :c, :d, :e],
|
52
|
+
# @data=[1, 2, 3, 0, 0], @tags=["cat", "dog"]>
|
53
|
+
#
|
54
|
+
def initialize( data, fields, options={} )
|
55
|
+
@fields = fields
|
56
|
+
@tags = options[:tags] || {}
|
57
|
+
@data = []
|
58
|
+
nr_action =
|
59
|
+
if data.kind_of?(Array)
|
60
|
+
lambda { |key, index| @data[index] = data.shift || options[:filler] }
|
61
|
+
elsif data.kind_of?(DataRow)
|
62
|
+
lambda { |key, index| @data = data.to_a }
|
63
|
+
else
|
64
|
+
lambda { |key, index| @data[index] = data[key] || options[:filler] }
|
65
|
+
end
|
66
|
+
@fields.each_with_index { |key, index| nr_action.call(key,index) }
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_accessor :fields, :tags
|
70
|
+
|
71
|
+
# Returns an array of values. Should probably return a DataRow.
|
72
|
+
# Loses field information.
|
73
|
+
def +(other)
|
74
|
+
self.to_a + other.to_a
|
75
|
+
end
|
76
|
+
|
77
|
+
# Lets you access individual fields
|
78
|
+
#
|
79
|
+
# i.e. row["phone"] or row[4]
|
80
|
+
def [](key)
|
81
|
+
key.kind_of?(Fixnum) ? @data[key] : @data[@fields.index(key)]
|
82
|
+
end
|
83
|
+
|
84
|
+
# Lets you set field values
|
85
|
+
#
|
86
|
+
# i.e. row["phone"] = '2038291203', row[7] = "allen"
|
87
|
+
def []=(key,value)
|
88
|
+
if key.kind_of?(Fixnum)
|
89
|
+
@data[key] = value
|
90
|
+
else
|
91
|
+
@data[@fields.index(key)] = value
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
# Converts the DataRow to a plain old Array
|
96
|
+
def to_a
|
97
|
+
@data
|
98
|
+
end
|
99
|
+
|
100
|
+
# Converts the DataRow to a string representation
|
101
|
+
# for outputting to screen.
|
102
|
+
def to_s
|
103
|
+
"[" + @data.join(",") + "]"
|
104
|
+
end
|
105
|
+
|
106
|
+
# Checks to see row includes the tag given.
|
107
|
+
#
|
108
|
+
# Example:
|
109
|
+
#
|
110
|
+
# >> row.has_tag? :running_balance
|
111
|
+
# => true
|
112
|
+
#
|
113
|
+
def has_tag?(tag)
|
114
|
+
@tags.include?(tag)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Iterates through DataRow elements. Accepts a block.
|
118
|
+
def each(&action)
|
119
|
+
@data.each(&action)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Allows you to add a tag to a row.
|
123
|
+
#
|
124
|
+
# Examples:
|
125
|
+
#
|
126
|
+
# row.tag_as(:jay_cross) if row["product"].eql?("im_courier")
|
127
|
+
# row.tag_as(:running_balance) if row.fields.include?("RB")
|
128
|
+
#
|
129
|
+
def tag_as(something)
|
130
|
+
@tags[something] = true
|
131
|
+
end
|
132
|
+
|
133
|
+
# Compares two DataRow objects. If values and fields are the same
|
134
|
+
# (and in the correct order) returns true. Otherwise returns false.
|
135
|
+
def ==(other)
|
136
|
+
self.to_a.eql?(other.to_a) && @fields.eql?(other.fields)
|
137
|
+
end
|
138
|
+
|
139
|
+
# Synonym for DataRow#==
|
140
|
+
def eql?
|
141
|
+
self == other
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# data_set.rb : Ruby Reports core datastructure.
|
2
|
+
#
|
3
|
+
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2006, All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This is free software. You may modify and redistribute this freely under
|
8
|
+
# your choice of the GNU General Public License or the Ruby License.
|
9
|
+
#
|
10
|
+
# See LICENSE and COPYING for details
|
11
|
+
module Ruport
|
12
|
+
|
13
|
+
# The DataSet is the core datastructure for Ruport. It provides methods that
|
14
|
+
# allow you to compare and combine query results, data loaded in from CSVs,
|
15
|
+
# and user-defined sets of data.
|
16
|
+
#
|
17
|
+
# It is tightly integrated with Ruport's formatting and query systems, so if
|
18
|
+
# you'd like to take advantage of these models, you will probably find DataSet
|
19
|
+
# useful.
|
20
|
+
#
|
21
|
+
# Sample Usage:
|
22
|
+
#
|
23
|
+
# my_data = [[1,2,3],[4,5,6],[7,8,9]]
|
24
|
+
#
|
25
|
+
# ds = Ruport::DataSet.new([:col1, :col2, :col3],my_data)
|
26
|
+
# ds << [ 10, 11, 12]
|
27
|
+
# ds << { :col3 => 15, :col1 => 13, :col2 => 14 }
|
28
|
+
# puts ds.select_columns(:col1, :col3).to_csv
|
29
|
+
#
|
30
|
+
# Output:
|
31
|
+
#
|
32
|
+
# col1,col3
|
33
|
+
# 1,3
|
34
|
+
# 4,6
|
35
|
+
# 7,9
|
36
|
+
# 10,12
|
37
|
+
# 13,15
|
38
|
+
#
|
39
|
+
# The wild and crazy might want to try the Array hack:
|
40
|
+
#
|
41
|
+
# puts [[1,2,3],[4,5,6],[7,8,9]].to_ds(%w[col1 col2 col3])
|
42
|
+
#
|
43
|
+
# Output:
|
44
|
+
#
|
45
|
+
# fields: ( col1, col2, col3 )
|
46
|
+
# row0: ( 1, 2, 3 )
|
47
|
+
# row1: ( 4, 5, 6 )
|
48
|
+
# row2: ( 7, 8, 9 )
|
49
|
+
#
|
50
|
+
class DataSet
|
51
|
+
|
52
|
+
include Enumerable
|
53
|
+
|
54
|
+
# DataSets must be given a set of fields to be defined.
|
55
|
+
#
|
56
|
+
# These field names will define the columns for the DataSet and how you
|
57
|
+
# access them.
|
58
|
+
#
|
59
|
+
# data = Ruport::DataSet.new %w[ id name phone ]
|
60
|
+
#
|
61
|
+
# You can optionally pass in some content as well. (Must be Enumerable)
|
62
|
+
#
|
63
|
+
# content = [ %w[ a1 gregory 203-525-0523 ],
|
64
|
+
# %w[ a2 james 555-555-5555 ] ]
|
65
|
+
#
|
66
|
+
# data = Ruport::DataSet.new(%w[ id name phone],content)
|
67
|
+
def initialize(fields=[], content=nil, default=nil)
|
68
|
+
@fields = fields
|
69
|
+
@data = []
|
70
|
+
@default = default
|
71
|
+
content.each { |r| self << r } if content
|
72
|
+
end
|
73
|
+
|
74
|
+
#an array which contains column names
|
75
|
+
attr_accessor :fields
|
76
|
+
|
77
|
+
#the default value to fill empty cells with
|
78
|
+
attr_accessor :default
|
79
|
+
|
80
|
+
#data holds the elements of the Row
|
81
|
+
attr_reader :data
|
82
|
+
|
83
|
+
#provides a deep copy of the DataSet.
|
84
|
+
def clone
|
85
|
+
DataSet.new(@fields,@data)
|
86
|
+
end
|
87
|
+
|
88
|
+
#Allows ordinal access to rows
|
89
|
+
#
|
90
|
+
# my_data[2] -> Ruport::DataRow
|
91
|
+
def [](index)
|
92
|
+
@data[index]
|
93
|
+
end
|
94
|
+
|
95
|
+
#allows setting of rows (providing a DataRow is passed in)
|
96
|
+
def []=(index,value)
|
97
|
+
throw "Invalid object type" unless value.kind_of?(DataRow)
|
98
|
+
@data[index] = value
|
99
|
+
end
|
100
|
+
|
101
|
+
# appends a row to the DataSet
|
102
|
+
# can be added as an array or a keyed hash-like object.
|
103
|
+
#
|
104
|
+
# Columns left undefined will be filled with DataSet#default values.
|
105
|
+
#
|
106
|
+
# data << [ 1, 2, 3 ]
|
107
|
+
# data << { :some_field_name => 3, :other => 2, :another => 1 }
|
108
|
+
def << ( stuff, filler=@default )
|
109
|
+
@data << DataRow.new(stuff,@fields,:filler => filler)
|
110
|
+
end
|
111
|
+
|
112
|
+
# checks if one dataset equals another
|
113
|
+
def eql?(data2)
|
114
|
+
return false unless ( @data.length == data2.data.length and
|
115
|
+
@fields.eql?(data2.fields) )
|
116
|
+
@data.each_with_index do |row, r_index|
|
117
|
+
row.each_with_index do |field, f_index|
|
118
|
+
return false unless field.eql?(data2[r_index][f_index])
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
return true
|
123
|
+
end
|
124
|
+
|
125
|
+
# checks if one dataset equals another
|
126
|
+
def ==(data2)
|
127
|
+
eql?(data2)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns true if DataSet contains no rows, false otherwise.
|
131
|
+
def empty?
|
132
|
+
return @data.empty?
|
133
|
+
end
|
134
|
+
|
135
|
+
# Allows loading of CSV files or YAML dumps. Returns a DataSet
|
136
|
+
#
|
137
|
+
# FasterCSV will be used if it is installed.
|
138
|
+
#
|
139
|
+
# my_data = Ruport::DataSet.load("foo.csv")
|
140
|
+
# my_data = Ruport::DataSet.load("foo.yaml")
|
141
|
+
# my_data = Ruport::DataSet.load("foo.yml")
|
142
|
+
def self.load ( source, default="")
|
143
|
+
case source
|
144
|
+
when /\.(yaml|yml)/
|
145
|
+
return YAML.load(File.open(source))
|
146
|
+
when /\.csv/
|
147
|
+
csv_klass = defined?(FasterCSV) ? FasterCSV : CSV
|
148
|
+
input = csv_klass.read(source) if source =~ /\.csv/
|
149
|
+
loaded_data = self.new
|
150
|
+
loaded_data.fields = input[0]
|
151
|
+
loaded_data.default = default
|
152
|
+
input[1..-1].each { |row| loaded_data << row }
|
153
|
+
return loaded_data
|
154
|
+
else
|
155
|
+
raise "Invalid file type"
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Iterates through the rows, yielding a DataRow for each.
|
160
|
+
def each(&action)
|
161
|
+
@data.each(&action)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns a new DataSet composed of the fields specified.
|
165
|
+
def select_columns(*fields)
|
166
|
+
rows = fields.inject([]) { |s,e| s << map { |row| row[e] } }.transpose
|
167
|
+
my_data = DataSet.new(fields,rows)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Returns a new DataSet with the specified fields removed
|
171
|
+
def remove_columns(*fields)
|
172
|
+
select_fields(*(@fields-fields))
|
173
|
+
end
|
174
|
+
|
175
|
+
# removes the specified fields from this DataSet (DESTRUCTIVE!)
|
176
|
+
def remove_columns!(*fields)
|
177
|
+
@fields -= fields
|
178
|
+
@data = select_fields(*(@fields)).to_a
|
179
|
+
end
|
180
|
+
|
181
|
+
# uses Format::Builder to render DataSets in various ready to output
|
182
|
+
# formats.
|
183
|
+
#
|
184
|
+
# data.as(:html) -> String
|
185
|
+
#
|
186
|
+
# data.as(:text) do |builder|
|
187
|
+
# builder.range = 2..4 -> String
|
188
|
+
# builder.header = "My Title"
|
189
|
+
# end
|
190
|
+
#
|
191
|
+
# To add new formats to this function, simply re-open Format::Builder
|
192
|
+
# and add methods like <tt>render_my_format_name</tt>.
|
193
|
+
#
|
194
|
+
# This will enable <tt>data.as(:my_format_name)</tt>
|
195
|
+
def as(format,&action)
|
196
|
+
builder = Format::Builder.new( self )
|
197
|
+
builder.format = format
|
198
|
+
action.call(builder) if block_given?
|
199
|
+
builder.render
|
200
|
+
end
|
201
|
+
|
202
|
+
# Converts a DataSet to CSV
|
203
|
+
def to_csv; as(:csv) end
|
204
|
+
|
205
|
+
# Converts a Dataset to html
|
206
|
+
def to_html; as(:html) end
|
207
|
+
|
208
|
+
# Readable string representation of the DataSet
|
209
|
+
def to_s; as(:text) end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
class Array
|
214
|
+
|
215
|
+
# Will convert Arrays of Enumerable objects to DataSets.
|
216
|
+
# May have dragons.
|
217
|
+
def to_ds(fields,default=nil)
|
218
|
+
Ruport::DataSet.new(fields,to_a,default)
|
219
|
+
end
|
220
|
+
|
221
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# format.rb : Ruby Reports formatting module
|
2
|
+
#
|
3
|
+
# Author: Gregory T. Brown (gregory.t.brown at gmail dot com)
|
4
|
+
#
|
5
|
+
# Copyright (c) 2006, All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This is free software. You may modify and redistribute this freely under
|
8
|
+
# your choice of the GNU General Public License or the Ruby License.
|
9
|
+
#
|
10
|
+
# See LICENSE and COPYING for details
|
11
|
+
%w[builder open_node document].each { |lib| require "ruport/format/#{lib}" }
|
12
|
+
begin; require "faster_csv"; rescue LoadError; require "csv"; end
|
13
|
+
begin; require "pdf/writer"; rescue LoadError; nil; end
|
14
|
+
module Ruport
|
15
|
+
|
16
|
+
|
17
|
+
# Ruport's Format model is meant to help get your data in a suitable format for
|
18
|
+
# output. Rather than make too many assumptions about how you will want your
|
19
|
+
# data to look, a number of tools have been built so that you can quickly define
|
20
|
+
# those things yourself.
|
21
|
+
#
|
22
|
+
# There are three main sets of functionality the Ruport::Format model provides.
|
23
|
+
# * Structured printable document support ( Format::Document and friends)
|
24
|
+
# * Text filter support ( Report#render and the Format class)
|
25
|
+
# * Support for DataSet Formatting ( Format::Builder)
|
26
|
+
#
|
27
|
+
# The support for structured printable documents is currently geared towards PDF
|
28
|
+
# support and needs some additional work to be truly useful. Suggestions would
|
29
|
+
# be much appreciated.
|
30
|
+
#
|
31
|
+
# Format::Builder lets you define functions that will be used via DataSet#as
|
32
|
+
# This is primary geared towards tabular data output, but there is no reason why
|
33
|
+
# DataSet#as and the <tt>render_foo</tt> methods of Format::Builder cannot be
|
34
|
+
# adapted to fit whatever needs you may need.
|
35
|
+
#
|
36
|
+
# The filters implemented in the Format class are meant to process strings or
|
37
|
+
# entire templates. The Format class will soon automatically build a
|
38
|
+
# Ruport::Parser for any string input. By default, filters are provided to
|
39
|
+
# process erb, pure ruby, and redcloth. It is trivial to extend this
|
40
|
+
# functionality though.
|
41
|
+
#
|
42
|
+
# This is best shown by a simple example:
|
43
|
+
#
|
44
|
+
# a = Ruport::Report.new
|
45
|
+
# Ruport::Format.register_filter :reverser do
|
46
|
+
# content.reverse
|
47
|
+
# end
|
48
|
+
# a.render "somestring", :filters => [:reverser]
|
49
|
+
#
|
50
|
+
# Output: "gnirtsemos"
|
51
|
+
#
|
52
|
+
# Filters can be combined, and you can run them in different orders to obtain
|
53
|
+
# different results.
|
54
|
+
#
|
55
|
+
# See the source for the built in filters for ideas.
|
56
|
+
#
|
57
|
+
# Also, see Report#render for how to bind Format objects to your own classes.
|
58
|
+
#
|
59
|
+
# When combined, filters, data set output templates, and structured printable
|
60
|
+
# document facilities create a complete Formatting system.
|
61
|
+
#
|
62
|
+
# This part of Ruport is under active development. Please do feel free to
|
63
|
+
# submit feature requests or suggestions.
|
64
|
+
class Format
|
65
|
+
|
66
|
+
# To hook up a Format object to your current class, you need to pass it a
|
67
|
+
# binding. This way, when filters are being processed, they will be
|
68
|
+
# evaluated in the context of the object they are being called from, rather
|
69
|
+
# than within an instance of Format.
|
70
|
+
#
|
71
|
+
def initialize(klass_binding)
|
72
|
+
@binding = klass_binding
|
73
|
+
end
|
74
|
+
|
75
|
+
# This is the text to be processed by the filters
|
76
|
+
attr_accessor :content
|
77
|
+
|
78
|
+
# This is the binding to the object Format is tied to
|
79
|
+
attr_accessor :binding
|
80
|
+
|
81
|
+
# Processes the ERB text in <tt>@content</tt> in the context
|
82
|
+
# of the object that Format is bound to.
|
83
|
+
def filter_erb
|
84
|
+
ERB.new(@content).result(@binding)
|
85
|
+
end
|
86
|
+
|
87
|
+
# Processes the RedCloth text in <tt>@content</tt> in the context
|
88
|
+
# of the object that Format is bound to.
|
89
|
+
def filter_red_cloth
|
90
|
+
RedCloth.new(@content).to_html
|
91
|
+
end
|
92
|
+
|
93
|
+
# Processes the ruby code in <tt>@content</tt> in the context
|
94
|
+
# of the object that Format is bound to.
|
95
|
+
#
|
96
|
+
# (Does an eval on the binding)
|
97
|
+
def filter_ruby
|
98
|
+
eval(@content,@binding)
|
99
|
+
end
|
100
|
+
|
101
|
+
# Takes a name and a block and creates a filter method
|
102
|
+
# This will define methods in the form of
|
103
|
+
# <tt>Format#filter_my_filter_name</tt>.
|
104
|
+
#
|
105
|
+
# Example:
|
106
|
+
#
|
107
|
+
# Format.register_filter :no_ohz do
|
108
|
+
# content.gsub(/O/i,"")
|
109
|
+
# end
|
110
|
+
def Format.register_filter(name,&filter_proc)
|
111
|
+
define_method "filter_#{name}".to_sym, &filter_proc
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|