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
|