rufus-decision 0.9 → 1.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.
- data/CHANGELOG.txt +10 -0
- data/CREDITS.txt +7 -0
- data/README.txt +13 -3
- data/lib/rufus/decision.rb +484 -484
- data/lib/rufus/hashes.rb +143 -108
- data/lib/rufus-decision.rb +3 -0
- data/test/decision_0_test.rb +293 -293
- data/test/decision_1_test.rb +38 -38
- data/test/dmixin.rb +28 -28
- data/test/eval_test.rb +12 -12
- metadata +13 -6
data/lib/rufus/decision.rb
CHANGED
@@ -8,10 +8,10 @@
|
|
8
8
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
9
|
# copies of the Software, and to permit persons to whom the Software is
|
10
10
|
# furnished to do so, subject to the following conditions:
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# The above copyright notice and this permission notice shall be included in
|
13
13
|
# all copies or substantial portions of the Software.
|
14
|
-
#
|
14
|
+
#
|
15
15
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
16
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
17
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
@@ -40,584 +40,584 @@ require 'rufus/hashes'
|
|
40
40
|
module Rufus
|
41
41
|
|
42
42
|
|
43
|
+
#
|
44
|
+
# Does s starts with prefix ?
|
45
|
+
#
|
46
|
+
def Rufus.starts_with? (s, prefix)
|
47
|
+
|
48
|
+
return false unless s
|
49
|
+
(s[0, prefix.length] == prefix)
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
#
|
54
|
+
# A decision table is a description of a set of rules as a CSV (comma
|
55
|
+
# separated values) file. Such a file can be edited / generated by
|
56
|
+
# a spreadsheet (Excel, Google spreadsheets, Gnumeric, ...)
|
57
|
+
#
|
58
|
+
# == Disclaimer
|
59
|
+
#
|
60
|
+
# The decision / CSV table system is no replacement for
|
61
|
+
# full rule engines with forward and backward chaining, RETE implementation
|
62
|
+
# and the like...
|
63
|
+
#
|
64
|
+
#
|
65
|
+
# == Usage
|
66
|
+
#
|
67
|
+
# The following CSV file
|
68
|
+
#
|
69
|
+
# in:topic,in:region,out:team_member
|
70
|
+
# sports,europe,Alice
|
71
|
+
# sports,,Bob
|
72
|
+
# finance,america,Charly
|
73
|
+
# finance,europe,Donald
|
74
|
+
# finance,,Ernest
|
75
|
+
# politics,asia,Fujio
|
76
|
+
# politics,america,Gilbert
|
77
|
+
# politics,,Henry
|
78
|
+
# ,,Zach
|
79
|
+
#
|
80
|
+
# embodies a rule for distributing items (piece of news) labelled with a
|
81
|
+
# topic and a region to various members of a team.
|
82
|
+
# For example, all news about finance from Europe are to be routed to
|
83
|
+
# Donald.
|
84
|
+
#
|
85
|
+
# Evaluation occurs row by row. The "in out" row states which field
|
86
|
+
# is considered at input and which are to be modified if the "ins" do
|
87
|
+
# match.
|
88
|
+
#
|
89
|
+
# The default behaviour is to change the value of the "outs" if all the
|
90
|
+
# "ins" match and then terminate.
|
91
|
+
# An empty "in" cell means "matches any".
|
92
|
+
#
|
93
|
+
# Enough words, some code :
|
94
|
+
#
|
95
|
+
# table = DecisionTable.new("""
|
96
|
+
# in:topic,in:region,out:team_member
|
97
|
+
# sports,europe,Alice
|
98
|
+
# sports,,Bob
|
99
|
+
# finance,america,Charly
|
100
|
+
# finance,europe,Donald
|
101
|
+
# finance,,Ernest
|
102
|
+
# politics,asia,Fujio
|
103
|
+
# politics,america,Gilbert
|
104
|
+
# politics,,Henry
|
105
|
+
# ,,Zach
|
106
|
+
# """)
|
107
|
+
#
|
108
|
+
# h = {}
|
109
|
+
# h["topic"] = "politics"
|
110
|
+
#
|
111
|
+
# table.transform! h
|
112
|
+
#
|
113
|
+
# puts h["team_member"]
|
114
|
+
# # will yield "Henry" who takes care of all the politics stuff,
|
115
|
+
# # except for Asia and America
|
116
|
+
#
|
117
|
+
# '>', '>=', '<' and '<=' can be put in front of individual cell values :
|
118
|
+
#
|
119
|
+
# table = DecisionTable.new("""
|
120
|
+
# ,
|
121
|
+
# in:fx, out:fy
|
122
|
+
# ,
|
123
|
+
# >100,a
|
124
|
+
# >=10,b
|
125
|
+
# ,c
|
126
|
+
# """)
|
127
|
+
#
|
128
|
+
# h = { 'fx' => '10' }
|
129
|
+
# table.transform! h
|
130
|
+
#
|
131
|
+
# require 'pp'; pp h
|
132
|
+
# # will yield { 'fx' => '10', 'fy' => 'b' }
|
133
|
+
#
|
134
|
+
# Such comparisons are done after the elements are transformed to float
|
135
|
+
# numbers. By default, non-numeric arguments will get compared as Strings.
|
136
|
+
#
|
137
|
+
#
|
138
|
+
# == transform and transform!
|
139
|
+
#
|
140
|
+
# The method transform! acts directly on its parameter hash, the method
|
141
|
+
# transform will act on a copy of it. Both methods return their transformed
|
142
|
+
# hash.
|
143
|
+
#
|
144
|
+
#
|
145
|
+
# == Ruby ranges
|
146
|
+
#
|
147
|
+
# Ruby ranges are also accepted in cells.
|
148
|
+
#
|
149
|
+
# in:f0,out:result
|
150
|
+
# ,
|
151
|
+
# 0..32,low
|
152
|
+
# 33..66,medium
|
153
|
+
# 67..100,high
|
154
|
+
#
|
155
|
+
# will set the field 'result' to 'low' for f0 => 24
|
156
|
+
#
|
157
|
+
#
|
158
|
+
# == Options
|
159
|
+
#
|
160
|
+
# You can put options on their own in a cell BEFORE the line containing
|
161
|
+
# "in:xxx" and "out:yyy" (ins and outs).
|
162
|
+
#
|
163
|
+
# Currently, two options are supported, "ignorecase" and "through".
|
164
|
+
#
|
165
|
+
# * "ignorecase", if found by the DecisionTable will make any match (in the
|
166
|
+
# "in" columns) case unsensitive.
|
167
|
+
#
|
168
|
+
# * "through", will make sure that EVERY row is evaluated and potentially
|
169
|
+
# applied. The default behaviour (without "through"), is to stop the
|
170
|
+
# evaluation after applying the results of the first matching row.
|
171
|
+
#
|
172
|
+
# * "accumulate", behaves as with "through" set but instead of overriding
|
173
|
+
# values each time a match is found, will gather them in an array.
|
174
|
+
#
|
175
|
+
# accumulate
|
176
|
+
# in:f0,out:result
|
177
|
+
# ,
|
178
|
+
# ,normal
|
179
|
+
# >10,large
|
180
|
+
# >100,xl
|
181
|
+
#
|
182
|
+
# will yield { result => [ 'normal', 'large' ]} for f0 => 56
|
183
|
+
#
|
184
|
+
#
|
185
|
+
# == Cross references
|
186
|
+
#
|
187
|
+
# By using the 'dollar notation', it's possible to reference a value
|
188
|
+
# already in the hash.
|
189
|
+
#
|
190
|
+
# in:value,in:roundup,out:newvalue
|
191
|
+
# 0..32,true,32
|
192
|
+
# 33..65,true,65
|
193
|
+
# 66..99,true,99
|
194
|
+
# ,,${value}
|
195
|
+
#
|
196
|
+
# Here, if 'roundup' is set to true, newvalue will hold 32, 65 or 99
|
197
|
+
# as value, else it will simply hold the 'value'.
|
198
|
+
#
|
199
|
+
# The value is the value as currently found in the transformed hash, not
|
200
|
+
# as found in the original (non-transformed) hash.
|
201
|
+
#
|
202
|
+
#
|
203
|
+
# == Ruby code evaluation
|
204
|
+
#
|
205
|
+
# The dollar notation can be used for yet another trick, evaluation of
|
206
|
+
# ruby code at transform time.
|
207
|
+
#
|
208
|
+
# Note though that this feature is only enabled via the :ruby_eval
|
209
|
+
# option of the transform!() method.
|
210
|
+
#
|
211
|
+
# decisionTable.transform! h, :ruby_eval => true
|
212
|
+
#
|
213
|
+
# That decision table may look like :
|
214
|
+
#
|
215
|
+
# in:value,in:result
|
216
|
+
# 0..32,${r:Time.now.to_f}
|
217
|
+
# 33..65,${r:call_that_other_function()}
|
218
|
+
# 66..99,${r:${value} * 3}
|
219
|
+
#
|
220
|
+
# (It's a very simplistic example, but I hope it demonstrates the
|
221
|
+
# capabilities of this technique)
|
222
|
+
#
|
223
|
+
#
|
224
|
+
# == See also
|
225
|
+
#
|
226
|
+
# * http://jmettraux.wordpress.com/2007/02/11/ruby-decision-tables/
|
227
|
+
#
|
228
|
+
class DecisionTable
|
229
|
+
|
230
|
+
#
|
231
|
+
# when set to true, the transformation process stops after the
|
232
|
+
# first match got applied.
|
233
|
+
#
|
234
|
+
attr_accessor :first_match
|
235
|
+
|
236
|
+
#
|
237
|
+
# when set to true, matches evaluation ignores case.
|
238
|
+
#
|
239
|
+
attr_accessor :ignore_case
|
240
|
+
|
241
|
+
#
|
242
|
+
# when set to true, multiple matches result get accumulated in
|
243
|
+
# an array.
|
244
|
+
#
|
245
|
+
attr_accessor :accumulate
|
246
|
+
|
247
|
+
#
|
248
|
+
# The constructor for DecisionTable, you can pass a String, an Array
|
249
|
+
# (of arrays), a File object. The CSV parser coming with Ruby will take
|
250
|
+
# care of it and a DecisionTable instance will be built.
|
251
|
+
#
|
252
|
+
def initialize (csv_data)
|
253
|
+
|
254
|
+
@first_match = true
|
255
|
+
@ignore_case = false
|
256
|
+
@accumulate = false
|
257
|
+
|
258
|
+
@header = nil
|
259
|
+
@rows = []
|
260
|
+
|
261
|
+
csv_array = to_csv_array(csv_data)
|
262
|
+
|
263
|
+
csv_array.each do |row|
|
264
|
+
|
265
|
+
next if empty_row? row
|
266
|
+
|
267
|
+
if @header
|
268
|
+
|
269
|
+
@rows << row.collect { |c| c.strip if c }
|
270
|
+
else
|
271
|
+
|
272
|
+
parse_header_row row
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
43
277
|
#
|
44
|
-
#
|
278
|
+
# Like transform, but the original hash doesn't get touched,
|
279
|
+
# a copy of it gets transformed and finally returned.
|
45
280
|
#
|
46
|
-
def
|
281
|
+
def transform (hash, options={})
|
47
282
|
|
48
|
-
|
49
|
-
(s[0, prefix.length] == prefix)
|
283
|
+
transform! hash.dup, options
|
50
284
|
end
|
51
285
|
|
52
|
-
|
53
|
-
#
|
54
|
-
# A decision table is a description of a set of rules as a CSV (comma
|
55
|
-
# separated values) file. Such a file can be edited / generated by
|
56
|
-
# a spreadsheet (Excel, Google spreadsheets, Gnumeric, ...)
|
57
|
-
#
|
58
|
-
# == Disclaimer
|
59
|
-
#
|
60
|
-
# The decision / CSV table system is no replacement for
|
61
|
-
# full rule engines with forward and backward chaining, RETE implementation
|
62
|
-
# and the like...
|
63
|
-
#
|
64
|
-
#
|
65
|
-
# == Usage
|
66
|
-
#
|
67
|
-
# The following CSV file
|
68
|
-
#
|
69
|
-
# in:topic,in:region,out:team_member
|
70
|
-
# sports,europe,Alice
|
71
|
-
# sports,,Bob
|
72
|
-
# finance,america,Charly
|
73
|
-
# finance,europe,Donald
|
74
|
-
# finance,,Ernest
|
75
|
-
# politics,asia,Fujio
|
76
|
-
# politics,america,Gilbert
|
77
|
-
# politics,,Henry
|
78
|
-
# ,,Zach
|
79
|
-
#
|
80
|
-
# embodies a rule for distributing items (piece of news) labelled with a
|
81
|
-
# topic and a region to various members of a team.
|
82
|
-
# For example, all news about finance from Europe are to be routed to
|
83
|
-
# Donald.
|
84
|
-
#
|
85
|
-
# Evaluation occurs row by row. The "in out" row states which field
|
86
|
-
# is considered at input and which are to be modified if the "ins" do
|
87
|
-
# match.
|
88
|
-
#
|
89
|
-
# The default behaviour is to change the value of the "outs" if all the
|
90
|
-
# "ins" match and then terminate.
|
91
|
-
# An empty "in" cell means "matches any".
|
92
|
-
#
|
93
|
-
# Enough words, some code :
|
94
|
-
#
|
95
|
-
# table = DecisionTable.new("""
|
96
|
-
# in:topic,in:region,out:team_member
|
97
|
-
# sports,europe,Alice
|
98
|
-
# sports,,Bob
|
99
|
-
# finance,america,Charly
|
100
|
-
# finance,europe,Donald
|
101
|
-
# finance,,Ernest
|
102
|
-
# politics,asia,Fujio
|
103
|
-
# politics,america,Gilbert
|
104
|
-
# politics,,Henry
|
105
|
-
# ,,Zach
|
106
|
-
# """)
|
107
|
-
#
|
108
|
-
# h = {}
|
109
|
-
# h["topic"] = "politics"
|
110
|
-
#
|
111
|
-
# table.transform! h
|
112
|
-
#
|
113
|
-
# puts h["team_member"]
|
114
|
-
# # will yield "Henry" who takes care of all the politics stuff,
|
115
|
-
# # except for Asia and America
|
116
|
-
#
|
117
|
-
# '>', '>=', '<' and '<=' can be put in front of individual cell values :
|
118
|
-
#
|
119
|
-
# table = DecisionTable.new("""
|
120
|
-
# ,
|
121
|
-
# in:fx, out:fy
|
122
|
-
# ,
|
123
|
-
# >100,a
|
124
|
-
# >=10,b
|
125
|
-
# ,c
|
126
|
-
# """)
|
127
|
-
#
|
128
|
-
# h = { 'fx' => '10' }
|
129
|
-
# table.transform! h
|
130
|
-
#
|
131
|
-
# require 'pp'; pp h
|
132
|
-
# # will yield { 'fx' => '10', 'fy' => 'b' }
|
133
|
-
#
|
134
|
-
# Such comparisons are done after the elements are transformed to float
|
135
|
-
# numbers. By default, non-numeric arguments will get compared as Strings.
|
136
|
-
#
|
137
|
-
#
|
138
|
-
# == transform and transform!
|
139
|
-
#
|
140
|
-
# The method transform! acts directly on its parameter hash, the method
|
141
|
-
# transform will act on a copy of it. Both methods return their transformed
|
142
|
-
# hash.
|
143
|
-
#
|
144
|
-
#
|
145
|
-
# == Ruby ranges
|
146
|
-
#
|
147
|
-
# Ruby ranges are also accepted in cells.
|
148
|
-
#
|
149
|
-
# in:f0,out:result
|
150
|
-
# ,
|
151
|
-
# 0..32,low
|
152
|
-
# 33..66,medium
|
153
|
-
# 67..100,high
|
154
|
-
#
|
155
|
-
# will set the field 'result' to 'low' for f0 => 24
|
156
|
-
#
|
157
|
-
#
|
158
|
-
# == Options
|
159
|
-
#
|
160
|
-
# You can put options on their own in a cell BEFORE the line containing
|
161
|
-
# "in:xxx" and "out:yyy" (ins and outs).
|
162
|
-
#
|
163
|
-
# Currently, two options are supported, "ignorecase" and "through".
|
164
|
-
#
|
165
|
-
# * "ignorecase", if found by the DecisionTable will make any match (in the "in"
|
166
|
-
# columns) case unsensitive.
|
167
|
-
#
|
168
|
-
# * "through", will make sure that EVERY row is evaluated and potentially
|
169
|
-
# applied. The default behaviour (without "through"), is to stop the
|
170
|
-
# evaluation after applying the results of the first matching row.
|
171
|
-
#
|
172
|
-
# * "accumulate", behaves as with "through" set but instead of overriding
|
173
|
-
# values each time a match is found, will gather them in an array.
|
174
|
-
#
|
175
|
-
# accumulate
|
176
|
-
# in:f0,out:result
|
177
|
-
# ,
|
178
|
-
# ,normal
|
179
|
-
# >10,large
|
180
|
-
# >100,xl
|
181
|
-
#
|
182
|
-
# will yield { result => [ 'normal', 'large' ]} for f0 => 56
|
183
|
-
#
|
184
|
-
#
|
185
|
-
# == Cross references
|
186
|
-
#
|
187
|
-
# By using the 'dollar notation', it's possible to reference a value
|
188
|
-
# already in the hash.
|
189
|
-
#
|
190
|
-
# in:value,in:roundup,out:newvalue
|
191
|
-
# 0..32,true,32
|
192
|
-
# 33..65,true,65
|
193
|
-
# 66..99,true,99
|
194
|
-
# ,,${value}
|
195
|
-
#
|
196
|
-
# Here, if 'roundup' is set to true, newvalue will hold 32, 65 or 99
|
197
|
-
# as value, else it will simply hold the 'value'.
|
198
|
-
#
|
199
|
-
# The value is the value as currently found in the transformed hash, not
|
200
|
-
# as found in the original (non-transformed) hash.
|
201
|
-
#
|
202
|
-
#
|
203
|
-
# == Ruby code evaluation
|
204
|
-
#
|
205
|
-
# The dollar notation can be used for yet another trick, evaluation of
|
206
|
-
# ruby code at transform time.
|
207
|
-
#
|
208
|
-
# Note though that this feature is only enabled via the :ruby_eval
|
209
|
-
# option of the transform!() method.
|
210
|
-
#
|
211
|
-
# decisionTable.transform! h, :ruby_eval => true
|
212
|
-
#
|
213
|
-
# That decision table may look like :
|
214
|
-
#
|
215
|
-
# in:value,in:result
|
216
|
-
# 0..32,${r:Time.now.to_f}
|
217
|
-
# 33..65,${r:call_that_other_function()}
|
218
|
-
# 66..99,${r:${value} * 3}
|
219
|
-
#
|
220
|
-
# (It's a very simplistic example, but I hope it demonstrates the
|
221
|
-
# capabilities of this technique)
|
222
|
-
#
|
223
|
-
#
|
224
|
-
# == See also
|
225
286
|
#
|
226
|
-
#
|
287
|
+
# Passes the hash through the decision table and returns it,
|
288
|
+
# transformed.
|
227
289
|
#
|
228
|
-
|
290
|
+
def transform! (hash, options={})
|
229
291
|
|
230
|
-
|
231
|
-
|
232
|
-
# first match got applied.
|
233
|
-
#
|
234
|
-
attr_accessor :first_match
|
292
|
+
hash = Rufus::EvalHashFilter.new(hash) \
|
293
|
+
if options[:ruby_eval] == true
|
235
294
|
|
236
|
-
|
237
|
-
# when set to true, matches evaluation ignores case.
|
238
|
-
#
|
239
|
-
attr_accessor :ignore_case
|
295
|
+
@rows.each do |row|
|
240
296
|
|
241
|
-
|
242
|
-
# when set to true, multiple matches result get accumulated in
|
243
|
-
# an array.
|
244
|
-
#
|
245
|
-
attr_accessor :accumulate
|
297
|
+
if matches?(row, hash)
|
246
298
|
|
247
|
-
|
248
|
-
|
249
|
-
# (of arrays), a File object. The CSV parser coming with Ruby will take
|
250
|
-
# care of it and a DecisionTable instance will be built.
|
251
|
-
#
|
252
|
-
def initialize (csv_data)
|
253
|
-
|
254
|
-
@first_match = true
|
255
|
-
@ignore_case = false
|
256
|
-
@accumulate = false
|
257
|
-
|
258
|
-
@header = nil
|
259
|
-
@rows = []
|
260
|
-
|
261
|
-
csv_array = to_csv_array(csv_data)
|
262
|
-
|
263
|
-
csv_array.each do |row|
|
264
|
-
|
265
|
-
next if empty_row? row
|
266
|
-
|
267
|
-
if @header
|
268
|
-
|
269
|
-
@rows << row.collect { |c| c.strip if c }
|
270
|
-
else
|
271
|
-
|
272
|
-
parse_header_row row
|
273
|
-
end
|
274
|
-
end
|
299
|
+
apply row, hash
|
300
|
+
break if @first_match
|
275
301
|
end
|
302
|
+
end
|
276
303
|
|
277
|
-
|
278
|
-
|
279
|
-
# a copy of it gets transformed and finally returned.
|
280
|
-
#
|
281
|
-
def transform (hash, options={})
|
282
|
-
|
283
|
-
transform! hash.dup, options
|
284
|
-
end
|
304
|
+
hash
|
305
|
+
end
|
285
306
|
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
def transform! (hash, options={})
|
307
|
+
#
|
308
|
+
# Outputs back this table as a CSV String
|
309
|
+
#
|
310
|
+
def to_csv
|
291
311
|
|
292
|
-
|
293
|
-
|
312
|
+
s = ""
|
313
|
+
s << @header.to_csv
|
314
|
+
s << "\n"
|
315
|
+
@rows.each do |row|
|
316
|
+
s << row.join(",")
|
317
|
+
s << "\n"
|
318
|
+
end
|
319
|
+
s
|
320
|
+
end
|
294
321
|
|
295
|
-
|
322
|
+
private
|
296
323
|
|
297
|
-
|
324
|
+
def parse_uri (string)
|
298
325
|
|
299
|
-
|
300
|
-
break if @first_match
|
301
|
-
end
|
302
|
-
end
|
326
|
+
return nil if string.split("\n").size > 1
|
303
327
|
|
304
|
-
|
328
|
+
begin
|
329
|
+
return URI::parse(string)
|
330
|
+
rescue Exception => e
|
305
331
|
end
|
306
332
|
|
307
|
-
|
308
|
-
|
309
|
-
#
|
310
|
-
def to_csv
|
333
|
+
nil
|
334
|
+
end
|
311
335
|
|
312
|
-
|
313
|
-
s << @header.to_csv
|
314
|
-
s << "\n"
|
315
|
-
@rows.each do |row|
|
316
|
-
s << row.join(",")
|
317
|
-
s << "\n"
|
318
|
-
end
|
319
|
-
s
|
320
|
-
end
|
321
|
-
|
322
|
-
private
|
336
|
+
def to_csv_array (csv_data)
|
323
337
|
|
324
|
-
|
338
|
+
return csv_data if csv_data.kind_of?(Array)
|
325
339
|
|
326
|
-
|
340
|
+
csv_data = csv_data.to_s if csv_data.is_a?(URI)
|
327
341
|
|
328
|
-
|
329
|
-
return URI::parse(string)
|
330
|
-
rescue Exception => e
|
331
|
-
end
|
342
|
+
csv_data = open(csv_data) if parse_uri(csv_data)
|
332
343
|
|
333
|
-
|
334
|
-
|
344
|
+
CSV::Reader.parse csv_data
|
345
|
+
end
|
335
346
|
|
336
|
-
|
347
|
+
def matches? (row, hash)
|
337
348
|
|
338
|
-
|
349
|
+
return false if empty_row?(row)
|
339
350
|
|
340
|
-
|
351
|
+
#puts
|
352
|
+
#puts "__row match ?"
|
353
|
+
#p row
|
341
354
|
|
342
|
-
|
355
|
+
@header.ins.each_with_index do |in_header, icol|
|
343
356
|
|
344
|
-
|
345
|
-
end
|
357
|
+
in_header = resolve_in_header(in_header)
|
346
358
|
|
347
|
-
|
359
|
+
value = Rufus::dsub in_header, hash
|
348
360
|
|
349
|
-
|
361
|
+
cell = row[icol]
|
350
362
|
|
351
|
-
|
352
|
-
#puts "__row match ?"
|
353
|
-
#p row
|
363
|
+
next if not cell
|
354
364
|
|
355
|
-
|
365
|
+
cell = cell.strip
|
356
366
|
|
357
|
-
|
367
|
+
next if cell.length < 1
|
358
368
|
|
359
|
-
|
369
|
+
cell = Rufus::dsub cell, hash
|
360
370
|
|
361
|
-
|
371
|
+
#puts "__does '#{value}' match '#{cell}' ?"
|
362
372
|
|
363
|
-
|
373
|
+
c = cell[0, 1]
|
364
374
|
|
365
|
-
|
375
|
+
b = if c == '<' or c == '>'
|
366
376
|
|
367
|
-
|
377
|
+
numeric_compare value, cell
|
378
|
+
else
|
368
379
|
|
369
|
-
|
380
|
+
range = to_ruby_range cell
|
370
381
|
|
371
|
-
|
382
|
+
if range
|
383
|
+
range.include?(value)
|
384
|
+
else
|
385
|
+
regex_compare value, cell
|
386
|
+
end
|
387
|
+
end
|
372
388
|
|
373
|
-
|
389
|
+
return false unless b
|
390
|
+
end
|
374
391
|
|
375
|
-
|
392
|
+
#puts "__row matches"
|
376
393
|
|
377
|
-
|
378
|
-
|
394
|
+
true
|
395
|
+
end
|
379
396
|
|
380
|
-
|
397
|
+
def regex_compare (value, cell)
|
381
398
|
|
382
|
-
|
383
|
-
|
384
|
-
else
|
385
|
-
regex_compare value, cell
|
386
|
-
end
|
387
|
-
end
|
399
|
+
modifiers = 0
|
400
|
+
modifiers += Regexp::IGNORECASE if @ignore_case
|
388
401
|
|
389
|
-
|
390
|
-
end
|
402
|
+
rcell = Regexp.new(cell, modifiers)
|
391
403
|
|
392
|
-
|
404
|
+
rcell.match(value)
|
405
|
+
end
|
393
406
|
|
394
|
-
|
395
|
-
end
|
407
|
+
def numeric_compare (value, cell)
|
396
408
|
|
397
|
-
|
409
|
+
comparator = cell[0, 1]
|
410
|
+
comparator += "=" if cell[1, 1] == "="
|
411
|
+
cell = cell[comparator.length..-1]
|
398
412
|
|
399
|
-
|
400
|
-
|
413
|
+
nvalue = narrow(value)
|
414
|
+
ncell = narrow(cell)
|
401
415
|
|
402
|
-
|
416
|
+
if nvalue.is_a? String or ncell.is_a? String
|
417
|
+
value = '"' + value + '"'
|
418
|
+
cell = '"' + cell + '"'
|
419
|
+
else
|
420
|
+
value = nvalue
|
421
|
+
cell = ncell
|
422
|
+
end
|
403
423
|
|
404
|
-
|
405
|
-
end
|
424
|
+
s = "#{value} #{comparator} #{cell}"
|
406
425
|
|
407
|
-
|
426
|
+
#puts "...>>>#{s}<<<"
|
408
427
|
|
409
|
-
|
410
|
-
|
411
|
-
|
428
|
+
begin
|
429
|
+
return Rufus::check_and_eval(s)
|
430
|
+
rescue Exception => e
|
431
|
+
end
|
412
432
|
|
413
|
-
|
414
|
-
|
433
|
+
false
|
434
|
+
end
|
415
435
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
436
|
+
def narrow (s)
|
437
|
+
begin
|
438
|
+
return Float(s)
|
439
|
+
rescue Exception => e
|
440
|
+
end
|
441
|
+
s
|
442
|
+
end
|
423
443
|
|
424
|
-
|
444
|
+
def resolve_in_header (in_header)
|
425
445
|
|
426
|
-
|
446
|
+
"${#{in_header}}"
|
447
|
+
end
|
427
448
|
|
428
|
-
|
429
|
-
return Rufus::eval_safely(s, 4)
|
430
|
-
rescue Exception => e
|
431
|
-
end
|
449
|
+
def apply (row, hash)
|
432
450
|
|
433
|
-
|
434
|
-
end
|
451
|
+
@header.outs.each_with_index do |out_header, icol|
|
435
452
|
|
436
|
-
|
437
|
-
begin
|
438
|
-
return Float(s)
|
439
|
-
rescue Exception => e
|
440
|
-
end
|
441
|
-
s
|
442
|
-
end
|
453
|
+
next unless out_header
|
443
454
|
|
444
|
-
|
455
|
+
value = row[icol]
|
445
456
|
|
446
|
-
|
447
|
-
|
457
|
+
next unless value
|
458
|
+
#next unless value.strip.length > 0
|
459
|
+
next unless value.length > 0
|
448
460
|
|
449
|
-
|
461
|
+
value = Rufus::dsub value, hash
|
450
462
|
|
451
|
-
|
463
|
+
hash[out_header] = if @accumulate
|
464
|
+
#
|
465
|
+
# accumulate
|
466
|
+
|
467
|
+
v = hash[out_header]
|
468
|
+
if v and v.is_a?(Array)
|
469
|
+
v + Array(value)
|
470
|
+
elsif v
|
471
|
+
[ v, value ]
|
472
|
+
else
|
473
|
+
value
|
474
|
+
end
|
475
|
+
else
|
476
|
+
#
|
477
|
+
# override
|
452
478
|
|
453
|
-
|
479
|
+
value
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
454
483
|
|
455
|
-
|
484
|
+
def parse_header_row (row)
|
456
485
|
|
457
|
-
|
458
|
-
#next unless value.strip.length > 0
|
459
|
-
next unless value.length > 0
|
486
|
+
row.each_with_index do |cell, icol|
|
460
487
|
|
461
|
-
|
488
|
+
next unless cell
|
462
489
|
|
463
|
-
|
464
|
-
|
465
|
-
# accumulate
|
490
|
+
cell = cell.strip
|
491
|
+
s = cell.downcase
|
466
492
|
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
[ v, value ]
|
472
|
-
else
|
473
|
-
value
|
474
|
-
end
|
475
|
-
else
|
476
|
-
#
|
477
|
-
# override
|
493
|
+
if s == "ignorecase" or s == "ignore_case"
|
494
|
+
@ignore_case = true
|
495
|
+
next
|
496
|
+
end
|
478
497
|
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
498
|
+
if s == "through"
|
499
|
+
@first_match = false
|
500
|
+
next
|
501
|
+
end
|
483
502
|
|
484
|
-
|
503
|
+
if s == "accumulate"
|
504
|
+
@first_match = false
|
505
|
+
@accumulate = true
|
506
|
+
next
|
507
|
+
end
|
485
508
|
|
486
|
-
|
509
|
+
if Rufus::starts_with?(cell, "in:") or \
|
510
|
+
Rufus::starts_with?(cell, "out:")
|
487
511
|
|
488
|
-
|
512
|
+
@header = Header.new unless @header
|
513
|
+
@header.add cell, icol
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
489
517
|
|
490
|
-
|
491
|
-
s = cell.downcase
|
518
|
+
def empty_row? (row)
|
492
519
|
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
520
|
+
return true unless row
|
521
|
+
return true if (row.length == 1 and not row[0])
|
522
|
+
row.each do |cell|
|
523
|
+
return false if cell
|
524
|
+
end
|
525
|
+
true
|
526
|
+
end
|
527
|
+
|
528
|
+
#
|
529
|
+
# A regexp for checking if a string is a numeric Ruby range
|
530
|
+
#
|
531
|
+
RUBY_NUMERIC_RANGE_REGEXP = Regexp.compile(
|
532
|
+
"^\\d+(\\.\\d+)?\\.{2,3}\\d+(\\.\\d+)?$")
|
533
|
+
|
534
|
+
#
|
535
|
+
# A regexp for checking if a string is an alpha Ruby range
|
536
|
+
#
|
537
|
+
RUBY_ALPHA_RANGE_REGEXP = Regexp.compile(
|
538
|
+
"^([A-Za-z])(\\.{2,3})([A-Za-z])$")
|
539
|
+
|
540
|
+
#
|
541
|
+
# If the string contains a Ruby range definition
|
542
|
+
# (ie something like "93.0..94.5" or "56..72"), it will return
|
543
|
+
# the Range instance.
|
544
|
+
# Will return nil else.
|
545
|
+
#
|
546
|
+
# The Ruby range returned (if any) will accept String or Numeric,
|
547
|
+
# ie (4..6).include?("5") will yield true.
|
548
|
+
#
|
549
|
+
def to_ruby_range (s)
|
550
|
+
|
551
|
+
range = if RUBY_NUMERIC_RANGE_REGEXP.match(s)
|
552
|
+
|
553
|
+
eval s
|
554
|
+
|
555
|
+
else
|
556
|
+
|
557
|
+
m = RUBY_ALPHA_RANGE_REGEXP.match(s)
|
558
|
+
|
559
|
+
if m
|
560
|
+
eval "'#{m[1]}'#{m[2]}'#{m[3]}'"
|
561
|
+
else
|
562
|
+
nil
|
563
|
+
end
|
564
|
+
end
|
497
565
|
|
498
|
-
|
499
|
-
@first_match = false
|
500
|
-
next
|
501
|
-
end
|
566
|
+
class << range
|
502
567
|
|
503
|
-
|
504
|
-
@first_match = false
|
505
|
-
@accumulate = true
|
506
|
-
next
|
507
|
-
end
|
568
|
+
alias :old_include? :include?
|
508
569
|
|
509
|
-
|
510
|
-
Rufus::starts_with?(cell, "out:")
|
570
|
+
def include? (elt)
|
511
571
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
572
|
+
elt = if first.is_a?(Numeric)
|
573
|
+
Float(elt)
|
574
|
+
else
|
575
|
+
elt
|
516
576
|
end
|
517
577
|
|
518
|
-
|
578
|
+
old_include?(elt)
|
579
|
+
end
|
519
580
|
|
520
|
-
|
521
|
-
return true if (row.length == 1 and not row[0])
|
522
|
-
row.each do |cell|
|
523
|
-
return false if cell
|
524
|
-
end
|
525
|
-
true
|
526
|
-
end
|
581
|
+
end if range
|
527
582
|
|
528
|
-
|
529
|
-
|
530
|
-
#
|
531
|
-
RUBY_NUMERIC_RANGE_REGEXP = Regexp.compile(
|
532
|
-
"^\\d+(\\.\\d+)?\\.{2,3}\\d+(\\.\\d+)?$")
|
533
|
-
|
534
|
-
#
|
535
|
-
# A regexp for checking if a string is an alpha Ruby range
|
536
|
-
#
|
537
|
-
RUBY_ALPHA_RANGE_REGEXP = Regexp.compile(
|
538
|
-
"^([A-Za-z])(\\.{2,3})([A-Za-z])$")
|
539
|
-
|
540
|
-
#
|
541
|
-
# If the string contains a Ruby range definition
|
542
|
-
# (ie something like "93.0..94.5" or "56..72"), it will return
|
543
|
-
# the Range instance.
|
544
|
-
# Will return nil else.
|
545
|
-
#
|
546
|
-
# The Ruby range returned (if any) will accept String or Numeric,
|
547
|
-
# ie (4..6).include?("5") will yield true.
|
548
|
-
#
|
549
|
-
def to_ruby_range (s)
|
550
|
-
|
551
|
-
range = if RUBY_NUMERIC_RANGE_REGEXP.match(s)
|
552
|
-
|
553
|
-
eval s
|
554
|
-
|
555
|
-
else
|
556
|
-
|
557
|
-
m = RUBY_ALPHA_RANGE_REGEXP.match(s)
|
558
|
-
|
559
|
-
if m
|
560
|
-
eval "'#{m[1]}'#{m[2]}'#{m[3]}'"
|
561
|
-
else
|
562
|
-
nil
|
563
|
-
end
|
564
|
-
end
|
565
|
-
|
566
|
-
class << range
|
567
|
-
|
568
|
-
alias :old_include? :include?
|
569
|
-
|
570
|
-
def include? (elt)
|
571
|
-
|
572
|
-
elt = if first.is_a?(Numeric)
|
573
|
-
Float(elt)
|
574
|
-
else
|
575
|
-
elt
|
576
|
-
end
|
577
|
-
|
578
|
-
old_include?(elt)
|
579
|
-
end
|
580
|
-
|
581
|
-
end if range
|
582
|
-
|
583
|
-
range
|
584
|
-
end
|
583
|
+
range
|
584
|
+
end
|
585
585
|
|
586
|
-
|
586
|
+
class Header
|
587
587
|
|
588
|
-
|
588
|
+
attr_accessor :ins, :outs
|
589
589
|
|
590
|
-
|
590
|
+
def initialize
|
591
591
|
|
592
|
-
|
593
|
-
|
594
|
-
|
592
|
+
@ins = []
|
593
|
+
@outs = []
|
594
|
+
end
|
595
595
|
|
596
|
-
|
596
|
+
def add (cell, icol)
|
597
597
|
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
598
|
+
if Rufus::starts_with?(cell, "in:")
|
599
|
+
@ins[icol] = cell[3..-1]
|
600
|
+
#puts "i added #{@ins[icol]}"
|
601
|
+
elsif Rufus::starts_with?(cell, "out:")
|
602
|
+
@outs[icol] = cell[4..-1]
|
603
|
+
#puts "o added #{@outs[icol]}"
|
604
|
+
end
|
605
|
+
# else don't add
|
606
|
+
end
|
607
607
|
|
608
|
-
|
608
|
+
def to_csv
|
609
609
|
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
|
617
|
-
|
618
|
-
|
619
|
-
|
620
|
-
|
610
|
+
s = ""
|
611
|
+
@ins.each do |_in|
|
612
|
+
s << "in:#{_in}," if _in
|
613
|
+
end
|
614
|
+
@outs.each do |out|
|
615
|
+
s << "out:#{out}," if out
|
616
|
+
end
|
617
|
+
s[0..-2]
|
618
|
+
end
|
619
|
+
end
|
620
|
+
end
|
621
621
|
|
622
622
|
end
|
623
623
|
|