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 CHANGED
@@ -1,7 +1,21 @@
1
1
 
2
2
  = rufus-decision CHANGELOG.txt
3
3
 
4
- == rufus-decision - 0.10 released 2008/09/01
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
- sudo gem install -y rufus-decision
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
- require 'rubygems'
20
- require 'rufus/decision'
21
-
22
- include Rufus
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
- # 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).
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[http://groups.google.com/group/rufus-ruby] :
112
+ On the rufus-ruby list :
80
113
 
81
- http://groups.google.com/group/rufus-ruby
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
- git clone git://github.com/jmettraux/rufus-decision.git
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
+
@@ -1,6 +1,5 @@
1
- #
2
1
  #--
3
- # Copyright (c) 2007-2008, John Mettraux, jmettraux@gmail.com
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
- # table = DecisionTable.new("""
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! h
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 = DecisionTable.new("""
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! h
118
+ # h = table.transform(h)
130
119
  #
131
- # require 'pp'; pp h
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
- # == Ruby ranges
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
- # Currently, two options are supported, "ignorecase" and "through".
151
+ # Three options are supported, "ignorecase", "through" and "accumulate".
164
152
  #
165
- # * "ignorecase", if found by the DecisionTable will make any match (in 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! h, :ruby_eval => true
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 DecisionTable
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
- 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|
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
- next if empty_row? row
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
- if @header
268
+ @first_match = false if @accumulate
268
269
 
269
- @rows << row.collect { |c| c.strip if c }
270
- else
270
+ @rows = Rufus::Decision.csv_to_a(csv)
271
271
 
272
- parse_header_row row
273
- end
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! hash.dup, options
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
- if matches?(row, hash)
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
- 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
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
- def parse_uri (string)
313
+ # Returns true if the hash matches the in: values for this row
314
+ #
315
+ def matches? (row, hash)
325
316
 
326
- return nil if string.split("\n").size > 1
317
+ @header.ins.each do |x, in_header|
327
318
 
328
- begin
329
- return URI::parse(string)
330
- rescue Exception => e
331
- end
319
+ in_header = "${#{in_header}}"
332
320
 
333
- nil
334
- end
321
+ value = Rufus::dsub(in_header, hash)
335
322
 
336
- def to_csv_array (csv_data)
323
+ cell = row[x]
337
324
 
338
- return csv_data if csv_data.kind_of?(Array)
325
+ next if cell == nil || cell == ''
339
326
 
340
- csv_data = csv_data.to_s if csv_data.is_a?(URI)
327
+ cell = Rufus::dsub(cell, hash)
341
328
 
342
- csv_data = open(csv_data) if parse_uri(csv_data)
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
- CSV::Reader.parse csv_data
338
+ return false unless b
345
339
  end
346
340
 
347
- def matches? (row, hash)
341
+ true
342
+ end
343
+
344
+ def regex_compare (value, cell)
348
345
 
349
- return false if empty_row?(row)
346
+ modifiers = 0
347
+ modifiers += Regexp::IGNORECASE if @ignore_case
350
348
 
351
- #puts
352
- #puts "__row match ?"
353
- #p row
349
+ rcell = Regexp.new(cell, modifiers)
350
+
351
+ rcell.match(value)
352
+ end
354
353
 
355
- @header.ins.each_with_index do |in_header, icol|
354
+ def numeric_compare (match, value, cell)
356
355
 
357
- in_header = resolve_in_header(in_header)
356
+ comparator = match[1]
357
+ cell = match[2]
358
358
 
359
- value = Rufus::dsub in_header, hash
359
+ nvalue = Float(value) rescue value
360
+ ncell = Float(cell) rescue cell
360
361
 
361
- cell = row[icol]
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
- next if not cell
368
+ s = "#{value} #{comparator} #{cell}"
364
369
 
365
- cell = cell.strip
370
+ Rufus::Decision::check_and_eval(s) rescue false
371
+ end
366
372
 
367
- next if cell.length < 1
373
+ def apply (row, hash)
368
374
 
369
- cell = Rufus::dsub cell, hash
375
+ @header.outs.each do |x, out_header|
370
376
 
371
- #puts "__does '#{value}' match '#{cell}' ?"
377
+ value = row[x]
372
378
 
373
- c = cell[0, 1]
379
+ next if value == nil || value == ''
374
380
 
375
- b = if c == '<' or c == '>'
381
+ value = Rufus::dsub(value, hash)
376
382
 
377
- numeric_compare value, cell
378
- else
383
+ hash[out_header] = if @accumulate
384
+ #
385
+ # accumulate
379
386
 
380
- range = to_ruby_range cell
387
+ v = hash[out_header]
381
388
 
382
- if range
383
- range.include?(value)
384
- else
385
- regex_compare value, cell
386
- end
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
- return false unless b
400
+ value
390
401
  end
402
+ end
403
+ end
391
404
 
392
- #puts "__row matches"
405
+ def extract_options
393
406
 
394
- true
395
- end
407
+ row = @rows.first
396
408
 
397
- def regex_compare (value, cell)
409
+ return unless row
410
+ # end of table somehow
398
411
 
399
- modifiers = 0
400
- modifiers += Regexp::IGNORECASE if @ignore_case
412
+ return if row.find { |cell| cell && cell.match(IN_OR_OUT) }
413
+ # just hit the header row
401
414
 
402
- rcell = Regexp.new(cell, modifiers)
415
+ row.each do |cell|
403
416
 
404
- rcell.match(value)
405
- end
417
+ cell = cell.downcase
406
418
 
407
- def numeric_compare (value, cell)
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
- comparator = cell[0, 1]
410
- comparator += "=" if cell[1, 1] == "="
411
- cell = cell[comparator.length..-1]
429
+ @rows.shift
412
430
 
413
- nvalue = narrow(value)
414
- ncell = narrow(cell)
431
+ extract_options
432
+ end
415
433
 
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
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
- s = "#{value} #{comparator} #{cell}"
448
+ def parse_header_row
425
449
 
426
- #puts "...>>>#{s}<<<"
450
+ row = @rows.first
427
451
 
428
- begin
429
- return Rufus::check_and_eval(s)
430
- rescue Exception => e
431
- end
452
+ return unless row
432
453
 
433
- false
454
+ if is_vertical_table?(row)
455
+ @rows = @rows.transpose
456
+ row = @rows.first
434
457
  end
435
458
 
436
- def narrow (s)
437
- begin
438
- return Float(s)
439
- rescue Exception => e
440
- end
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
- def resolve_in_header (in_header)
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
- "${#{in_header}}"
447
- end
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
- def apply (row, hash)
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
- @header.outs.each_with_index do |out_header, icol|
487
+ range = if RUBY_NUMERIC_RANGE_REGEXP.match(s)
452
488
 
453
- next unless out_header
489
+ eval(s)
454
490
 
455
- value = row[icol]
491
+ else
456
492
 
457
- next unless value
458
- #next unless value.strip.length > 0
459
- next unless value.length > 0
493
+ m = RUBY_ALPHA_RANGE_REGEXP.match(s)
460
494
 
461
- value = Rufus::dsub value, hash
495
+ m ? eval("'#{m[1]}'#{m[2]}'#{m[3]}'") : nil
496
+ end
462
497
 
463
- hash[out_header] = if @accumulate
464
- #
465
- # accumulate
498
+ class << range
466
499
 
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
500
+ alias :old_include? :include?
478
501
 
479
- value
480
- end
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
- def parse_header_row (row)
508
+ end if range
485
509
 
486
- row.each_with_index do |cell, icol|
510
+ range
511
+ end
487
512
 
488
- next unless cell
513
+ class Header
489
514
 
490
- cell = cell.strip
491
- s = cell.downcase
515
+ attr_accessor :ins, :outs
492
516
 
493
- if s == "ignorecase" or s == "ignore_case"
494
- @ignore_case = true
495
- next
496
- end
517
+ def initialize
497
518
 
498
- if s == "through"
499
- @first_match = false
500
- next
501
- end
519
+ @ins = {}
520
+ @outs = {}
521
+ end
502
522
 
503
- if s == "accumulate"
504
- @first_match = false
505
- @accumulate = true
506
- next
507
- end
523
+ def add (cell, x)
508
524
 
509
- if Rufus::starts_with?(cell, "in:") or \
510
- Rufus::starts_with?(cell, "out:")
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 empty_row? (row)
537
+ def to_csv
519
538
 
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
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
- # 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
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
- else
550
+ return csv if csv.is_a?(Array)
556
551
 
557
- m = RUBY_ALPHA_RANGE_REGEXP.match(s)
552
+ csv = csv.to_s if csv.is_a?(URI)
553
+ csv = open(csv) if is_uri?(csv)
558
554
 
559
- if m
560
- eval "'#{m[1]}'#{m[2]}'#{m[3]}'"
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
- class << range
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
- alias :old_include? :include?
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
- def include? (elt)
570
+ return false if string.index("\n") # quick one
571
571
 
572
- elt = if first.is_a?(Numeric)
573
- Float(elt)
574
- else
575
- elt
576
- end
572
+ begin
573
+ URI::parse(string); return true
574
+ rescue
575
+ end
577
576
 
578
- old_include?(elt)
579
- end
577
+ false
578
+ end
580
579
 
581
- end if range
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
- range
584
- end
601
+ a = csv_to_a(a) if a.is_a?(String)
585
602
 
586
- class Header
603
+ return a if a.empty?
587
604
 
588
- attr_accessor :ins, :outs
605
+ first = a.first
589
606
 
590
- def initialize
607
+ if first.is_a?(Hash)
591
608
 
592
- @ins = []
593
- @outs = []
594
- end
609
+ keys = first.keys.sort
610
+ [ keys ] + a.collect { |row|
611
+ keys.collect { |k| row[k] }
612
+ }
613
+ else
595
614
 
596
- def add (cell, icol)
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
- 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
622
+ end
623
+ end
607
624
 
608
- def to_csv
625
+ module Rufus
609
626
 
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
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