lib/openwfe/extras/util/csvtable.rb in openwferu-extras-0.9.15 vs lib/openwfe/extras/util/csvtable.rb in openwferu-extras-0.9.16
- old
+ new
@@ -49,10 +49,67 @@
module OpenWFE
module Extras
#
+ # A regexp for checking if a string is a numeric Ruby range
+ #
+ RUBY_NUMERIC_RANGE_REGEXP = Regexp.compile(
+ "^\\d+(\\.\\d+)?\\.{2,3}\\d+(\\.\\d+)?$")
+
+ #
+ # A regexp for checking if a string is an alpha Ruby range
+ #
+ RUBY_ALPHA_RANGE_REGEXP = Regexp.compile(
+ "^([A-Za-z])(\\.{2,3})([A-Za-z])$")
+
+ #
+ # If the string contains a Ruby range definition
+ # (ie something like "93.0..94.5" or "56..72"), it will return
+ # the Range instance.
+ # Will return nil else.
+ #
+ # The Ruby range returned (if any) will accept String or Numeric,
+ # ie (4..6).include?("5") will yield true.
+ #
+ def to_ruby_range (s)
+
+ range = if RUBY_NUMERIC_RANGE_REGEXP.match(s)
+
+ eval s
+ else
+
+ m = RUBY_ALPHA_RANGE_REGEXP.match(s)
+
+ if m
+ eval "'#{m[1]}'#{m[2]}'#{m[3]}'"
+ else
+ nil
+ end
+ end
+
+ class << range
+
+ alias :old_include? :include?
+
+ def include? (elt)
+
+ elt = if first.is_a?(Numeric)
+ Float(elt)
+ else
+ elt
+ end
+
+ old_include?(elt)
+ end
+
+ end if range
+
+ range
+ end
+
+ #
# 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
@@ -126,11 +183,20 @@
# # 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...
#
#
@@ -139,43 +205,56 @@
# 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".
#
- # "ignorecase", if found by the CsvTable will make any match (in the "in"
- # columns) case unsensitive.
+ # * "ignorecase", if found by the CsvTable will make any match (in the "in"
+ # columns) case unsensitive.
#
- # "through", will make sure that EVERY row is evaluated and potentially
- # applied. The default behaviour (without "through"), is to stop the
- # evaluation after applying the results of the first matching row.
+ # * "through", will make sure that EVERY row is evaluated and potentially
+ # applied. The default behaviour (without "through"), is to stop the
+ # evaluation after applying the results of the first matching row.
#
+ # * "accumulate", behaves as with "through" set but instead of overriding
+ # values each time a match is found, will gather them in an array.
#
+ # 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 :
#
- # http://jmettraux.wordpress.com/2007/02/11/ruby-decision-tables/
- # http://rubyforge.org/viewvc/trunk/openwfe-ruby/test/extras/csv_test.rb?root=openwferu&view=co
+ # * http://jmettraux.wordpress.com/2007/02/11/ruby-decision-tables/
+ # * http://rubyforge.org/viewvc/trunk/openwfe-ruby/test/extras/csv_test.rb?root=openwferu&view=co
#
class CsvTable
attr_accessor \
:first_match,
:ignore_case,
:header,
- :rows
+ :rows,
+ :accumulate
#
# 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.
#
def initialize (csv_data)
@first_match = true
@ignore_case = false
+ @accumulate = false
@header = nil
@rows = []
csv_array = to_csv_array(csv_data)
@@ -199,11 +278,11 @@
def transform_wi (flow_expression, workitem)
@rows.each do |row|
if matches?(row, flow_expression, workitem)
- apply(row, flow_expression, workitem)
+ apply row, flow_expression, workitem
break if @first_match
end
end
workitem
@@ -251,11 +330,11 @@
CSV::Reader.parse(csv_data)
end
def matches? (row, fexp, wi)
- return false if empty_row? row
+ return false if empty_row?(row)
#puts
#puts "__row match ?"
#require 'pp'
#pp row
@@ -277,13 +356,20 @@
cell = OpenWFE::dosub(cell, fexp, wi)
#puts "__does '#{value}' match '#{cell}' ?"
b = if cell[0, 1] == '<' or cell[0, 1] == '>'
- numeric_compare(value, cell)
+
+ numeric_compare value, cell
else
- regex_compare(value, cell)
+
+ range = to_ruby_range cell
+ if range
+ range.include?(value)
+ else
+ regex_compare value, cell
+ end
end
return false unless b
end
@@ -324,20 +410,21 @@
#puts "...>>>#{s}<<<"
begin
return OpenWFE::eval_safely(s, 4)
rescue Exception => e
- return false
end
+
+ false
end
def narrow (s)
begin
return Float(s)
rescue Exception => e
- return s
end
+ s
end
def resolve_in_header (in_header)
in_header = "f:#{in_header}" \
@@ -360,27 +447,67 @@
#next unless value.strip.length > 0
next unless value.length > 0
value = OpenWFE::dosub(value, fexp, wi)
+ #puts "___ value.class:#{value.class}"
#puts "___ value:'#{value}'"
#puts "___ value:'"+value+"'"
type, target = points_at(out_header)
#puts "___ t:'#{type}' target:'#{target}'"
+ value = accumulate_values(type, target, value, wi, fexp) \
+ if @accumulate
+
if type == "v"
fexp.set_variable(target, value) if fexp
elsif type == "f"
wi.set_attribute(target, value)
elsif type == "r"
OpenWFE::instance_eval_safely(self, value, 3)
end
end
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.
+ #
+ def accumulate_values (type, target, value, workitem, fexp)
+
+ current_value = case type
+ when 'v'
+ if fexp
+ fexp.lookup_variable target
+ else
+ nil
+ end
+ when 'f'
+ workitem.lookup_attribute target
+ when 'r'
+ nil
+ end
+
+ return value unless current_value
+
+ return current_value + Array(value) \
+ if current_value.is_a?(Array)
+
+ [ current_value, value ]
+ end
+
def parse_header_row (row)
row.each_with_index do |cell, icol|
next unless cell
@@ -393,9 +520,15 @@
next
end
if s == "through"
@first_match = false
+ next
+ end
+
+ if s == "accumulate"
+ @first_match = false
+ @accumulate = true
next
end
if OpenWFE::starts_with(cell, "in:") or OpenWFE::starts_with(cell, "out:")
@header = Header.new unless @header