| Class | OpenWFE::Extras::CsvTable |
| In: |
lib/openwfe/extras/util/csvtable.rb
|
| Parent: | Object |
A ‘CsvTable’ is called a ‘decision table’ in OpenWFEja (the initial Java implementation of OpenWFE).
A csv table is a description of a set of rules as a CSV (comma separated values) file. Such a file can be edited / generated by a spreadsheet (Excel, Google spreadsheets, Gnumeric, …)
The following CSV file
in:topic,in:region,out:team_member
sports,europe,Alice
sports,,Bob
finance,america,Charly
finance,europe,Donald
finance,,Ernest
politics,asia,Fujio
politics,america,Gilbert
politics,,Henry
,,Zach
embodies a rule for distributing items (piece of news) labelled with a topic and a region to various members of a team. For example, all news about finance from Europe are to be routed to Donald.
Evaluation occurs row by row. The "in out" row states which field is considered at input and which are to be modified if the "ins" do match.
The default behaviour is to change the value of the "outs" if all the "ins" match and then terminate. An empty "in" cell means "matches any".
Enough words, some code :
table = CsvTable.new("""
in:topic,in:region,out:team_member
sports,europe,Alice
sports,,Bob
finance,america,Charly
finance,europe,Donald
finance,,Ernest
politics,asia,Fujio
politics,america,Gilbert
politics,,Henry
,,Zach
""")
h = {}
h["topic"] = "politics"
table.transform(h)
puts h["team_member"]
# will yield "Henry" who takes care of all the politics stuff,
# except for Asia and America
’>’, ’>=’, ’<’ and ’<=’ can be put in front of individual cell values :
table = CsvTable.new("""
,
in:fx, out:fy
,
>100,a
>=10,b
,c
""")
h = { 'fx' => '10' }
table.transform(h)
require 'pp'; pp h
# will yield { 'fx' => '10', 'fy' => 'b' }
Such comparisons are done after the elements are transformed to float numbers. By default, non-numeric arguments will get compared as Strings.
Ruby ranges are also accepted in cells.
in:f0,out:result
,
0..32,low
33..66,medium
67..100,high
will set the field ‘result’ to ‘low’ for f0 => 24
Disclaimer : the decision / CSV table system is no replacement for full rule engines with forward and backward chaining, RETE implementation and the like…
Options :
You can put options on their own in a cell BEFORE the line containing "in:xxx" and "out:yyy" (ins and outs).
Currently, two options are supported, "ignorecase" and "through".
accumulate in:f0,out:result , ,normal >10,large >100,xl
will yield { result => [ ‘normal’, ‘large’ ]} for f0 => 56
CSV Tables are available to workflows as CsvParticipant.
See also :
| accumulate | [RW] | |
| first_match | [RW] | |
| header | [RW] | |
| ignore_case | [RW] | |
| rows | [RW] |
The constructor for CsvTable, you can pass a String, an Array (of arrays), a File object. The CSV parser coming with Ruby will take care of it and a CsvTable instance will be built.
# File lib/openwfe/extras/util/csvtable.rb, line 251
251: def initialize (csv_data)
252:
253: @first_match = true
254: @ignore_case = false
255: @accumulate = false
256:
257: @header = nil
258: @rows = []
259:
260: csv_array = to_csv_array(csv_data)
261:
262: csv_array.each do |row|
263:
264: next if empty_row? row
265:
266: if @header
267: #@rows << row
268: @rows << row.collect { |c| c.strip if c }
269: else
270: parse_header_row(row)
271: end
272: end
273: end
Outputs back this table as a CSV String
# File lib/openwfe/extras/util/csvtable.rb, line 305
305: def to_csv
306:
307: s = ""
308: s << @header.to_csv
309: s << "\n"
310: @rows.each do |row|
311: s << row.join(",")
312: s << "\n"
313: end
314: s
315: end
Passes a simple Hash instance though the csv table
# File lib/openwfe/extras/util/csvtable.rb, line 294
294: def transform (hash)
295:
296: wi = InFlowWorkItem.new()
297: wi.attributes = hash
298:
299: transform_wi(nil, wi).attributes
300: end
Returns the workitem massaged by the csv table
# File lib/openwfe/extras/util/csvtable.rb, line 278
278: def transform_wi (flow_expression, workitem)
279:
280: @rows.each do |row|
281:
282: if matches?(row, flow_expression, workitem)
283: apply row, flow_expression, workitem
284: break if @first_match
285: end
286: end
287:
288: workitem
289: end
‘accumulate’ is on, this method got called to compute the new value.
If it finds nothing in the target field, the new value is value. If there is already a value and its an array, the new value will be current_array + value. Else the two values (current and new) are combined into an array.
Sorry, if you want f([ x ], y) -> [[ x ], y]… It‘s not implemented like that.
# File lib/openwfe/extras/util/csvtable.rb, line 486
486: def accumulate_values (type, target, value, workitem, fexp)
487:
488: current_value = case type
489: when 'v'
490: if fexp
491: fexp.lookup_variable target
492: else
493: nil
494: end
495: when 'f'
496: workitem.lookup_attribute target
497: when 'r'
498: nil
499: end
500:
501: return value unless current_value
502:
503: return current_value + Array(value) \
504: if current_value.is_a?(Array)
505:
506: [ current_value, value ]
507: end
# File lib/openwfe/extras/util/csvtable.rb, line 436
436: def apply (row, fexp, wi)
437:
438: #puts "__ apply() wi.class : #{wi.class.name}"
439:
440: @header.outs.each_with_index do |out_header, icol|
441:
442: next unless out_header
443:
444: value = row[icol]
445:
446: next unless value
447: #next unless value.strip.length > 0
448: next unless value.length > 0
449:
450: value = OpenWFE::dosub(value, fexp, wi)
451:
452: #puts "___ value.class:#{value.class}"
453: #puts "___ value:'#{value}'"
454: #puts "___ value:'"+value+"'"
455:
456: type, target = points_at(out_header)
457:
458: #puts "___ t:'#{type}' target:'#{target}'"
459:
460: value = accumulate_values(type, target, value, wi, fexp) \
461: if @accumulate
462:
463: if type == "v"
464: fexp.set_variable(target, value) if fexp
465: elsif type == "f"
466: wi.set_attribute(target, value)
467: elsif type == "r"
468: OpenWFE::instance_eval_safely(self, value, 3)
469: end
470: end
471: end
# File lib/openwfe/extras/util/csvtable.rb, line 541
541: def empty_row? (row)
542:
543: return true unless row
544: return true if (row.length == 1 and not row[0])
545: row.each do |cell|
546: return false if cell
547: end
548: true
549: end
# File lib/openwfe/extras/util/csvtable.rb, line 333
333: def matches? (row, fexp, wi)
334:
335: return false if empty_row?(row)
336:
337: #puts
338: #puts "__row match ?"
339: #require 'pp'
340: #pp row
341:
342: @header.ins.each_with_index do |in_header, icol|
343:
344: in_header = resolve_in_header(in_header)
345:
346: value = OpenWFE::dosub(in_header, fexp, wi)
347:
348: cell = row[icol]
349:
350: next if not cell
351:
352: cell = cell.strip
353:
354: next if cell.length < 1
355:
356: cell = OpenWFE::dosub(cell, fexp, wi)
357:
358: #puts "__does '#{value}' match '#{cell}' ?"
359:
360: b = if cell[0, 1] == '<' or cell[0, 1] == '>'
361:
362: numeric_compare value, cell
363: else
364:
365: range = to_ruby_range cell
366: if range
367: range.include?(value)
368: else
369: regex_compare value, cell
370: end
371: end
372:
373: return false unless b
374: end
375:
376: #puts "__row matches"
377:
378: true
379: end
# File lib/openwfe/extras/util/csvtable.rb, line 420
420: def narrow (s)
421: begin
422: return Float(s)
423: rescue Exception => e
424: end
425: s
426: end
# File lib/openwfe/extras/util/csvtable.rb, line 391
391: def numeric_compare (value, cell)
392:
393: comparator = cell[0, 1]
394: comparator += "=" if cell[1, 1] == "="
395: cell = cell[comparator.length..-1]
396:
397: nvalue = narrow(value)
398: ncell = narrow(cell)
399:
400: if nvalue.is_a? String or ncell.is_a? String
401: value = '"' + value + '"'
402: cell = '"' + cell + '"'
403: else
404: value = nvalue
405: cell = ncell
406: end
407:
408: s = "#{value} #{comparator} #{cell}"
409:
410: #puts "...>>>#{s}<<<"
411:
412: begin
413: return OpenWFE::eval_safely(s, 4)
414: rescue Exception => e
415: end
416:
417: false
418: end
# File lib/openwfe/extras/util/csvtable.rb, line 509
509: def parse_header_row (row)
510:
511: row.each_with_index do |cell, icol|
512:
513: next unless cell
514:
515: cell = cell.strip
516: s = cell.downcase
517:
518: if s == "ignorecase" or s == "ignore_case"
519: @ignore_case = true
520: next
521: end
522:
523: if s == "through"
524: @first_match = false
525: next
526: end
527:
528: if s == "accumulate"
529: @first_match = false
530: @accumulate = true
531: next
532: end
533:
534: if OpenWFE::starts_with(cell, "in:") or OpenWFE::starts_with(cell, "out:")
535: @header = Header.new unless @header
536: @header.add cell, icol
537: end
538: end
539: end
# File lib/openwfe/extras/util/csvtable.rb, line 556
556: def points_at (label)
557: v = points_to_variable? label
558: return "v", v if v
559: r = points_to_ruby? label
560: return "r", r if r
561: f = points_to_field? label
562: return "f", f if f
563: return "f", label # else
564: end
# File lib/openwfe/extras/util/csvtable.rb, line 574
574: def points_to? (label, names)
575: i = label.index(":")
576: return nil unless i
577: s = label[0..i-1]
578: names.each do |name|
579: return label[i+1..-1] if s == name
580: end
581: nil
582: end
# File lib/openwfe/extras/util/csvtable.rb, line 568
568: def points_to_field? (label)
569: points_to?(label, [ "f", "field" ])
570: end
# File lib/openwfe/extras/util/csvtable.rb, line 551
551: def points_to_nothing? (label)
552: (not points_to_variable? label) and \
553: (not points_to_field? label) and \
554: (not points_to_ruby? label)
555: end
# File lib/openwfe/extras/util/csvtable.rb, line 571
571: def points_to_ruby? (label)
572: points_to?(label, [ "r", "ruby", "reval" ])
573: end
# File lib/openwfe/extras/util/csvtable.rb, line 565
565: def points_to_variable? (label)
566: points_to?(label, [ "v", "var", "variable" ])
567: end
# File lib/openwfe/extras/util/csvtable.rb, line 381
381: def regex_compare (value, cell)
382:
383: modifiers = 0
384: modifiers += Regexp::IGNORECASE if @ignore_case
385:
386: rcell = Regexp.new(cell, modifiers)
387:
388: rcell.match(value)
389: end
# File lib/openwfe/extras/util/csvtable.rb, line 428
428: def resolve_in_header (in_header)
429:
430: in_header = "f:#{in_header}" \
431: if points_to_nothing?(in_header)
432:
433: "${#{in_header}}"
434: end
# File lib/openwfe/extras/util/csvtable.rb, line 319
319: def to_csv_array (csv_data)
320:
321: return csv_data if csv_data.kind_of?(Array)
322:
323: if csv_data.is_a?(URI)
324: csv_data = csv_data.to_s
325: end
326: if OpenWFE::parse_uri(csv_data)
327: csv_data = open(csv_data)
328: end
329:
330: CSV::Reader.parse(csv_data)
331: end