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