rufus-decision 1.0 → 1.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 +15 -1
- data/README.txt +82 -49
- data/bin/rufus_decide +149 -0
- data/lib/rufus/decision.rb +295 -283
- data/lib/rufus/hashes.rb +56 -60
- data/test/decision_0_test.rb +120 -162
- data/test/decision_1_test.rb +60 -32
- data/test/eval_test.rb +6 -14
- data/test/goal.csv +3 -0
- data/test/input.csv +3 -0
- data/test/table.csv +9 -0
- data/test/test.rb +7 -3
- data/test/test_base.rb +51 -0
- data/test/transpose_test.rb +64 -0
- metadata +14 -8
- data/test/dmixin.rb +0 -55
data/CHANGELOG.txt
CHANGED
@@ -1,7 +1,21 @@
|
|
1
1
|
|
2
2
|
= rufus-decision CHANGELOG.txt
|
3
3
|
|
4
|
-
|
4
|
+
|
5
|
+
== rufus-decision - 1.1 released 2009/04/25
|
6
|
+
|
7
|
+
- todo #25670 : :ruby_eval settable at table initialization
|
8
|
+
- todo #25667 : :ignore_case, :through and :accumulate settable at table
|
9
|
+
initialization (instead of only in the csv table itself)
|
10
|
+
- todo #25647 : now accepts horizontal and vertical decision tables
|
11
|
+
- todo #25642 : introducing bin/rufus_decided -t table.csv -i input.csv
|
12
|
+
- todo #25629 : implemented Rufus::Decision.transpose(a)
|
13
|
+
- todo #25630 : made Ruby 1.9.1 compatible
|
14
|
+
- todo #25595 : Rufus::DecisionTable -> Rufus::Decision::Table
|
15
|
+
- bug #25589 : fixed issue with empty values and in:ranges
|
16
|
+
|
17
|
+
|
18
|
+
== rufus-decision - 1.0 released 2008/09/01
|
5
19
|
|
6
20
|
- todo #20670 : dropped rufus-eval in favour of rufus-treechecker
|
7
21
|
|
data/README.txt
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
== getting it
|
6
6
|
|
7
|
-
|
7
|
+
sudo gem install -y rufus-decision
|
8
8
|
|
9
9
|
or at
|
10
10
|
|
@@ -13,15 +13,15 @@ http://rubyforge.org/frs/?group_id=4812
|
|
13
13
|
|
14
14
|
== usage
|
15
15
|
|
16
|
+
more info at http://rufus.rubyforge.org/rufus-decision/classes/Rufus/Decision/Table.html, but here is a recap.
|
17
|
+
|
16
18
|
An example where a few rules determine which salesperson should interact with a customer with given characteristics.
|
17
19
|
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
TABLE = DecisionTable.new("""
|
21
|
+
require 'rubygems'
|
22
|
+
require 'rufus/decision'
|
23
|
+
|
24
|
+
TABLE = Rufus::Decision::Table.new(%{
|
25
25
|
in:age,in:trait,out:salesperson
|
26
26
|
|
27
27
|
18..35,,adeslky
|
@@ -33,52 +33,85 @@ An example where a few rules determine which salesperson should interact with a
|
|
33
33
|
25..35,rich,kerfelden
|
34
34
|
,cheerful,swanson
|
35
35
|
,maniac,korolev
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
=
|
73
|
-
|
74
|
-
|
36
|
+
})
|
37
|
+
|
38
|
+
# Given a customer (a Hash instance directly, for
|
39
|
+
# convenience), returns the name of the first
|
40
|
+
# corresponding salesman.
|
41
|
+
#
|
42
|
+
def determine_salesperson (customer)
|
43
|
+
|
44
|
+
TABLE.transform(customer)["salesperson"]
|
45
|
+
end
|
46
|
+
|
47
|
+
puts determine_salesperson(
|
48
|
+
"age" => 72) # => thorsten
|
49
|
+
|
50
|
+
puts determine_salesperson(
|
51
|
+
"age" => 25, "trait" => "rich") # => adeslky
|
52
|
+
|
53
|
+
puts determine_salesperson(
|
54
|
+
"age" => 23, "trait" => "cheerful") # => adeslky
|
55
|
+
|
56
|
+
puts determine_salesperson(
|
57
|
+
"age" => 25, "trait" => "maniac") # => adeslky
|
58
|
+
|
59
|
+
puts determine_salesperson(
|
60
|
+
"age" => 44, "trait" => "maniac") # => espadas
|
61
|
+
|
62
|
+
|
63
|
+
More at Rufus::Decision::Table
|
64
|
+
|
65
|
+
Note that you can use a CSV table served over HTTP like in :
|
66
|
+
|
67
|
+
|
68
|
+
require 'rubygems'
|
69
|
+
require 'rufus/decision'
|
70
|
+
|
71
|
+
TABLE = Rufus::DecisionTable.new(
|
72
|
+
'http://spreadsheets.google.com/pub?key=pCkopoeZwCNsMWOVeDjR1TQ&output=csv')
|
73
|
+
|
74
|
+
# the CSV is :
|
75
|
+
#
|
76
|
+
# in:weather,in:month,out:take_umbrella?
|
77
|
+
#
|
78
|
+
# raining,,yes
|
79
|
+
# sunny,,no
|
80
|
+
# cloudy,june,yes
|
81
|
+
# cloudy,may,yes
|
82
|
+
# cloudy,,no
|
83
|
+
|
84
|
+
def take_umbrella? (weather, month=nil)
|
85
|
+
h = TABLE.transform('weather' => weather, 'month' => month)
|
86
|
+
h['take_umbrella?'] == 'yes'
|
87
|
+
end
|
88
|
+
|
89
|
+
puts take_umbrella?('cloudy', 'june')
|
90
|
+
# => true
|
91
|
+
|
92
|
+
puts take_umbrella?('sunny', 'june')
|
93
|
+
# => false
|
94
|
+
|
95
|
+
In this example, the CSV table is the direction CSV representation of the Google spreadsheet at : http://spreadsheets.google.com/pub?key=pCkopoeZwCNsMWOVeDjR1TQ
|
96
|
+
|
97
|
+
WARNING though : use at your own risk. CSV loaded from untrusted locations may contain harmful code. The rufus-decision gem has an abstract tree checker integrated, it will check all the CSVs that contain calls in Ruby and raise a security error when possibly harmful code is spotted. Bullet vs Armor. Be warned.
|
98
|
+
|
99
|
+
|
100
|
+
== uninstalling it
|
101
|
+
|
102
|
+
sudo gem uninstall -y rufus-decision
|
103
|
+
|
104
|
+
|
105
|
+
== dependencies
|
106
|
+
|
107
|
+
The gem 'rufus-dollar' (http://rufus.rubyforge.org/rufus-dollar) and the 'rufus-treechecker' gem (http://rufus.rubyforge.org/rufus-treechecker).
|
75
108
|
|
76
109
|
|
77
110
|
== mailing list
|
78
111
|
|
79
|
-
On the rufus-ruby list
|
112
|
+
On the rufus-ruby list :
|
80
113
|
|
81
|
-
|
114
|
+
http://groups.google.com/group/rufus-ruby
|
82
115
|
|
83
116
|
|
84
117
|
== irc
|
@@ -95,7 +128,7 @@ http://rubyforge.org/tracker/?atid=18584&group_id=4812&func=browse
|
|
95
128
|
|
96
129
|
http://github.com/jmettraux/rufus-decision
|
97
130
|
|
98
|
-
|
131
|
+
git clone git://github.com/jmettraux/rufus-decision.git
|
99
132
|
|
100
133
|
|
101
134
|
== author
|
data/bin/rufus_decide
ADDED
@@ -0,0 +1,149 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib') \
|
4
|
+
if File.exist?(File.dirname(__FILE__) + '/../lib/rufus')
|
5
|
+
# in dev mode, use the local rufus/decision
|
6
|
+
|
7
|
+
require 'rubygems'
|
8
|
+
require 'rufus/decision'
|
9
|
+
|
10
|
+
rest = []
|
11
|
+
opts = {}
|
12
|
+
while arg = ARGV.shift do
|
13
|
+
if arg.match(/^-/)
|
14
|
+
opts[arg] = (ARGV.first && ! ARGV.first.match(/^-/)) ? ARGV.shift : true
|
15
|
+
else
|
16
|
+
rest << arg
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
USAGE = %{
|
21
|
+
|
22
|
+
= #{File.basename(__FILE__)} -i input.csv -t table.csv
|
23
|
+
|
24
|
+
runs decision table 'table.csv' on input 'input.csv', outputs as CSV.
|
25
|
+
|
26
|
+
== for example
|
27
|
+
|
28
|
+
#{File.basename(__FILE__)} -i input.csv -t table.csv
|
29
|
+
|
30
|
+
== options
|
31
|
+
|
32
|
+
-v, --version : print the version of itog.rb and exits
|
33
|
+
-h, --help : print this help text and exits
|
34
|
+
|
35
|
+
-i, --input : points to input file (mandatory)
|
36
|
+
-t, --table : points to the decision table file (mandatory)
|
37
|
+
|
38
|
+
-r, --ruby : output as a Ruby hash representation instead of CSV
|
39
|
+
-j, --json : output as a JSON hash representation instead of CSV
|
40
|
+
|
41
|
+
-T, --through : don't stop at first match, run each row
|
42
|
+
-I, --ignore-case : ignore case when comparing values for row matching
|
43
|
+
-A, --accumulate : use with -t, each time a new match is made for an 'out',
|
44
|
+
values are not overriden but gathered in an array
|
45
|
+
-R, --ruby-eval : allow evaluation of embedded ruby code (potentially
|
46
|
+
harmful)
|
47
|
+
|
48
|
+
-g, --goal : points to an ideal target CSV file
|
49
|
+
(decision table testing)
|
50
|
+
|
51
|
+
}
|
52
|
+
|
53
|
+
if (opts['-h'] or opts['--help'])
|
54
|
+
puts USAGE
|
55
|
+
exit(0)
|
56
|
+
end
|
57
|
+
|
58
|
+
if (opts['-v'] or opts['--version'])
|
59
|
+
puts "rufus-decision #{Rufus::Decision::VERSION}"
|
60
|
+
exit(0)
|
61
|
+
end
|
62
|
+
|
63
|
+
ipath = opts['-i'] || opts['--input']
|
64
|
+
tpath = opts['-t'] || opts['--table']
|
65
|
+
gpath = opts['-g'] || opts['--goal']
|
66
|
+
|
67
|
+
if ipath == nil or tpath == nil
|
68
|
+
|
69
|
+
puts
|
70
|
+
puts " ** missing --input and/or --table parameter"
|
71
|
+
puts USAGE
|
72
|
+
exit(1)
|
73
|
+
end
|
74
|
+
|
75
|
+
#
|
76
|
+
# load CSV files
|
77
|
+
|
78
|
+
input = Rufus::Decision.csv_to_a(ipath)
|
79
|
+
input = Rufus::Decision.transpose(input)
|
80
|
+
|
81
|
+
params = {}
|
82
|
+
params[:ignore_case] = opts['-I'] || opts['--ignore-case']
|
83
|
+
params[:ruby_eval] = opts['-R'] || opts['--ruby-eval']
|
84
|
+
params[:through] = opts['-T'] || opts['--through']
|
85
|
+
params[:accumulate] = opts['-A'] || opts['--accumulate']
|
86
|
+
|
87
|
+
table = Rufus::Decision::Table.new(tpath, params)
|
88
|
+
|
89
|
+
goal = gpath ? Rufus::Decision.csv_to_a(gpath) : nil
|
90
|
+
|
91
|
+
#
|
92
|
+
# run the decision table for each input row
|
93
|
+
|
94
|
+
output = input.inject([]) { |a, hash| a << table.transform(hash); a }
|
95
|
+
|
96
|
+
if goal
|
97
|
+
#
|
98
|
+
# check if output matches 'goal'
|
99
|
+
|
100
|
+
puts
|
101
|
+
|
102
|
+
goal = Rufus::Decision.transpose(goal)
|
103
|
+
|
104
|
+
failures = []
|
105
|
+
|
106
|
+
goal.each_with_index do |hash, y|
|
107
|
+
if hash == output[y]
|
108
|
+
print '.'
|
109
|
+
else
|
110
|
+
print 'f'
|
111
|
+
failures << [ y, output[y], hash ]
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
puts
|
116
|
+
|
117
|
+
failures.each do |f|
|
118
|
+
row, output, expected = f
|
119
|
+
puts
|
120
|
+
puts " at row #{row}, expected"
|
121
|
+
puts " #{expected.inspect}"
|
122
|
+
puts " but got"
|
123
|
+
puts " #{output.inspect}"
|
124
|
+
end
|
125
|
+
|
126
|
+
puts "\n#{goal.size} rows, #{failures.size} failures"
|
127
|
+
|
128
|
+
else
|
129
|
+
#
|
130
|
+
# print output
|
131
|
+
|
132
|
+
if opts['-j'] or opts['--json']
|
133
|
+
|
134
|
+
require 'json' # sudo gem install json
|
135
|
+
puts output.to_json
|
136
|
+
|
137
|
+
elsif opts['-r'] or opts['--ruby']
|
138
|
+
|
139
|
+
p output
|
140
|
+
|
141
|
+
else # CSV
|
142
|
+
|
143
|
+
output = Rufus::Decision.transpose(output)
|
144
|
+
output.each do |row|
|
145
|
+
puts row.join(',')
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
data/lib/rufus/decision.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
|
-
#
|
2
1
|
#--
|
3
|
-
# Copyright (c) 2007-
|
2
|
+
# Copyright (c) 2007-2009, John Mettraux, jmettraux@gmail.com
|
4
3
|
#
|
5
4
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
5
|
# of this software and associated documentation files (the "Software"), to deal
|
@@ -19,14 +18,10 @@
|
|
19
18
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
19
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
20
|
# THE SOFTWARE.
|
22
|
-
#++
|
23
21
|
#
|
22
|
+
# Made in Japan.
|
23
|
+
#++
|
24
24
|
|
25
|
-
#
|
26
|
-
# "made in Japan"
|
27
|
-
#
|
28
|
-
# John Mettraux at openwfe.org
|
29
|
-
#
|
30
25
|
|
31
26
|
require 'csv'
|
32
27
|
require 'open-uri'
|
@@ -38,17 +33,9 @@ require 'rufus/hashes'
|
|
38
33
|
|
39
34
|
|
40
35
|
module Rufus
|
36
|
+
module Decision
|
41
37
|
|
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
|
-
|
38
|
+
VERSION = '1.1.0'
|
52
39
|
|
53
40
|
#
|
54
41
|
# A decision table is a description of a set of rules as a CSV (comma
|
@@ -92,7 +79,9 @@ module Rufus
|
|
92
79
|
#
|
93
80
|
# Enough words, some code :
|
94
81
|
#
|
95
|
-
#
|
82
|
+
# require 'rufus/decision'
|
83
|
+
#
|
84
|
+
# table = Rufus::Decision::Table.new(%{
|
96
85
|
# in:topic,in:region,out:team_member
|
97
86
|
# sports,europe,Alice
|
98
87
|
# sports,,Bob
|
@@ -103,12 +92,12 @@ module Rufus
|
|
103
92
|
# politics,america,Gilbert
|
104
93
|
# politics,,Henry
|
105
94
|
# ,,Zach
|
106
|
-
#
|
95
|
+
# })
|
107
96
|
#
|
108
97
|
# h = {}
|
109
98
|
# h["topic"] = "politics"
|
110
99
|
#
|
111
|
-
# table.transform!
|
100
|
+
# table.transform!(h)
|
112
101
|
#
|
113
102
|
# puts h["team_member"]
|
114
103
|
# # will yield "Henry" who takes care of all the politics stuff,
|
@@ -116,20 +105,19 @@ module Rufus
|
|
116
105
|
#
|
117
106
|
# '>', '>=', '<' and '<=' can be put in front of individual cell values :
|
118
107
|
#
|
119
|
-
# table =
|
108
|
+
# table = Rufus::Decision::Table.new(%{
|
120
109
|
# ,
|
121
110
|
# in:fx, out:fy
|
122
111
|
# ,
|
123
112
|
# >100,a
|
124
113
|
# >=10,b
|
125
114
|
# ,c
|
126
|
-
#
|
115
|
+
# })
|
127
116
|
#
|
128
117
|
# h = { 'fx' => '10' }
|
129
|
-
# table.transform
|
118
|
+
# h = table.transform(h)
|
130
119
|
#
|
131
|
-
#
|
132
|
-
# # will yield { 'fx' => '10', 'fy' => 'b' }
|
120
|
+
# p h # => { 'fx' => '10', 'fy' => 'b' }
|
133
121
|
#
|
134
122
|
# Such comparisons are done after the elements are transformed to float
|
135
123
|
# numbers. By default, non-numeric arguments will get compared as Strings.
|
@@ -142,9 +130,9 @@ module Rufus
|
|
142
130
|
# hash.
|
143
131
|
#
|
144
132
|
#
|
145
|
-
# ==
|
133
|
+
# == [ruby] ranges
|
146
134
|
#
|
147
|
-
# Ruby ranges are also accepted in cells.
|
135
|
+
# Ruby-like ranges are also accepted in cells.
|
148
136
|
#
|
149
137
|
# in:f0,out:result
|
150
138
|
# ,
|
@@ -160,9 +148,9 @@ module Rufus
|
|
160
148
|
# You can put options on their own in a cell BEFORE the line containing
|
161
149
|
# "in:xxx" and "out:yyy" (ins and outs).
|
162
150
|
#
|
163
|
-
#
|
151
|
+
# Three options are supported, "ignorecase", "through" and "accumulate".
|
164
152
|
#
|
165
|
-
# * "ignorecase", if found by the
|
153
|
+
# * "ignorecase", if found by the decision table will make any match (in the
|
166
154
|
# "in" columns) case unsensitive.
|
167
155
|
#
|
168
156
|
# * "through", will make sure that EVERY row is evaluated and potentially
|
@@ -172,6 +160,8 @@ module Rufus
|
|
172
160
|
# * "accumulate", behaves as with "through" set but instead of overriding
|
173
161
|
# values each time a match is found, will gather them in an array.
|
174
162
|
#
|
163
|
+
# an example of 'accumulate'
|
164
|
+
#
|
175
165
|
# accumulate
|
176
166
|
# in:f0,out:result
|
177
167
|
# ,
|
@@ -181,11 +171,18 @@ module Rufus
|
|
181
171
|
#
|
182
172
|
# will yield { result => [ 'normal', 'large' ]} for f0 => 56
|
183
173
|
#
|
174
|
+
# === Setting options at table initialization
|
175
|
+
#
|
176
|
+
# It's OK to set the options at initialization time :
|
177
|
+
#
|
178
|
+
# table = Rufus::Decision::Table.new(
|
179
|
+
# csv, :ruby_eval => true, :accumulate => true)
|
180
|
+
#
|
184
181
|
#
|
185
182
|
# == Cross references
|
186
183
|
#
|
187
184
|
# By using the 'dollar notation', it's possible to reference a value
|
188
|
-
# already in the hash.
|
185
|
+
# already in the hash (that is, the hash undergoing 'transformation').
|
189
186
|
#
|
190
187
|
# in:value,in:roundup,out:newvalue
|
191
188
|
# 0..32,true,32
|
@@ -208,7 +205,7 @@ module Rufus
|
|
208
205
|
# Note though that this feature is only enabled via the :ruby_eval
|
209
206
|
# option of the transform!() method.
|
210
207
|
#
|
211
|
-
# decisionTable.transform!
|
208
|
+
# decisionTable.transform!(h, :ruby_eval => true)
|
212
209
|
#
|
213
210
|
# That decision table may look like :
|
214
211
|
#
|
@@ -220,404 +217,419 @@ module Rufus
|
|
220
217
|
# (It's a very simplistic example, but I hope it demonstrates the
|
221
218
|
# capabilities of this technique)
|
222
219
|
#
|
220
|
+
# It's OK to set the :ruby_eval parameter when initializing the decision
|
221
|
+
# table :
|
222
|
+
#
|
223
|
+
# table = Rufus::Decision::Table.new(csv, :ruby_eval => true)
|
224
|
+
#
|
225
|
+
# so that there is no need to specify it at transform() call time.
|
226
|
+
#
|
223
227
|
#
|
224
228
|
# == See also
|
225
229
|
#
|
226
230
|
# * http://jmettraux.wordpress.com/2007/02/11/ruby-decision-tables/
|
227
231
|
#
|
228
|
-
class
|
232
|
+
class Table
|
233
|
+
|
234
|
+
IN = /^in:/
|
235
|
+
OUT = /^out:/
|
236
|
+
IN_OR_OUT = /^(in|out):/
|
237
|
+
NUMERIC_COMPARISON = /^([><]=?)(.*)$/
|
229
238
|
|
230
|
-
#
|
231
239
|
# when set to true, the transformation process stops after the
|
232
240
|
# first match got applied.
|
233
241
|
#
|
234
242
|
attr_accessor :first_match
|
235
243
|
|
236
|
-
#
|
237
244
|
# when set to true, matches evaluation ignores case.
|
238
245
|
#
|
239
246
|
attr_accessor :ignore_case
|
240
247
|
|
241
|
-
#
|
242
248
|
# when set to true, multiple matches result get accumulated in
|
243
249
|
# an array.
|
244
250
|
#
|
245
251
|
attr_accessor :accumulate
|
246
252
|
|
247
|
-
#
|
248
253
|
# The constructor for DecisionTable, you can pass a String, an Array
|
249
254
|
# (of arrays), a File object. The CSV parser coming with Ruby will take
|
250
255
|
# care of it and a DecisionTable instance will be built.
|
251
256
|
#
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
@header = nil
|
259
|
-
@rows = []
|
260
|
-
|
261
|
-
csv_array = to_csv_array(csv_data)
|
262
|
-
|
263
|
-
csv_array.each do |row|
|
257
|
+
# parameters (options) are :through, :ignore_case, :accumulate (which
|
258
|
+
# forces :through to true when set) and :ruby_eval. See
|
259
|
+
# Rufus::Decision::Table for more details.
|
260
|
+
#
|
261
|
+
def initialize (csv, params={})
|
264
262
|
|
265
|
-
|
263
|
+
@first_match = (params[:through] != true)
|
264
|
+
@ignore_case = params[:ignore_case] || params[:ignorecase]
|
265
|
+
@accumulate = params[:accumulate]
|
266
|
+
@ruby_eval = params[:ruby_eval]
|
266
267
|
|
267
|
-
|
268
|
+
@first_match = false if @accumulate
|
268
269
|
|
269
|
-
|
270
|
-
else
|
270
|
+
@rows = Rufus::Decision.csv_to_a(csv)
|
271
271
|
|
272
|
-
|
273
|
-
|
274
|
-
end
|
272
|
+
extract_options
|
273
|
+
parse_header_row
|
275
274
|
end
|
276
275
|
|
277
|
-
#
|
278
276
|
# Like transform, but the original hash doesn't get touched,
|
279
277
|
# a copy of it gets transformed and finally returned.
|
280
278
|
#
|
281
279
|
def transform (hash, options={})
|
282
280
|
|
283
|
-
transform!
|
281
|
+
transform!(hash.dup)
|
284
282
|
end
|
285
283
|
|
286
|
-
#
|
287
284
|
# Passes the hash through the decision table and returns it,
|
288
285
|
# transformed.
|
289
286
|
#
|
290
287
|
def transform! (hash, options={})
|
291
288
|
|
292
|
-
hash = Rufus::EvalHashFilter.new(hash) \
|
293
|
-
if options[:ruby_eval] == true
|
289
|
+
hash = Rufus::Decision::EvalHashFilter.new(hash) \
|
290
|
+
if @ruby_eval || options[:ruby_eval] == true
|
294
291
|
|
295
292
|
@rows.each do |row|
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
apply row, hash
|
300
|
-
break if @first_match
|
301
|
-
end
|
293
|
+
next unless matches?(row, hash)
|
294
|
+
apply(row, hash)
|
295
|
+
break if @first_match
|
302
296
|
end
|
303
297
|
|
304
|
-
hash
|
298
|
+
hash.is_a?(Rufus::Decision::HashFilter) ? hash.parent_hash : hash
|
305
299
|
end
|
306
300
|
|
307
|
-
|
301
|
+
alias :run :transform
|
302
|
+
|
308
303
|
# Outputs back this table as a CSV String
|
309
304
|
#
|
310
305
|
def to_csv
|
311
306
|
|
312
|
-
|
313
|
-
|
314
|
-
s << "\n"
|
315
|
-
@rows.each do |row|
|
316
|
-
s << row.join(",")
|
317
|
-
s << "\n"
|
318
|
-
end
|
319
|
-
s
|
307
|
+
a = [ @header.to_csv ]
|
308
|
+
@rows.inject(a) { |a, row| a << row.join(',') }.join("\n")
|
320
309
|
end
|
321
310
|
|
322
311
|
private
|
323
312
|
|
324
|
-
|
313
|
+
# Returns true if the hash matches the in: values for this row
|
314
|
+
#
|
315
|
+
def matches? (row, hash)
|
325
316
|
|
326
|
-
|
317
|
+
@header.ins.each do |x, in_header|
|
327
318
|
|
328
|
-
|
329
|
-
return URI::parse(string)
|
330
|
-
rescue Exception => e
|
331
|
-
end
|
319
|
+
in_header = "${#{in_header}}"
|
332
320
|
|
333
|
-
|
334
|
-
end
|
321
|
+
value = Rufus::dsub(in_header, hash)
|
335
322
|
|
336
|
-
|
323
|
+
cell = row[x]
|
337
324
|
|
338
|
-
|
325
|
+
next if cell == nil || cell == ''
|
339
326
|
|
340
|
-
|
327
|
+
cell = Rufus::dsub(cell, hash)
|
341
328
|
|
342
|
-
|
329
|
+
b = if m = NUMERIC_COMPARISON.match(cell)
|
330
|
+
|
331
|
+
numeric_compare(m, value, cell)
|
332
|
+
else
|
333
|
+
|
334
|
+
range = to_ruby_range(cell)
|
335
|
+
range ? range.include?(value) : regex_compare(value, cell)
|
336
|
+
end
|
343
337
|
|
344
|
-
|
338
|
+
return false unless b
|
345
339
|
end
|
346
340
|
|
347
|
-
|
341
|
+
true
|
342
|
+
end
|
343
|
+
|
344
|
+
def regex_compare (value, cell)
|
348
345
|
|
349
|
-
|
346
|
+
modifiers = 0
|
347
|
+
modifiers += Regexp::IGNORECASE if @ignore_case
|
350
348
|
|
351
|
-
|
352
|
-
|
353
|
-
|
349
|
+
rcell = Regexp.new(cell, modifiers)
|
350
|
+
|
351
|
+
rcell.match(value)
|
352
|
+
end
|
354
353
|
|
355
|
-
|
354
|
+
def numeric_compare (match, value, cell)
|
356
355
|
|
357
|
-
|
356
|
+
comparator = match[1]
|
357
|
+
cell = match[2]
|
358
358
|
|
359
|
-
|
359
|
+
nvalue = Float(value) rescue value
|
360
|
+
ncell = Float(cell) rescue cell
|
360
361
|
|
361
|
-
|
362
|
+
value, cell = if nvalue.is_a?(String) or ncell.is_a?(String)
|
363
|
+
[ "\"#{value}\"", "\"#{cell}\"" ]
|
364
|
+
else
|
365
|
+
[ nvalue, ncell ]
|
366
|
+
end
|
362
367
|
|
363
|
-
|
368
|
+
s = "#{value} #{comparator} #{cell}"
|
364
369
|
|
365
|
-
|
370
|
+
Rufus::Decision::check_and_eval(s) rescue false
|
371
|
+
end
|
366
372
|
|
367
|
-
|
373
|
+
def apply (row, hash)
|
368
374
|
|
369
|
-
|
375
|
+
@header.outs.each do |x, out_header|
|
370
376
|
|
371
|
-
|
377
|
+
value = row[x]
|
372
378
|
|
373
|
-
|
379
|
+
next if value == nil || value == ''
|
374
380
|
|
375
|
-
|
381
|
+
value = Rufus::dsub(value, hash)
|
376
382
|
|
377
|
-
|
378
|
-
|
383
|
+
hash[out_header] = if @accumulate
|
384
|
+
#
|
385
|
+
# accumulate
|
379
386
|
|
380
|
-
|
387
|
+
v = hash[out_header]
|
381
388
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
389
|
+
if v and v.is_a?(Array)
|
390
|
+
v + Array(value)
|
391
|
+
elsif v
|
392
|
+
[ v, value ]
|
393
|
+
else
|
394
|
+
value
|
387
395
|
end
|
396
|
+
else
|
397
|
+
#
|
398
|
+
# override
|
388
399
|
|
389
|
-
|
400
|
+
value
|
390
401
|
end
|
402
|
+
end
|
403
|
+
end
|
391
404
|
|
392
|
-
|
405
|
+
def extract_options
|
393
406
|
|
394
|
-
|
395
|
-
end
|
407
|
+
row = @rows.first
|
396
408
|
|
397
|
-
|
409
|
+
return unless row
|
410
|
+
# end of table somehow
|
398
411
|
|
399
|
-
|
400
|
-
|
412
|
+
return if row.find { |cell| cell && cell.match(IN_OR_OUT) }
|
413
|
+
# just hit the header row
|
401
414
|
|
402
|
-
|
415
|
+
row.each do |cell|
|
403
416
|
|
404
|
-
|
405
|
-
end
|
417
|
+
cell = cell.downcase
|
406
418
|
|
407
|
-
|
419
|
+
if cell == 'ignorecase' or cell == 'ignore_case'
|
420
|
+
@ignore_case = true
|
421
|
+
elsif cell == 'through'
|
422
|
+
@first_match = false
|
423
|
+
elsif cell == 'accumulate'
|
424
|
+
@first_match = false
|
425
|
+
@accumulate = true
|
426
|
+
end
|
427
|
+
end
|
408
428
|
|
409
|
-
|
410
|
-
comparator += "=" if cell[1, 1] == "="
|
411
|
-
cell = cell[comparator.length..-1]
|
429
|
+
@rows.shift
|
412
430
|
|
413
|
-
|
414
|
-
|
431
|
+
extract_options
|
432
|
+
end
|
415
433
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
434
|
+
# Returns true if the first row of the table contains just an "in:" or
|
435
|
+
# an "out:"
|
436
|
+
#
|
437
|
+
def is_vertical_table? (first_row)
|
438
|
+
bin = false
|
439
|
+
bout = false
|
440
|
+
first_row.each do |cell|
|
441
|
+
bin ||= cell.match(IN)
|
442
|
+
bout ||= cell.match(OUT)
|
443
|
+
return false if bin and bout
|
444
|
+
end
|
445
|
+
true
|
446
|
+
end
|
423
447
|
|
424
|
-
|
448
|
+
def parse_header_row
|
425
449
|
|
426
|
-
|
450
|
+
row = @rows.first
|
427
451
|
|
428
|
-
|
429
|
-
return Rufus::check_and_eval(s)
|
430
|
-
rescue Exception => e
|
431
|
-
end
|
452
|
+
return unless row
|
432
453
|
|
433
|
-
|
454
|
+
if is_vertical_table?(row)
|
455
|
+
@rows = @rows.transpose
|
456
|
+
row = @rows.first
|
434
457
|
end
|
435
458
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
s
|
459
|
+
@rows.shift
|
460
|
+
|
461
|
+
row.each_with_index do |cell, x|
|
462
|
+
next unless cell.match(IN_OR_OUT)
|
463
|
+
(@header ||= Header.new).add(cell, x)
|
442
464
|
end
|
465
|
+
end
|
443
466
|
|
444
|
-
|
467
|
+
# A regexp for checking if a string is a numeric Ruby range
|
468
|
+
#
|
469
|
+
RUBY_NUMERIC_RANGE_REGEXP = Regexp.compile(
|
470
|
+
"^\\d+(\\.\\d+)?\\.{2,3}\\d+(\\.\\d+)?$")
|
445
471
|
|
446
|
-
|
447
|
-
|
472
|
+
# A regexp for checking if a string is an alpha Ruby range
|
473
|
+
#
|
474
|
+
RUBY_ALPHA_RANGE_REGEXP = Regexp.compile(
|
475
|
+
"^([A-Za-z])(\\.{2,3})([A-Za-z])$")
|
448
476
|
|
449
|
-
|
477
|
+
# If the string contains a Ruby range definition
|
478
|
+
# (ie something like "93.0..94.5" or "56..72"), it will return
|
479
|
+
# the Range instance.
|
480
|
+
# Will return nil else.
|
481
|
+
#
|
482
|
+
# The Ruby range returned (if any) will accept String or Numeric,
|
483
|
+
# ie (4..6).include?("5") will yield true.
|
484
|
+
#
|
485
|
+
def to_ruby_range (s)
|
450
486
|
|
451
|
-
|
487
|
+
range = if RUBY_NUMERIC_RANGE_REGEXP.match(s)
|
452
488
|
|
453
|
-
|
489
|
+
eval(s)
|
454
490
|
|
455
|
-
|
491
|
+
else
|
456
492
|
|
457
|
-
|
458
|
-
#next unless value.strip.length > 0
|
459
|
-
next unless value.length > 0
|
493
|
+
m = RUBY_ALPHA_RANGE_REGEXP.match(s)
|
460
494
|
|
461
|
-
|
495
|
+
m ? eval("'#{m[1]}'#{m[2]}'#{m[3]}'") : nil
|
496
|
+
end
|
462
497
|
|
463
|
-
|
464
|
-
#
|
465
|
-
# accumulate
|
498
|
+
class << range
|
466
499
|
|
467
|
-
|
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
|
500
|
+
alias :old_include? :include?
|
478
501
|
|
479
|
-
|
480
|
-
|
502
|
+
def include? (elt)
|
503
|
+
|
504
|
+
elt = first.is_a?(Numeric) ? (Float(elt) rescue '') : elt
|
505
|
+
old_include?(elt)
|
481
506
|
end
|
482
|
-
end
|
483
507
|
|
484
|
-
|
508
|
+
end if range
|
485
509
|
|
486
|
-
|
510
|
+
range
|
511
|
+
end
|
487
512
|
|
488
|
-
|
513
|
+
class Header
|
489
514
|
|
490
|
-
|
491
|
-
s = cell.downcase
|
515
|
+
attr_accessor :ins, :outs
|
492
516
|
|
493
|
-
|
494
|
-
@ignore_case = true
|
495
|
-
next
|
496
|
-
end
|
517
|
+
def initialize
|
497
518
|
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
end
|
519
|
+
@ins = {}
|
520
|
+
@outs = {}
|
521
|
+
end
|
502
522
|
|
503
|
-
|
504
|
-
@first_match = false
|
505
|
-
@accumulate = true
|
506
|
-
next
|
507
|
-
end
|
523
|
+
def add (cell, x)
|
508
524
|
|
509
|
-
|
510
|
-
|
525
|
+
if cell.match(IN)
|
526
|
+
|
527
|
+
@ins[x] = cell[3..-1]
|
528
|
+
|
529
|
+
elsif cell.match(OUT)
|
530
|
+
|
531
|
+
@outs[x] = cell[4..-1]
|
511
532
|
|
512
|
-
@header = Header.new unless @header
|
513
|
-
@header.add cell, icol
|
514
|
-
end
|
515
533
|
end
|
534
|
+
# else don't add
|
516
535
|
end
|
517
536
|
|
518
|
-
def
|
537
|
+
def to_csv
|
519
538
|
|
520
|
-
|
521
|
-
|
522
|
-
row.each do |cell|
|
523
|
-
return false if cell
|
524
|
-
end
|
525
|
-
true
|
539
|
+
(@ins.keys.sort.collect { |k| "in:#{@ins[k]}" } +
|
540
|
+
@outs.keys.sort.collect { |k| "out:#{@outs[k]}" }).join(',')
|
526
541
|
end
|
542
|
+
end
|
543
|
+
end
|
527
544
|
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
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
|
545
|
+
# Given a CSV string or the URI / path to a CSV file, turns the CSV
|
546
|
+
# into an array of array.
|
547
|
+
#
|
548
|
+
def self.csv_to_a (csv)
|
554
549
|
|
555
|
-
|
550
|
+
return csv if csv.is_a?(Array)
|
556
551
|
|
557
|
-
|
552
|
+
csv = csv.to_s if csv.is_a?(URI)
|
553
|
+
csv = open(csv) if is_uri?(csv)
|
558
554
|
|
559
|
-
|
560
|
-
|
561
|
-
else
|
562
|
-
nil
|
563
|
-
end
|
564
|
-
end
|
555
|
+
csv_lib = defined?(CSV::Reader) ? CSV::Reader : CSV
|
556
|
+
# no CSV::Reader for Ruby 1.9.1
|
565
557
|
|
566
|
-
|
558
|
+
csv_lib.parse(csv).inject([]) { |rows, row|
|
559
|
+
row = row.collect { |cell| cell ? cell.strip : '' }
|
560
|
+
rows << row if row.find { |cell| (cell != '') }
|
561
|
+
rows
|
562
|
+
}
|
563
|
+
end
|
567
564
|
|
568
|
-
|
565
|
+
# Returns true if the string is a URI false if it's something else
|
566
|
+
# (CSV data ?)
|
567
|
+
#
|
568
|
+
def self.is_uri? (string)
|
569
569
|
|
570
|
-
|
570
|
+
return false if string.index("\n") # quick one
|
571
571
|
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
end
|
572
|
+
begin
|
573
|
+
URI::parse(string); return true
|
574
|
+
rescue
|
575
|
+
end
|
577
576
|
|
578
|
-
|
579
|
-
|
577
|
+
false
|
578
|
+
end
|
580
579
|
|
581
|
-
|
580
|
+
# Turns an array of array (rows / columns) into an array of hashes.
|
581
|
+
# The first row is considered the "row of keys".
|
582
|
+
#
|
583
|
+
# [
|
584
|
+
# [ 'age', 'name' ],
|
585
|
+
# [ 33, 'Jeff' ],
|
586
|
+
# [ 35, 'John' ]
|
587
|
+
# ]
|
588
|
+
#
|
589
|
+
# =>
|
590
|
+
#
|
591
|
+
# [
|
592
|
+
# { 'age' => 33, 'name' => 'Jeff' },
|
593
|
+
# { 'age' => 35, 'name' => 'John' }
|
594
|
+
# ]
|
595
|
+
#
|
596
|
+
# You can also pass the CSV as a string or the URI/path to the actual CSV
|
597
|
+
# file.
|
598
|
+
#
|
599
|
+
def self.transpose (a)
|
582
600
|
|
583
|
-
|
584
|
-
end
|
601
|
+
a = csv_to_a(a) if a.is_a?(String)
|
585
602
|
|
586
|
-
|
603
|
+
return a if a.empty?
|
587
604
|
|
588
|
-
|
605
|
+
first = a.first
|
589
606
|
|
590
|
-
|
607
|
+
if first.is_a?(Hash)
|
591
608
|
|
592
|
-
|
593
|
-
|
594
|
-
|
609
|
+
keys = first.keys.sort
|
610
|
+
[ keys ] + a.collect { |row|
|
611
|
+
keys.collect { |k| row[k] }
|
612
|
+
}
|
613
|
+
else
|
595
614
|
|
596
|
-
|
615
|
+
keys = first
|
616
|
+
a[1..-1].collect { |row|
|
617
|
+
(0..keys.size - 1).inject({}) { |h, i| h[keys[i]] = row[i]; h }
|
618
|
+
}
|
619
|
+
end
|
620
|
+
end
|
597
621
|
|
598
|
-
|
599
|
-
|
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
|
622
|
+
end
|
623
|
+
end
|
607
624
|
|
608
|
-
|
625
|
+
module Rufus
|
609
626
|
|
610
|
-
|
611
|
-
|
612
|
-
|
613
|
-
|
614
|
-
|
615
|
-
|
616
|
-
end
|
617
|
-
s[0..-2]
|
618
|
-
end
|
619
|
-
end
|
627
|
+
#
|
628
|
+
# An 'alias' for the class Rufus::Decision::Table
|
629
|
+
#
|
630
|
+
# (for backward compatibility)
|
631
|
+
#
|
632
|
+
class DecisionTable < Rufus::Decision::Table
|
620
633
|
end
|
621
|
-
|
622
634
|
end
|
623
635
|
|