dentaku 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|