Module: ActionAgent::AgentToolbox::Calculator

Defined in:
app/services/action_agent/agent_toolbox.rb

Overview

Minimal recursive-descent arithmetic evaluator (no eval). Grammar: expr := term (('+'|'-') term); term := factor ((''|'/'|'%') factor)*; factor := '-'? (number | '(' expr ')')

Defined Under Namespace

Classes: Error

Class Method Summary collapse

Class Method Details

.evaluate(expression) ⇒ Object

Raises:



439
440
441
442
443
444
445
446
# File 'app/services/action_agent/agent_toolbox.rb', line 439

def evaluate(expression)
  tokens = tokenize(expression)
  result, rest = parse_expr(tokens)
  raise Error, "Unexpected input: #{rest.join(' ')}" unless rest.empty?

  result = result.to_f
  result % 1 == 0 ? result.to_i : result.round(10)
end

.parse_expr(tokens) ⇒ Object



457
458
459
460
461
462
463
464
465
# File 'app/services/action_agent/agent_toolbox.rb', line 457

def parse_expr(tokens)
  value, tokens = parse_term(tokens)
  while tokens.first == "+" || tokens.first == "-"
    op = tokens.shift
    rhs, tokens = parse_term(tokens)
    value = op == "+" ? value + rhs : value - rhs
  end
  [ value, tokens ]
end

.parse_factor(tokens) ⇒ Object

Raises:



482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
# File 'app/services/action_agent/agent_toolbox.rb', line 482

def parse_factor(tokens)
  raise Error, "Unexpected end of expression" if tokens.empty?

  if tokens.first == "-"
    tokens.shift
    value, tokens = parse_factor(tokens)
    return [ -value, tokens ]
  end

  token = tokens.shift
  if token == "("
    value, tokens = parse_expr(tokens)
    raise Error, "Missing closing parenthesis" unless tokens.shift == ")"
    [ value, tokens ]
  elsif token =~ /\A\d/
    [ token.include?(".") ? token.to_f : Rational(token), tokens ]
  else
    raise Error, "Unexpected token: #{token}"
  end
end

.parse_term(tokens) ⇒ Object



467
468
469
470
471
472
473
474
475
476
477
478
479
480
# File 'app/services/action_agent/agent_toolbox.rb', line 467

def parse_term(tokens)
  value, tokens = parse_factor(tokens)
  while [ "*", "/", "%" ].include?(tokens.first)
    op = tokens.shift
    rhs, tokens = parse_factor(tokens)
    raise Error, "Division by zero" if rhs.zero? && op != "*"
    value = case op
    when "*" then value * rhs
    when "/" then value / rhs
    else value % rhs
    end
  end
  [ value, tokens ]
end

.tokenize(expression) ⇒ Object

Raises:



448
449
450
451
452
453
454
455
# File 'app/services/action_agent/agent_toolbox.rb', line 448

def tokenize(expression)
  tokens = expression.scan(%r{\d+(?:\.\d+)?|[-+*/%()]|\S})
  invalid = tokens.grep_v(%r{\A(?:\d+(?:\.\d+)?|[-+*/%()])\z})
  raise Error, "Unsupported characters: #{invalid.uniq.join(' ')}" if invalid.any?
  raise Error, "Empty expression" if tokens.empty?

  tokens
end