# frozen_string_literal: true
require 'rast/rules/operator'
require 'rast/rules/logic_helper'
require 'rast/converters/int_converter'
require 'rast/converters/float_converter'
require 'rast/converters/default_converter'
require 'rast/converters/bool_converter'
require 'rast/converters/str_converter'
# Evaluates the rules.
class RuleEvaluator
include LogicHelper
NOT = Operator.new(name: 'not', symbol: '!', precedence: 100)
AND = Operator.new(name: 'and', symbol: '&', precedence: 2)
OR = Operator.new(name: 'or', symbol: '|', precedence: 1)
OPERATORS = [NOT, AND, OR].freeze
OPERATORS_CONCAT = OPERATORS.map(&:to_s).join
# the "false" part of the "false[1]"
RE_TOKEN_BODY = /^.+(?=\[)/.freeze
RE_TOKENS = /([!|)(&])|([a-zA-Z\s0-9-]+\[\d\])/.freeze
def self.operator_from_symbol(symbol: nil)
OPERATORS.find { |operator| operator.symbol == symbol }
end
DEFAULT_CONVERT_HASH = {
Integer => IntConverter.new,
Float => FloatConverter.new,
Fixnum => FloatConverter.new,
Array => DefaultConverter.new,
TrueClass => BoolConverter.new,
FalseClass => BoolConverter.new,
String => StrConverter.new
}.freeze
# /** @param pConverterList list of rule token converters. */
def initialize(converters: [])
@converters = converters
@stack_operations = []
@stack_rpn = []
@stack_answer = []
end
# /**
# * Parses the math expression (complicated formula) and stores the result.
# *
# * @param pExpression String
input expression (logical
# * expression formula)
# * @since 0.3.0
# */
def parse(expression: '')
# /* cleaning stacks */
@stack_operations.clear
@stack_rpn.clear
tokens = if expression.is_a?(Array)
expression
else
RuleEvaluator.tokenize(clause: expression)
end
# /* loop for handling each token - shunting-yard algorithm */
tokens.each { |token| shunt_internal(token: token) }
@stack_rpn << @stack_operations.pop while @stack_operations.any?
@stack_rpn.reverse!
end
# splitting input string into tokens
# @ clause - rule clause to be tokenized
def self.tokenize(clause: '')
clause.to_s.split(RE_TOKENS).reject(&:empty?)
end
# /**
# * Evaluates once parsed math expression with "var" variable included.
# *
# * @param scenario List of values to evaluate against the rule expression.
# * @param rule_token_convert mapping of rule tokens to converter.
# * @return String
representation of the result
# */
def evaluate(scenario: [], rule_token_convert: {})
# /* check if is there something to evaluate */
if @stack_rpn.empty?
true
elsif @stack_rpn.size == 1
evaluate_one_rpn(scenario: scenario).to_s
else
evaluate_multi_rpn(
scenario: scenario,
rule_token_convert: rule_token_convert
)
end
end
# /**
# * @param rule_token_convert token to converter map.
# * @param default_converter default converter to use.
# */
def next_value(rule_token_convert: {}, default_converter: nil)
subscript = -1
retval = []
value = @stack_answer.pop
return [-1, value] if value.is_a? Array
if TRUE != value && FALSE != value
subscript = extract_subscript(token: value.to_s)
value_str = value.to_s.strip
if subscript > -1
converter = @converters[subscript]
value = converter.convert(value_str[/^.+(?=\[)/])
else
value = if rule_token_convert.nil? ||
rule_token_convert[value_str].nil?
default_converter.convert(value_str)
else
rule_token_convert[value_str].convert(value_str)
end
end
end
retval << subscript
retval << value
retval
end
# /** @param token token. */
def shunt_internal(token: '')
if open_bracket?(token: token)
@stack_operations << token
elsif close_bracket?(token: token)
while @stack_operations.any? &&
!open_bracket?(token: @stack_operations.last.strip)
@stack_rpn << @stack_operations.pop
end
@stack_operations.pop
elsif operator?(token: token)
while !@stack_operations.empty? &&
operator?(token: @stack_operations.last.strip) &&
precedence(symbol_char: token[0]) <=
precedence(symbol_char: @stack_operations.last.strip[0])
@stack_rpn << @stack_operations.pop
end
@stack_operations << token
else
@stack_rpn << token
end
end
private
# /**
# * Returns value of 'n' if rule token ends with '[n]'. where 'n' is the
# * variable group index.
# *
# * @param string token to check for subscript.
# */
def extract_subscript(token: '')
subscript = token[/\[(\d+)\]$/, 1]
subscript.nil? ? -1 : subscript.to_i
end
# /**
# * @param scenario List of values to evaluate against the rule expression.
# * @param rule_token_convert token to converter map.
# */
def evaluate_multi_rpn(scenario: [], rule_token_convert: {})
# /* clean answer stack */
@stack_answer.clear
# /* get the clone of the RPN stack for further evaluating */
stack_rpn_clone = Marshal.load(Marshal.dump(@stack_rpn))
# /* evaluating the RPN expression */
# binding.pry
while stack_rpn_clone.any?
token = stack_rpn_clone.pop
if operator?(token: token)
if NOT.symbol == token
evaluate_multi_not(scenario: scenario)
else
evaluate_multi(
scenario: scenario,
rule_token_convert: rule_token_convert,
operator: RuleEvaluator.operator_from_symbol(symbol: token[0])
)
end
else
@stack_answer << token
end
end
raise 'Some operator is missing' if @stack_answer.size > 1
last = @stack_answer.pop
last[1..last.size]
end
# /**
# * @param scenario List of values to evaluate against the rule expression.
# * @param rule_token_convert token to converter map.
# * @param operator OR/AND.
# */
def evaluate_multi(scenario: [], rule_token_convert: {}, operator: nil)
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
# binding.pry
left_arr = next_value(
rule_token_convert: rule_token_convert,
default_converter: default_converter
)
right_arr = next_value(
rule_token_convert: rule_token_convert,
default_converter: default_converter
)
answer = send(
"perform_logical_#{operator.name}",
scenario: scenario,
left_subscript: left_arr[0],
right_subscript: right_arr[0],
left: left_arr[1],
right: right_arr[1]
)
@stack_answer << if answer[0] == '|'
answer
else
"|#{answer}"
end
end
# /**
# * @param scenario List of values to evaluate against the rule expression.
# */
def evaluate_multi_not(scenario: [])
left = @stack_answer.pop.strip
# binding.pry
answer = if LogicHelper::TRUE == left
LogicHelper::FALSE
elsif LogicHelper::FALSE == left
LogicHelper::TRUE
else
subscript = extract_subscript(token: left)
if subscript < 0
(!scenario.include?(left)).to_s
else
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
converted = default_converter.convert(left[RE_TOKEN_BODY])
(scenario[subscript] == converted).to_s
end
end
@stack_answer << if answer[0] == '|'
answer
else
"|#{answer}"
end
end
# /** @param scenario to evaluate against the rule expression. */
def evaluate_one_rpn(scenario: [])
single = @stack_rpn.last
subscript = extract_subscript(token: single)
default_converter = DEFAULT_CONVERT_HASH[scenario.first.class]
if subscript > -1
scenario[subscript] == default_converter.convert(single[RE_TOKEN_BODY])
else
scenario.include?(default_converter.convert(single))
end
end
def operator?(token: '')
!token.nil? && OPERATORS.map(&:symbol).include?(token)
end
def precedence(symbol_char: '')
RuleEvaluator.operator_from_symbol(symbol: symbol_char).precedence
end
end