dentaku 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.travis.yml +6 -1
- data/README.md +16 -3
- data/lib/dentaku.rb +9 -0
- data/lib/dentaku/binary_operation.rb +1 -1
- data/lib/dentaku/calculator.rb +9 -31
- data/lib/dentaku/evaluator.rb +19 -11
- data/lib/dentaku/expression.rb +55 -0
- data/lib/dentaku/token.rb +4 -0
- data/lib/dentaku/token_matcher.rb +13 -9
- data/lib/dentaku/token_scanner.rb +3 -3
- data/lib/dentaku/version.rb +1 -1
- data/spec/binary_operation_spec.rb +14 -14
- data/spec/calculator_spec.rb +62 -62
- data/spec/dentaku_spec.rb +4 -4
- data/spec/evaluator_spec.rb +44 -44
- data/spec/expression_spec.rb +25 -0
- data/spec/external_function_spec.rb +11 -11
- data/spec/token_matcher_spec.rb +45 -45
- data/spec/token_scanner_spec.rb +11 -11
- data/spec/token_spec.rb +4 -4
- data/spec/tokenizer_spec.rb +75 -57
- metadata +5 -2
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA1:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: 8e51005fddbd5c542da8892bb3db03358e7d4cdf
         | 
| 4 | 
            +
              data.tar.gz: 7c13a0a048a14e5bb40151224a95f7a2635ae9fa
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 46026444a4b39bdc8df5f8913422619ac54b52d671e030b5599aa2db9642beabfdfd0c4e79ac9f40dc211de34e3ec791420aed2e162f86dd373c460229e6350a
         | 
| 7 | 
            +
              data.tar.gz: 14409916459c8b1481e6f9e1005bd7c2bf49f9c84e7b29a3a97cc2161d92bf0615bde9b3b08005e177e9fcd188d76298324041fd54b36f966961e52ee4ecdf4f
         | 
    
        data/.travis.yml
    CHANGED
    
    
    
        data/README.md
    CHANGED
    
    | @@ -10,7 +10,8 @@ DESCRIPTION | |
| 10 10 |  | 
| 11 11 | 
             
            Dentaku is a parser and evaluator for a mathematical and logical formula
         | 
| 12 12 | 
             
            language that allows run-time binding of values to variables referenced in the
         | 
| 13 | 
            -
            formulas.
         | 
| 13 | 
            +
            formulas.  It is intended to safely evaluate untrusted expressions without
         | 
| 14 | 
            +
            opening security holes.
         | 
| 14 15 |  | 
| 15 16 | 
             
            EXAMPLE
         | 
| 16 17 | 
             
            -------
         | 
| @@ -129,7 +130,9 @@ and the exponent, so the token list could be defined as: `[:numeric, | |
| 129 130 | 
             
            :numeric]`.  Other functions might be variadic -- consider `max`, a function
         | 
| 130 131 | 
             
            that takes any number of numeric inputs and returns the largest one.  Its token
         | 
| 131 132 | 
             
            list could be defined as: `[:non_close_plus]` (one or more tokens that are not
         | 
| 132 | 
            -
            closing parentheses.
         | 
| 133 | 
            +
            closing parentheses).  See the
         | 
