lib/dnf/boolean_expression.rb in dnf-0.1.0 vs lib/dnf/boolean_expression.rb in dnf-0.2.0

- old
+ new

@@ -1,56 +1,117 @@ module Dnf class BooleanExpression - attr_reader :expression + attr_reader :expression, :config - def initialize(expression) + DEFAULT_CONFIG = { + variable_regex: /\w+/, + not_symbol: '!', + and_symbol: '&', + or_symbol: '|' + } + + def initialize(expression, custom_config = {}) @expression = expression + @config = DEFAULT_CONFIG.merge(custom_config) end def to_dnf expr = parse(expression) dnf = convert_to_dnf(expr) dnf_to_string(dnf) end private + def validate_expression(tokens) + raise ExpressionSyntaxError, "Expression cannot be empty" if tokens.empty? + validate_tokens(tokens) + validate_syntax(tokens) + validate_parentheses(tokens) + end + + def validate_tokens(tokens) + valid_tokens = [ + config[:not_symbol], config[:and_symbol], config[:or_symbol], '(', ')' + ] + tokens.each do |token| + next if valid_tokens.include?(token) || token.match?(/\A#{config[:variable_regex].source}\z/) + raise ExpressionSyntaxError, "Invalid token: #{token}" + end + end + + def validate_syntax(tokens) + tokens.each_cons(2) do |prev, curr| + case prev + when config[:variable_regex], ')' + raise ExpressionSyntaxError, "Unexpected token: #{prev} #{curr}" unless curr == config[:and_symbol] || curr == config[:or_symbol] || curr == ')' + when config[:not_symbol] + raise ExpressionSyntaxError, "Unexpected token: #{prev} #{curr}" unless curr.match?(/\A#{config[:variable_regex].source}\z/) || curr == '(' + when config[:and_symbol], config[:or_symbol], '(' + raise ExpressionSyntaxError, "Unexpected token: #{prev} #{curr}" unless curr.match?(/\A#{config[:variable_regex].source}\z/) || curr == config[:not_symbol] || curr == '(' + end + end + raise ExpressionSyntaxError, "Unexpected end: #{tokens.last}" unless tokens.last.match?(/\A#{config[:variable_regex].source}\z/) || tokens.last == ')' + end + + def validate_parentheses(tokens) + stack = [] + tokens.each do |token| + if token == '(' + stack.push(token) + elsif token == ')' + raise ExpressionSyntaxError, "Mismatched parentheses" if stack.empty? + stack.pop + end + end + raise ExpressionSyntaxError, "Mismatched parentheses" unless stack.empty? + end + def parse(expr) tokens = tokenize(expr) + validate_expression(tokens) parse_expression(tokens) end def tokenize(expr) - expr.scan(/\w+|[&|!()]/) + regex = Regexp.union( + config[:variable_regex], + config[:not_symbol], + config[:and_symbol], + config[:or_symbol], + /[()]/, + /\S+/ + ) + expr.scan(regex) end def parse_expression(tokens) parse_or(tokens) end def parse_or(tokens) left = parse_and(tokens) - while tokens.first == '|' + while tokens.first == config[:or_symbol] tokens.shift right = parse_and(tokens) left = [:or, left, right] end left end def parse_and(tokens) left = parse_not(tokens) - while tokens.first == '&' + while tokens.first == config[:and_symbol] tokens.shift right = parse_not(tokens) left = [:and, left, right] end left end def parse_not(tokens) - if tokens.first == '!' + if tokens.first == config[:not_symbol] tokens.shift expr = parse_primary(tokens) [:not, expr] else parse_primary(tokens) @@ -117,14 +178,14 @@ def dnf_to_string(expr) case expr[0] when :var expr[1] when :not - "!#{dnf_to_string(expr[1])}" + "#{config[:not_symbol]}#{dnf_to_string(expr[1])}" when :and - "#{dnf_to_string(expr[1])} & #{dnf_to_string(expr[2])}" + "#{dnf_to_string(expr[1])} #{config[:and_symbol]} #{dnf_to_string(expr[2])}" when :or - "#{dnf_to_string(expr[1])} | #{dnf_to_string(expr[2])}" + "#{dnf_to_string(expr[1])} #{config[:or_symbol]} #{dnf_to_string(expr[2])}" end end end end