rufus-decision 0.9
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/README.txt +105 -0
- data/lib/rufus/decision.rb +623 -0
- data/lib/rufus/hashes.rb +185 -0
- data/test/decision_0_test.rb +479 -0
- data/test/decision_1_test.rb +69 -0
- data/test/dmixin.rb +55 -0
- data/test/eval_test.rb +36 -0
- data/test/test.rb +5 -0
- metadata +79 -0
data/README.txt
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
|
|
2
|
+
= rufus-decision
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
== getting it
|
|
6
|
+
|
|
7
|
+
sudo gem install rufus-decision
|
|
8
|
+
|
|
9
|
+
or at
|
|
10
|
+
|
|
11
|
+
http://rubyforge.org/frs/?group_id=4812
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
== usage
|
|
15
|
+
|
|
16
|
+
An example where a few rules determine which salesperson should interact with a customer with given characteristics.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
require 'rubygems'
|
|
20
|
+
require 'rufus/decision'
|
|
21
|
+
|
|
22
|
+
include Rufus
|
|
23
|
+
|
|
24
|
+
TABLE = DecisionTable.new("""
|
|
25
|
+
in:age,in:trait,out:salesperson
|
|
26
|
+
|
|
27
|
+
18..35,,adeslky
|
|
28
|
+
25..35,,bronco
|
|
29
|
+
36..50,,espadas
|
|
30
|
+
51..78,,thorsten
|
|
31
|
+
44..120,,ojiisan
|
|
32
|
+
|
|
33
|
+
25..35,rich,kerfelden
|
|
34
|
+
,cheerful,swanson
|
|
35
|
+
,maniac,korolev
|
|
36
|
+
""")
|
|
37
|
+
|
|
38
|
+
#
|
|
39
|
+
# Given a customer (a Hash instance directly, for
|
|
40
|
+
# convenience), returns the name of the first
|
|
41
|
+
# corresponding salesman.
|
|
42
|
+
#
|
|
43
|
+
def determine_salesperson (customer)
|
|
44
|
+
|
|
45
|
+
TABLE.transform(customer)["salesperson"]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
puts determine_salesperson(
|
|
49
|
+
"age" => 72)
|
|
50
|
+
# => thorsten
|
|
51
|
+
|
|
52
|
+
puts determine_salesperson(
|
|
53
|
+
"age" => 25, "trait" => "rich")
|
|
54
|
+
# => adeslky
|
|
55
|
+
|
|
56
|
+
puts determine_salesperson(
|
|
57
|
+
"age" => 23, "trait" => "cheerful")
|
|
58
|
+
# => adeslky
|
|
59
|
+
|
|
60
|
+
puts determine_salesperson(
|
|
61
|
+
"age" => 25, "trait" => "maniac")
|
|
62
|
+
# => adeslky
|
|
63
|
+
|
|
64
|
+
puts determine_salesperson(
|
|
65
|
+
"age" => 44, "trait" => "maniac")
|
|
66
|
+
# => espadas
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
More at Rufus::DecisionTable
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
= dependencies
|
|
73
|
+
|
|
74
|
+
The gem 'rufus-dollar' (http://rufus.rubyforge.org/rufus-dollar) and the 'rufus-eval' gem (http://rufus.rubyforge.org/rufus-eval).
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
== mailing list
|
|
78
|
+
|
|
79
|
+
On the rufus-ruby list[http://groups.google.com/group/rufus-ruby] :
|
|
80
|
+
|
|
81
|
+
http://groups.google.com/group/rufus-ruby
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
== issue tracker
|
|
85
|
+
|
|
86
|
+
http://rubyforge.org/tracker/?atid=18584&group_id=4812&func=browse
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
== source
|
|
90
|
+
|
|
91
|
+
http://rufus.rubyforge.org/svn/trunk/mnemo
|
|
92
|
+
|
|
93
|
+
svn checkout http://rufus.rubyforge.org/svn/trunk/mnemo
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
== author
|
|
97
|
+
|
|
98
|
+
John Mettraux, jmettraux@gmail.com
|
|
99
|
+
http://jmettraux.wordpress.com
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
== license
|
|
103
|
+
|
|
104
|
+
MIT
|
|
105
|
+
|
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
#
|
|
2
|
+
#--
|
|
3
|
+
# Copyright (c) 2007-2008, John Mettraux, jmettraux@gmail.com
|
|
4
|
+
#
|
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
# in the Software without restriction, including without limitation the rights
|
|
8
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
# furnished to do so, subject to the following conditions:
|
|
11
|
+
#
|
|
12
|
+
# The above copyright notice and this permission notice shall be included in
|
|
13
|
+
# all copies or substantial portions of the Software.
|
|
14
|
+
#
|
|
15
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
# THE SOFTWARE.
|
|
22
|
+
#++
|
|
23
|
+
#
|
|
24
|
+
|
|
25
|
+
#
|
|
26
|
+
# "made in Japan"
|
|
27
|
+
#
|
|
28
|
+
# John Mettraux at openwfe.org
|
|
29
|
+
#
|
|
30
|
+
|
|
31
|
+
require 'csv'
|
|
32
|
+
require 'open-uri'
|
|
33
|
+
|
|
34
|
+
require 'rubygems'
|
|
35
|
+
require 'rufus/dollar'
|
|
36
|
+
|
|
37
|
+
require 'rufus/hashes'
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
module Rufus
|
|
41
|
+
|
|
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 "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
|
+
#
|
|
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
|
+
|
|
277
|
+
#
|
|
278
|
+
# Like transform, but the original hash doesn't get touched,
|
|
279
|
+
# a copy of it gets transformed and finally returned.
|
|
280
|
+
#
|
|
281
|
+
def transform (hash, options={})
|
|
282
|
+
|
|
283
|
+
transform! hash.dup, options
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
#
|
|
287
|
+
# Passes the hash through the decision table and returns it,
|
|
288
|
+
# transformed.
|
|
289
|
+
#
|
|
290
|
+
def transform! (hash, options={})
|
|
291
|
+
|
|
292
|
+
hash = Rufus::EvalHashFilter.new(hash) \
|
|
293
|
+
if options[:ruby_eval] == true
|
|
294
|
+
|
|
295
|
+
@rows.each do |row|
|
|
296
|
+
|
|
297
|
+
if matches?(row, hash)
|
|
298
|
+
|
|
299
|
+
apply row, hash
|
|
300
|
+
break if @first_match
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
hash
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
#
|
|
308
|
+
# Outputs back this table as a CSV String
|
|
309
|
+
#
|
|
310
|
+
def to_csv
|
|
311
|
+
|
|
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
|
|
321
|
+
|
|
322
|
+
private
|
|
323
|
+
|
|
324
|
+
def parse_uri (string)
|
|
325
|
+
|
|
326
|
+
return nil if string.split("\n").size > 1
|
|
327
|
+
|
|
328
|
+
begin
|
|
329
|
+
return URI::parse(string)
|
|
330
|
+
rescue Exception => e
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
nil
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def to_csv_array (csv_data)
|
|
337
|
+
|
|
338
|
+
return csv_data if csv_data.kind_of?(Array)
|
|
339
|
+
|
|
340
|
+
csv_data = csv_data.to_s if csv_data.is_a?(URI)
|
|
341
|
+
|
|
342
|
+
csv_data = open(csv_data) if parse_uri(csv_data)
|
|
343
|
+
|
|
344
|
+
CSV::Reader.parse csv_data
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def matches? (row, hash)
|
|
348
|
+
|
|
349
|
+
return false if empty_row?(row)
|
|
350
|
+
|
|
351
|
+
#puts
|
|
352
|
+
#puts "__row match ?"
|
|
353
|
+
#p row
|
|
354
|
+
|
|
355
|
+
@header.ins.each_with_index do |in_header, icol|
|
|
356
|
+
|
|
357
|
+
in_header = resolve_in_header(in_header)
|
|
358
|
+
|
|
359
|
+
value = Rufus::dsub in_header, hash
|
|
360
|
+
|
|
361
|
+
cell = row[icol]
|
|
362
|
+
|
|
363
|
+
next if not cell
|
|
364
|
+
|
|
365
|
+
cell = cell.strip
|
|
366
|
+
|
|
367
|
+
next if cell.length < 1
|
|
368
|
+
|
|
369
|
+
cell = Rufus::dsub cell, hash
|
|
370
|
+
|
|
371
|
+
#puts "__does '#{value}' match '#{cell}' ?"
|
|
372
|
+
|
|
373
|
+
c = cell[0, 1]
|
|
374
|
+
|
|
375
|
+
b = if c == '<' or c == '>'
|
|
376
|
+
|
|
377
|
+
numeric_compare value, cell
|
|
378
|
+
else
|
|
379
|
+
|
|
380
|
+
range = to_ruby_range cell
|
|
381
|
+
|
|
382
|
+
if range
|
|
383
|
+
range.include?(value)
|
|
384
|
+
else
|
|
385
|
+
regex_compare value, cell
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
return false unless b
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
#puts "__row matches"
|
|
393
|
+
|
|
394
|
+
true
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def regex_compare (value, cell)
|
|
398
|
+
|
|
399
|
+
modifiers = 0
|
|
400
|
+
modifiers += Regexp::IGNORECASE if @ignore_case
|
|
401
|
+
|
|
402
|
+
rcell = Regexp.new(cell, modifiers)
|
|
403
|
+
|
|
404
|
+
rcell.match(value)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def numeric_compare (value, cell)
|
|
408
|
+
|
|
409
|
+
comparator = cell[0, 1]
|
|
410
|
+
comparator += "=" if cell[1, 1] == "="
|
|
411
|
+
cell = cell[comparator.length..-1]
|
|
412
|
+
|
|
413
|
+
nvalue = narrow(value)
|
|
414
|
+
ncell = narrow(cell)
|
|
415
|
+
|
|
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
|
|
423
|
+
|
|
424
|
+
s = "#{value} #{comparator} #{cell}"
|
|
425
|
+
|
|
426
|
+
#puts "...>>>#{s}<<<"
|
|
427
|
+
|
|
428
|
+
begin
|
|
429
|
+
return Rufus::eval_safely(s, 4)
|
|
430
|
+
rescue Exception => e
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
false
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def narrow (s)
|
|
437
|
+
begin
|
|
438
|
+
return Float(s)
|
|
439
|
+
rescue Exception => e
|
|
440
|
+
end
|
|
441
|
+
s
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
def resolve_in_header (in_header)
|
|
445
|
+
|
|
446
|
+
"${#{in_header}}"
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def apply (row, hash)
|
|
450
|
+
|
|
451
|
+
@header.outs.each_with_index do |out_header, icol|
|
|
452
|
+
|
|
453
|
+
next unless out_header
|
|
454
|
+
|
|
455
|
+
value = row[icol]
|
|
456
|
+
|
|
457
|
+
next unless value
|
|
458
|
+
#next unless value.strip.length > 0
|
|
459
|
+
next unless value.length > 0
|
|
460
|
+
|
|
461
|
+
value = Rufus::dsub value, hash
|
|
462
|
+
|
|
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
|
|
478
|
+
|
|
479
|
+
value
|
|
480
|
+
end
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def parse_header_row (row)
|
|
485
|
+
|
|
486
|
+
row.each_with_index do |cell, icol|
|
|
487
|
+
|
|
488
|
+
next unless cell
|
|
489
|
+
|
|
490
|
+
cell = cell.strip
|
|
491
|
+
s = cell.downcase
|
|
492
|
+
|
|
493
|
+
if s == "ignorecase" or s == "ignore_case"
|
|
494
|
+
@ignore_case = true
|
|
495
|
+
next
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
if s == "through"
|
|
499
|
+
@first_match = false
|
|
500
|
+
next
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
if s == "accumulate"
|
|
504
|
+
@first_match = false
|
|
505
|
+
@accumulate = true
|
|
506
|
+
next
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
if Rufus::starts_with?(cell, "in:") or \
|
|
510
|
+
Rufus::starts_with?(cell, "out:")
|
|
511
|
+
|
|
512
|
+
@header = Header.new unless @header
|
|
513
|
+
@header.add cell, icol
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def empty_row? (row)
|
|
519
|
+
|
|
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
|
|
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
|
|
585
|
+
|
|
586
|
+
class Header
|
|
587
|
+
|
|
588
|
+
attr_accessor :ins, :outs
|
|
589
|
+
|
|
590
|
+
def initialize
|
|
591
|
+
|
|
592
|
+
@ins = []
|
|
593
|
+
@outs = []
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def add (cell, icol)
|
|
597
|
+
|
|
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
|
+
|
|
608
|
+
def to_csv
|
|
609
|
+
|
|
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
|
+
|
|
622
|
+
end
|
|
623
|
+
|