| 134 | 
            +
            [rules definitions](https://github.com/rubysolo/dentaku/blob/master/lib/dentaku/token_matcher.rb#L61)
         | 
| 135 | 
            +
            for the names of token patterns you can use.
         | 
| 133 136 |  | 
| 134 137 | 
             
            Functions can be added individually using Calculator#add_function, or en masse using
         | 
| 135 138 | 
             
            Calculator#add_functions.
         | 
| @@ -169,7 +172,17 @@ THANKS | |
| 169 172 | 
             
            ------
         | 
| 170 173 |  | 
| 171 174 | 
             
            Big thanks to [ElkStone Basements](http://www.elkstonebasements.com/) for
         | 
| 172 | 
            -
            allowing me to extract and open source this code.
         | 
| 175 | 
            +
            allowing me to extract and open source this code.  Thanks also to all the
         | 
| 176 | 
            +
            contributors:
         | 
| 177 | 
            +
             | 
| 178 | 
            +
            * [CraigCottingham](https://github.com/CraigCottingham)
         | 
| 179 | 
            +
            * [arnaudl](https://github.com/arnaudl)
         | 
| 180 | 
            +
            * [thbar](https://github.com/thbar) / [BoxCar](https://www.boxcar.io)
         | 
| 181 | 
            +
            * [antonversal](https://github.com/antonversal)
         | 
| 182 | 
            +
            * [mvbrocato](https://github.com/mvbrocato)
         | 
| 183 | 
            +
            * [brixen](https://github.com/brixen)
         | 
| 184 | 
            +
            * [0xCCD](https://github.com/0xCCD)
         | 
| 185 | 
            +
             | 
| 173 186 |  | 
| 174 187 | 
             
            LICENSE
         | 
| 175 188 | 
             
            -------
         | 
    
        data/lib/dentaku.rb
    CHANGED
    
    | @@ -1,3 +1,4 @@ | |
| 1 | 
            +
            require "bigdecimal"
         | 
| 1 2 | 
             
            require "dentaku/calculator"
         | 
| 2 3 | 
             
            require "dentaku/version"
         | 
| 3 4 |  | 
| @@ -6,6 +7,14 @@ module Dentaku | |
| 6 7 | 
             
                calculator.evaluate(expression, data)
         | 
| 7 8 | 
             
              end
         | 
| 8 9 |  | 
| 10 | 
            +
              class UnboundVariableError < StandardError
         | 
| 11 | 
            +
                attr_reader :unbound_variables
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def initialize(unbound_variables)
         | 
| 14 | 
            +
                  @unbound_variables = unbound_variables
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
             | 
| 9 18 | 
             
              private
         | 
| 10 19 |  | 
| 11 20 | 
             
              def self.calculator
         | 
| @@ -15,7 +15,7 @@ module Dentaku | |
| 15 15 | 
             
                def divide
         | 
| 16 16 | 
             
                  quotient, remainder = left.divmod(right)
         | 
| 17 17 | 
             
                  return [:numeric, quotient] if remainder == 0
         | 
| 18 | 
            -
                  [:numeric, left. | 
| 18 | 
            +
                  [:numeric, BigDecimal.new(left.to_s) / BigDecimal.new(right.to_s)]
         | 
| 19 19 | 
             
                end
         | 
| 20 20 |  | 
| 21 21 | 
             
                def mod;      [:numeric, left % right]; end
         | 
    
        data/lib/dentaku/calculator.rb
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            require 'dentaku/evaluator'
         | 
| 2 | 
            +
            require 'dentaku/expression'
         | 
| 2 3 | 
             
            require 'dentaku/rules'
         | 
| 3 4 | 
             
            require 'dentaku/token'
         | 
| 4 | 
            -
            require 'dentaku/tokenizer'
         | 
| 5 5 |  | 
| 6 6 | 
             
            module Dentaku
         | 
| 7 7 | 
             
              class Calculator
         | 
| @@ -22,19 +22,20 @@ module Dentaku | |
| 22 22 | 
             
                end
         | 
| 23 23 |  | 
| 24 24 | 
             
                def evaluate(expression, data={})
         | 
| 25 | 
            -
                   | 
| 26 | 
            -
             | 
| 25 | 
            +
                  evaluate!(expression, data)
         | 
| 26 | 
            +
                rescue UnboundVariableError
         | 
| 27 | 
            +
                  yield expression if block_given?
         | 
| 28 | 
            +
                end
         | 
| 27 29 |  | 
| 30 | 
            +
                def evaluate!(expression, data={})
         | 
| 28 31 | 
             
                  store(data) do
         | 
| 32 | 
            +
                    expr = Expression.new(expression, @memory)
         | 
| 33 | 
            +
                    raise UnboundVariableError.new(expr.identifiers) if expr.unbound?
         | 
| 29 34 | 
             
                    @evaluator ||= Evaluator.new
         | 
| 30 | 
            -
                    @result = @evaluator.evaluate( | 
| 35 | 
            +
                    @result = @evaluator.evaluate(expr.tokens)
         | 
| 31 36 | 
             
                  end
         | 
| 32 37 | 
             
                end
         | 
| 33 38 |  | 
| 34 | 
            -
                def memory(key=nil)
         | 
| 35 | 
            -
                  key ? @memory[key.to_sym] : @memory
         | 
| 36 | 
            -
                end
         | 
| 37 | 
            -
             | 
| 38 39 | 
             
                def store(key_or_hash, value=nil)
         | 
| 39 40 | 
             
                  restore = @memory.dup
         | 
| 40 41 |  | 
| @@ -63,28 +64,5 @@ module Dentaku | |
| 63 64 | 
             
                def empty?
         | 
| 64 65 | 
             
                  @memory.empty?
         | 
| 65 66 | 
             
                end
         | 
| 66 | 
            -
             | 
| 67 | 
            -
                private
         | 
| 68 | 
            -
             | 
| 69 | 
            -
                def replace_identifiers_with_values
         | 
| 70 | 
            -
                  @tokens.map do |token|
         | 
| 71 | 
            -
                    if token.is?(:identifier)
         | 
| 72 | 
            -
                      value = memory(token.value)
         | 
| 73 | 
            -
                      type  = type_for_value(value)
         | 
| 74 | 
            -
             | 
| 75 | 
            -
                      Token.new(type, value)
         | 
| 76 | 
            -
                    else
         | 
| 77 | 
            -
                      token
         | 
| 78 | 
            -
                    end
         | 
| 79 | 
            -
                  end
         | 
| 80 | 
            -
                end
         | 
| 81 | 
            -
             | 
| 82 | 
            -
                def type_for_value(value)
         | 
| 83 | 
            -
                  case value
         | 
| 84 | 
            -
                  when String then :string
         | 
| 85 | 
            -
                  when TrueClass, FalseClass then :logical
         | 
| 86 | 
            -
                  else :numeric
         | 
| 87 | 
            -
                  end
         | 
| 88 | 
            -
                end
         | 
| 89 67 | 
             
              end
         | 
| 90 68 | 
             
            end
         | 
    
        data/lib/dentaku/evaluator.rb
    CHANGED
    
    | @@ -10,7 +10,7 @@ module Dentaku | |
| 10 10 | 
             
                def evaluate_token_stream(tokens)
         | 
| 11 11 | 
             
                  while tokens.length > 1
         | 
| 12 12 | 
             
                    matched, tokens = match_rule_pattern(tokens)
         | 
| 13 | 
            -
                    raise "no rule matched #{ tokens | 
| 13 | 
            +
                    raise "no rule matched {{#{ inspect_tokens(tokens) }}}" unless matched
         | 
| 14 14 | 
             
                  end
         | 
| 15 15 |  | 
| 16 16 | 
             
                  tokens << Token.new(:numeric, 0) if tokens.empty?
         | 
| @@ -18,6 +18,10 @@ module Dentaku | |
| 18 18 | 
             
                  tokens.first
         | 
| 19 19 | 
             
                end
         | 
| 20 20 |  | 
| 21 | 
            +
                def inspect_tokens(tokens)
         | 
| 22 | 
            +
                  tokens.map { |t| t.to_s }.join(' ')
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 21 25 | 
             
                def match_rule_pattern(tokens)
         | 
| 22 26 | 
             
                  matched = false
         | 
| 23 27 | 
             
                  Rules.each do |pattern, evaluator|
         | 
| @@ -41,8 +45,8 @@ module Dentaku | |
| 41 45 | 
             
                    matched = true
         | 
| 42 46 |  | 
| 43 47 | 
             
                    pattern.each do |matcher|
         | 
| 44 | 
            -
                      match = matcher.match(token_stream, position + matches.length)
         | 
| 45 | 
            -
                      matched &&=  | 
| 48 | 
            +
                      _matched, match = matcher.match(token_stream, position + matches.length)
         | 
| 49 | 
            +
                      matched &&= _matched
         | 
| 46 50 | 
             
                      matches += match
         | 
| 47 51 | 
             
                    end
         | 
| 48 52 |  | 
| @@ -54,19 +58,23 @@ module Dentaku | |
| 54 58 | 
             
                end
         | 
| 55 59 |  | 
| 56 60 | 
             
                def evaluate_step(token_stream, start, length, evaluator)
         | 
| 57 | 
            -
                   | 
| 61 | 
            +
                  substream = token_stream.slice!(start, length)
         | 
| 58 62 |  | 
| 59 63 | 
             
                  if self.respond_to?(evaluator)
         | 
| 60 | 
            -
                    token_stream.insert start, *self.send(evaluator, * | 
| 64 | 
            +
                    token_stream.insert start, *self.send(evaluator, *substream)
         | 
| 61 65 | 
             
                  else
         | 
| 62 | 
            -
                     | 
| 63 | 
            -
                     | 
| 66 | 
            +
                    result = user_defined_function(evaluator, substream)
         | 
| 67 | 
            +
                    token_stream.insert start, result
         | 
| 68 | 
            +
                  end
         | 
| 69 | 
            +
                end
         | 
| 64 70 |  | 
| 65 | 
            -
             | 
| 66 | 
            -
             | 
| 71 | 
            +
                def user_defined_function(evaluator, tokens)
         | 
| 72 | 
            +
                  function = Rules.func(evaluator)
         | 
| 73 | 
            +
                  raise "unknown function '#{ evaluator }'" unless function
         | 
| 67 74 |  | 
| 68 | 
            -
             | 
| 69 | 
            -
                   | 
| 75 | 
            +
                  arguments = extract_arguments_from_function_call(tokens).map { |t| t.value }
         | 
| 76 | 
            +
                  return_value = function.body.call(*arguments)
         | 
| 77 | 
            +
                  Token.new(function.type, return_value)
         | 
| 70 78 | 
             
                end
         | 
| 71 79 |  | 
| 72 80 | 
             
                def extract_arguments_from_function_call(tokens)
         | 
| @@ -0,0 +1,55 @@ | |
| 1 | 
            +
            require 'dentaku/tokenizer'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Dentaku
         | 
| 4 | 
            +
              class Expression
         | 
| 5 | 
            +
                attr_reader :tokens
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                def initialize(string, variables={})
         | 
| 8 | 
            +
                  @raw = string
         | 
| 9 | 
            +
                  @tokenizer ||= Tokenizer.new
         | 
| 10 | 
            +
                  @tokens = @tokenizer.tokenize(@raw)
         | 
| 11 | 
            +
                  replace_identifiers_with_values(variables)
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                def identifiers
         | 
| 15 | 
            +
                  @tokens.select { |t| t.category == :identifier }.map { |t| t.value }
         | 
| 16 | 
            +
                end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                def unbound?
         | 
| 19 | 
            +
                  identifiers.any?
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                private
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                def replace_identifiers_with_values(variables)
         | 
| 25 | 
            +
                  @tokens.map! do |token|
         | 
| 26 | 
            +
                    if token.is?(:identifier)
         | 
| 27 | 
            +
                      replace_identifier_with_value(token, variables)
         | 
| 28 | 
            +
                    else
         | 
| 29 | 
            +
                      token
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                def replace_identifier_with_value(token, variables)
         | 
| 35 | 
            +
                  key = token.value.to_sym
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  if variables.key? key
         | 
| 38 | 
            +
                    value = variables[key]
         | 
| 39 | 
            +
                    type  = type_for_value(value)
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                    Token.new(type, value)
         | 
| 42 | 
            +
                  else
         | 
| 43 | 
            +
                    token
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                def type_for_value(value)
         | 
| 48 | 
            +
                  case value
         | 
| 49 | 
            +
                  when String then :string
         | 
| 50 | 
            +
                  when TrueClass, FalseClass then :logical
         | 
| 51 | 
            +
                  else :numeric
         | 
| 52 | 
            +
                  end
         | 
| 53 | 
            +
                end
         | 
| 54 | 
            +
              end
         | 
| 55 | 
            +
            end
         | 
    
        data/lib/dentaku/token.rb
    CHANGED
    
    
| @@ -7,8 +7,12 @@ module Dentaku | |
| 7 7 | 
             
                  @values     = [values].compact.flatten
         | 
| 8 8 | 
             
                  @invert     = false
         | 
| 9 9 |  | 
| 10 | 
            +
                  @categories_hash = Hash[@categories.map { |cat| [cat, 1] }]
         | 
| 11 | 
            +
                  @values_hash = Hash[@values.map { |value| [value, 1] }]
         | 
| 12 | 
            +
             | 
| 10 13 | 
             
                  @min = 1
         | 
| 11 14 | 
             
                  @max = 1
         | 
| 15 | 
            +
                  @range = (@min..@max)
         | 
| 12 16 | 
             
                end
         | 
| 13 17 |  | 
| 14 18 | 
             
                def invert
         | 
| @@ -23,39 +27,40 @@ module Dentaku | |
| 23 27 |  | 
| 24 28 | 
             
                def match(token_stream, offset=0)
         | 
| 25 29 | 
             
                  matched_tokens = []
         | 
| 30 | 
            +
                  matched = false
         | 
| 26 31 |  | 
| 27 32 | 
             
                  while self == token_stream[matched_tokens.length + offset] && matched_tokens.length < @max
         | 
| 28 33 | 
             
                    matched_tokens << token_stream[matched_tokens.length + offset]
         | 
| 29 34 | 
             
                  end
         | 
| 30 35 |  | 
| 31 | 
            -
                  if  | 
| 32 | 
            -
                     | 
| 33 | 
            -
                  else
         | 
| 34 | 
            -
                    def matched_tokens.matched?() false end
         | 
| 36 | 
            +
                  if @range.cover?(matched_tokens.length)
         | 
| 37 | 
            +
                    matched = true
         | 
| 35 38 | 
             
                  end
         | 
| 36 39 |  | 
| 37 | 
            -
                  matched_tokens
         | 
| 40 | 
            +
                  [matched, matched_tokens]
         | 
| 38 41 | 
             
                end
         | 
| 39 42 |  | 
| 40 43 | 
             
                def star
         | 
| 41 44 | 
             
                  @min = 0
         | 
| 42 45 | 
             
                  @max = Float::INFINITY
         | 
| 46 | 
            +
                  @range = (@min..@max)
         | 
| 43 47 | 
             
                  self
         | 
| 44 48 | 
             
                end
         | 
| 45 49 |  | 
| 46 50 | 
             
                def plus
         | 
| 47 51 | 
             
                  @max = Float::INFINITY
         | 
| 52 | 
            +
                  @range = (@min..@max)
         | 
| 48 53 | 
             
                  self
         | 
| 49 54 | 
             
                end
         | 
| 50 55 |  | 
| 51 56 | 
             
                private
         | 
| 52 57 |  | 
| 53 58 | 
             
                def category_match(category)
         | 
| 54 | 
            -
                  @ | 
| 59 | 
            +
                  @categories_hash.empty? || @categories_hash.key?(category)
         | 
| 55 60 | 
             
                end
         | 
| 56 61 |  | 
| 57 62 | 
             
                def value_match(value)
         | 
| 58 | 
            -
                  @values.empty? || @ | 
| 63 | 
            +
                  @values.empty? || @values_hash.key?(value)
         | 
| 59 64 | 
             
                end
         | 
| 60 65 |  | 
| 61 66 | 
             
                def self.numeric;        new(:numeric);                        end
         | 
| @@ -92,5 +97,4 @@ module Dentaku | |
| 92 97 | 
             
                end
         | 
| 93 98 |  | 
| 94 99 | 
             
              end
         | 
| 95 | 
            -
            end
         | 
| 96 | 
            -
             | 
| 100 | 
            +
            end
         | 
| @@ -40,7 +40,7 @@ module Dentaku | |
| 40 40 | 
             
                  end
         | 
| 41 41 |  | 
| 42 42 | 
             
                  def numeric
         | 
| 43 | 
            -
                    new(:numeric, '(\d+(\.\d+)?|\.\d+)\b', lambda { |raw| raw =~ /\./ ? raw | 
| 43 | 
            +
                    new(:numeric, '(\d+(\.\d+)?|\.\d+)\b', lambda { |raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
         | 
| 44 44 | 
             
                  end
         | 
| 45 45 |  | 
| 46 46 | 
             
                  def double_quoted_string
         | 
| @@ -63,8 +63,8 @@ module Dentaku | |
| 63 63 |  | 
| 64 64 | 
             
                  def comparator
         | 
| 65 65 | 
             
                    names = { le: '<=', ge: '>=', ne: '!=', lt: '<', gt: '>', eq: '=' }.invert
         | 
| 66 | 
            -
                    alternate = { ne: '<>' }.invert
         | 
| 67 | 
            -
                    new(:comparator, ' | 
| 66 | 
            +
                    alternate = { ne: '<>', eq: '==' }.invert
         | 
| 67 | 
            +
                    new(:comparator, '<=|>=|!=|<>|<|>|==|=', lambda { |raw| names[raw] || alternate[raw] })
         | 
| 68 68 | 
             
                  end
         | 
| 69 69 |  | 
| 70 70 | 
             
                  def combinator
         | 
    
        data/lib/dentaku/version.rb
    CHANGED
    
    
| @@ -5,41 +5,41 @@ describe Dentaku::BinaryOperation do | |
| 5 5 | 
             
              let(:logical)   { described_class.new(true, false) }
         | 
| 6 6 |  | 
| 7 7 | 
             
              it 'raises a number to a power' do
         | 
| 8 | 
            -
                operation.pow. | 
| 8 | 
            +
                expect(operation.pow).to eq [:numeric, 8]
         | 
| 9 9 | 
             
              end
         | 
| 10 10 |  | 
| 11 11 | 
             
              it 'adds two numbers' do
         | 
| 12 | 
            -
                operation.add. | 
| 12 | 
            +
                expect(operation.add).to eq [:numeric, 5]
         | 
| 13 13 | 
             
              end
         | 
| 14 14 |  | 
| 15 15 | 
             
              it 'subtracts two numbers' do
         | 
| 16 | 
            -
                operation.subtract. | 
| 16 | 
            +
                expect(operation.subtract).to eq [:numeric, -1]
         | 
| 17 17 | 
             
              end
         | 
| 18 18 |  | 
| 19 19 | 
             
              it 'multiplies two numbers' do
         | 
| 20 | 
            -
                operation.multiply. | 
| 20 | 
            +
                expect(operation.multiply).to eq [:numeric, 6]
         | 
| 21 21 | 
             
              end
         | 
| 22 22 |  | 
| 23 23 | 
             
              it 'divides two numbers' do
         | 
| 24 | 
            -
                operation.divide. | 
| 24 | 
            +
                expect(operation.divide).to eq [:numeric, (BigDecimal.new('2.0')/BigDecimal.new('3.0'))]
         | 
| 25 25 | 
             
              end
         | 
| 26 26 |  | 
| 27 27 | 
             
              it 'compares two numbers' do
         | 
| 28 | 
            -
                operation.le. | 
| 29 | 
            -
                operation.lt. | 
| 30 | 
            -
                operation.ne. | 
| 28 | 
            +
                expect(operation.le).to eq [:logical, true]
         | 
| 29 | 
            +
                expect(operation.lt).to eq [:logical, true]
         | 
| 30 | 
            +
                expect(operation.ne).to eq [:logical, true]
         | 
| 31 31 |  | 
| 32 | 
            -
                operation.ge. | 
| 33 | 
            -
                operation.gt. | 
| 34 | 
            -
                operation.eq. | 
| 32 | 
            +
                expect(operation.ge).to eq [:logical, false]
         | 
| 33 | 
            +
                expect(operation.gt).to eq [:logical, false]
         | 
| 34 | 
            +
                expect(operation.eq).to eq [:logical, false]
         | 
| 35 35 | 
             
              end
         | 
| 36 36 |  | 
| 37 37 | 
             
              it 'performs logical AND and OR' do
         | 
| 38 | 
            -
                logical.and. | 
| 39 | 
            -
                logical.or. | 
| 38 | 
            +
                expect(logical.and).to eq [:logical, false]
         | 
| 39 | 
            +
                expect(logical.or).to  eq [:logical, true]
         | 
| 40 40 | 
             
              end
         | 
| 41 41 |  | 
| 42 42 | 
             
              it 'mods two numbers' do
         | 
| 43 | 
            -
                operation.mod. | 
| 43 | 
            +
                expect(operation.mod).to eq [:numeric, 2%3]
         | 
| 44 44 | 
             
              end
         | 
| 45 45 | 
             
            end
         | 
    
        data/spec/calculator_spec.rb
    CHANGED
    
    | @@ -5,102 +5,102 @@ describe Dentaku::Calculator do | |
| 5 5 | 
             
              let(:with_memory) { described_class.new.store(:apples => 3) }
         | 
| 6 6 |  | 
| 7 7 | 
             
              it 'evaluates an expression' do
         | 
| 8 | 
            -
                calculator.evaluate('7+3'). | 
| 8 | 
            +
                expect(calculator.evaluate('7+3')).to eq(10)
         | 
| 9 9 | 
             
              end
         | 
| 10 10 |  | 
| 11 11 | 
             
              describe 'memory' do
         | 
| 12 | 
            -
                it { calculator. | 
| 13 | 
            -
                it { with_memory. | 
| 14 | 
            -
                it { with_memory.clear. | 
| 12 | 
            +
                it { expect(calculator).to be_empty }
         | 
| 13 | 
            +
                it { expect(with_memory).not_to be_empty   }
         | 
| 14 | 
            +
                it { expect(with_memory.clear).to be_empty }
         | 
| 15 15 |  | 
| 16 | 
            -
                it  | 
| 17 | 
            -
             | 
| 18 | 
            -
             | 
| 19 | 
            -
                it { calculator.store(:apples, 3).memory('apples').should eq(3) }
         | 
| 20 | 
            -
                it { calculator.store('apples', 3).memory(:apples).should eq(3) }
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                it 'should discard local values' do
         | 
| 23 | 
            -
                  calculator.evaluate('pears * 2', :pears => 5).should eq(10)
         | 
| 24 | 
            -
                  calculator.should be_empty
         | 
| 25 | 
            -
                  lambda { calculator.tokenize('pears * 2') }.should raise_error
         | 
| 16 | 
            +
                it 'discards local values' do
         | 
| 17 | 
            +
                  expect(calculator.evaluate('pears * 2', :pears => 5)).to eq(10)
         | 
| 18 | 
            +
                  expect(calculator).to be_empty
         | 
| 26 19 | 
             
                end
         | 
| 27 20 | 
             
              end
         | 
| 28 21 |  | 
| 29 | 
            -
              it ' | 
| 30 | 
            -
                calculator.evaluate('5+3'). | 
| 31 | 
            -
                calculator.evaluate('(1+1+1)/3*100'). | 
| 22 | 
            +
              it 'evaluates a statement with no variables' do
         | 
| 23 | 
            +
                expect(calculator.evaluate('5+3')).to eq(8)
         | 
| 24 | 
            +
                expect(calculator.evaluate('(1+1+1)/3*100')).to eq(100)
         | 
| 32 25 | 
             
              end
         | 
| 33 26 |  | 
| 34 | 
            -
              it ' | 
| 35 | 
            -
                 | 
| 27 | 
            +
              it 'fails to evaluate unbound statements' do
         | 
| 28 | 
            +
                unbound = 'foo * 1.5'
         | 
| 29 | 
            +
                expect { calculator.evaluate!(unbound) }.to raise_error(Dentaku::UnboundVariableError)
         | 
| 30 | 
            +
                expect { calculator.evaluate!(unbound) }.to raise_error do |error|
         | 
| 31 | 
            +
                  expect(error.unbound_variables).to eq [:foo]
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
                expect(calculator.evaluate(unbound)).to be_nil
         | 
| 34 | 
            +
                expect(calculator.evaluate(unbound) { :bar }).to eq :bar
         | 
| 35 | 
            +
                expect(calculator.evaluate(unbound) { |e| e }).to eq unbound
         | 
| 36 36 | 
             
              end
         | 
| 37 37 |  | 
| 38 | 
            -
              it ' | 
| 39 | 
            -
                calculator.evaluate('foo * 1.5', :foo => 2). | 
| 40 | 
            -
                calculator.bind(:monkeys => 3).evaluate('monkeys < 7'). | 
| 41 | 
            -
                calculator.evaluate('monkeys / 1.5'). | 
| 38 | 
            +
              it 'evaluates unbound statements given a binding in memory' do
         | 
| 39 | 
            +
                expect(calculator.evaluate('foo * 1.5', :foo => 2)).to eq(3)
         | 
| 40 | 
            +
                expect(calculator.bind(:monkeys => 3).evaluate('monkeys < 7')).to be_truthy
         | 
| 41 | 
            +
                expect(calculator.evaluate('monkeys / 1.5')).to eq(2)
         | 
| 42 42 | 
             
              end
         | 
| 43 43 |  | 
| 44 | 
            -
              it ' | 
| 45 | 
            -
                calculator.evaluate('foo * 2', :foo => 2). | 
| 46 | 
            -
                calculator.evaluate('foo * 2', :foo => 4). | 
| 44 | 
            +
              it 'rebinds for each evaluation' do
         | 
| 45 | 
            +
                expect(calculator.evaluate('foo * 2', :foo => 2)).to eq(4)
         | 
| 46 | 
            +
                expect(calculator.evaluate('foo * 2', :foo => 4)).to eq(8)
         | 
| 47 47 | 
             
              end
         | 
| 48 48 |  | 
| 49 | 
            -
              it ' | 
| 50 | 
            -
                calculator.evaluate('foo * 2', :foo => 2). | 
| 51 | 
            -
                calculator.evaluate('foo * 2', 'foo' => 4). | 
| 49 | 
            +
              it 'accepts strings or symbols for binding keys' do
         | 
| 50 | 
            +
                expect(calculator.evaluate('foo * 2', :foo => 2)).to eq(4)
         | 
| 51 | 
            +
                expect(calculator.evaluate('foo * 2', 'foo' => 4)).to eq(8)
         | 
| 52 52 | 
             
              end
         | 
| 53 53 |  | 
| 54 | 
            -
              it ' | 
| 55 | 
            -
                calculator.evaluate('foo1 * 2', :foo1 => 2). | 
| 56 | 
            -
                calculator.evaluate('foo1 * 2', 'foo1' => 4). | 
| 57 | 
            -
                calculator.evaluate('1foo * 2', '1foo' => 2). | 
| 58 | 
            -
                calculator.evaluate('fo1o * 2', :fo1o => 4). | 
| 54 | 
            +
              it 'accepts digits in identifiers' do
         | 
| 55 | 
            +
                expect(calculator.evaluate('foo1 * 2', :foo1 => 2)).to eq(4)
         | 
| 56 | 
            +
                expect(calculator.evaluate('foo1 * 2', 'foo1' => 4)).to eq(8)
         | 
| 57 | 
            +
                expect(calculator.evaluate('1foo * 2', '1foo' => 2)).to eq(4)
         | 
| 58 | 
            +
                expect(calculator.evaluate('fo1o * 2', :fo1o => 4)).to eq(8)
         | 
| 59 59 | 
             
              end
         | 
| 60 60 |  | 
| 61 | 
            -
              it ' | 
| 62 | 
            -
                calculator.evaluate('fruit = "apple"', :fruit => 'apple'). | 
| 63 | 
            -
                calculator.evaluate('fruit = "apple"', :fruit => 'pear'). | 
| 61 | 
            +
              it 'compares string literals with string variables' do
         | 
| 62 | 
            +
                expect(calculator.evaluate('fruit = "apple"', :fruit => 'apple')).to be_truthy
         | 
| 63 | 
            +
                expect(calculator.evaluate('fruit = "apple"', :fruit => 'pear')).to be_falsey
         | 
| 64 64 | 
             
              end
         | 
| 65 65 |  | 
| 66 | 
            -
              it ' | 
| 67 | 
            -
                calculator.evaluate('fruit = "Apple"', :fruit => 'apple'). | 
| 68 | 
            -
                calculator.evaluate('fruit = "Apple"', :fruit => 'Apple'). | 
| 66 | 
            +
              it 'performs case-sensitive comparison' do
         | 
| 67 | 
            +
                expect(calculator.evaluate('fruit = "Apple"', :fruit => 'apple')).to be_falsey
         | 
| 68 | 
            +
                expect(calculator.evaluate('fruit = "Apple"', :fruit => 'Apple')).to be_truthy
         | 
| 69 69 | 
             
              end
         | 
| 70 70 |  | 
| 71 | 
            -
              it ' | 
| 72 | 
            -
                calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => true). | 
| 73 | 
            -
                calculator.evaluate('some_boolean AND 7 < 5', :some_boolean => true). | 
| 74 | 
            -
                calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => false). | 
| 71 | 
            +
              it 'allows binding logical values' do
         | 
| 72 | 
            +
                expect(calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => true)).to be_truthy
         | 
| 73 | 
            +
                expect(calculator.evaluate('some_boolean AND 7 < 5', :some_boolean => true)).to be_falsey
         | 
| 74 | 
            +
                expect(calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => false)).to be_falsey
         | 
| 75 75 |  | 
| 76 | 
            -
                calculator.evaluate('some_boolean OR 7 > 5', :some_boolean => true). | 
| 77 | 
            -
                calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => true). | 
| 78 | 
            -
                calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => false). | 
| 76 | 
            +
                expect(calculator.evaluate('some_boolean OR 7 > 5', :some_boolean => true)).to be_truthy
         | 
| 77 | 
            +
                expect(calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => true)).to be_truthy
         | 
| 78 | 
            +
                expect(calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => false)).to be_falsey
         | 
| 79 79 |  | 
| 80 80 | 
             
              end
         | 
| 81 81 |  | 
| 82 82 | 
             
              describe 'functions' do
         | 
| 83 | 
            -
                it ' | 
| 84 | 
            -
                  calculator.evaluate('if(foo < 8, 10, 20)', :foo => 2). | 
| 85 | 
            -
                  calculator.evaluate('if(foo < 8, 10, 20)', :foo => 9). | 
| 86 | 
            -
                  calculator.evaluate('if (foo < 8, 10, 20)', :foo => 2). | 
| 87 | 
            -
                  calculator.evaluate('if (foo < 8, 10, 20)', :foo => 9). | 
| 83 | 
            +
                it 'include IF' do
         | 
| 84 | 
            +
                  expect(calculator.evaluate('if(foo < 8, 10, 20)', :foo => 2)).to eq(10)
         | 
| 85 | 
            +
                  expect(calculator.evaluate('if(foo < 8, 10, 20)', :foo => 9)).to eq(20)
         | 
| 86 | 
            +
                  expect(calculator.evaluate('if (foo < 8, 10, 20)', :foo => 2)).to eq(10)
         | 
| 87 | 
            +
                  expect(calculator.evaluate('if (foo < 8, 10, 20)', :foo => 9)).to eq(20)
         | 
| 88 88 | 
             
                end
         | 
| 89 89 |  | 
| 90 | 
            -
                it ' | 
| 91 | 
            -
                  calculator.evaluate('round(8.2)'). | 
| 92 | 
            -
                  calculator.evaluate('round(8.8)'). | 
| 93 | 
            -
                  calculator.evaluate('round(8.75, 1)'). | 
| 90 | 
            +
                it 'include ROUND' do
         | 
| 91 | 
            +
                  expect(calculator.evaluate('round(8.2)')).to eq(8)
         | 
| 92 | 
            +
                  expect(calculator.evaluate('round(8.8)')).to eq(9)
         | 
| 93 | 
            +
                  expect(calculator.evaluate('round(8.75, 1)')).to eq(BigDecimal.new('8.8'))
         | 
| 94 94 |  | 
| 95 | 
            -
                  calculator.evaluate('ROUND(apples * 0.93)', { :apples => 10 }). | 
| 95 | 
            +
                  expect(calculator.evaluate('ROUND(apples * 0.93)', { :apples => 10 })).to eq(9)
         | 
| 96 96 | 
             
                end
         | 
| 97 97 |  | 
| 98 | 
            -
                it ' | 
| 99 | 
            -
                  calculator.evaluate('NOT(some_boolean)', :some_boolean => true). | 
| 100 | 
            -
                  calculator.evaluate('NOT(some_boolean)', :some_boolean => false). | 
| 98 | 
            +
                it 'include NOT' do
         | 
| 99 | 
            +
                  expect(calculator.evaluate('NOT(some_boolean)', :some_boolean => true)).to be_falsey
         | 
| 100 | 
            +
                  expect(calculator.evaluate('NOT(some_boolean)', :some_boolean => false)).to be_truthy
         | 
| 101 101 |  | 
| 102 | 
            -
                  calculator.evaluate('NOT(some_boolean) AND 7 > 5', :some_boolean => true). | 
| 103 | 
            -
                  calculator.evaluate('NOT(some_boolean) OR 7 < 5', :some_boolean => false). | 
| 102 | 
            +
                  expect(calculator.evaluate('NOT(some_boolean) AND 7 > 5', :some_boolean => true)).to be_falsey
         | 
| 103 | 
            +
                  expect(calculator.evaluate('NOT(some_boolean) OR 7 < 5', :some_boolean => false)).to be_truthy
         | 
| 104 104 | 
             
                end
         | 
| 105 105 | 
             
              end
         | 
| 106 106 | 
             
            end
         |