rufus-decision 1.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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