rufus-decision 0.9 → 1.0

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