rufus-decision 0.9 → 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.
@@ -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