dentaku 1.2.4 → 1.2.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +3 -0
- data/lib/dentaku/calculator.rb +6 -5
- data/lib/dentaku/dependency_resolver.rb +1 -1
- data/lib/dentaku/evaluator.rb +10 -3
- data/lib/dentaku/rule_set.rb +153 -0
- data/lib/dentaku/token_matcher.rb +1 -1
- data/lib/dentaku/version.rb +1 -1
- data/spec/benchmark.rb +71 -0
- data/spec/calculator_spec.rb +37 -31
- data/spec/dentaku_spec.rb +1 -1
- data/spec/evaluator_spec.rb +3 -2
- data/spec/rule_set_spec.rb +43 -0
- metadata +9 -4
- data/lib/dentaku/rules.rb +0 -115
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cca185ca8ad75d5c0c0cd13241ad8356a6d046f0
|
4
|
+
data.tar.gz: 469a8af2de92efdb3b75297ec7de268e6b066492
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b0348e2c7b06c318ab67f88bfddc5f6c6e5ac44efdaeacd53d35a23764f3d961530524c7929673ce3737375f0b93671dedadccb7ab5d0d363f4916b218ce0b30
|
7
|
+
data.tar.gz: 36043cafe22a9f2dd60f926a13832dc3175e3f7000c6eac081925d234501f287c6289adeed51b45334d9c7353a4a29a5458b7414cd612ecbb05039e852590914
|
data/.travis.yml
CHANGED
data/lib/dentaku/calculator.rb
CHANGED
@@ -1,25 +1,26 @@
|
|
1
1
|
require 'dentaku/evaluator'
|
2
2
|
require 'dentaku/exceptions'
|
3
3
|
require 'dentaku/expression'
|
4
|
-
require 'dentaku/
|
4
|
+
require 'dentaku/rule_set'
|
5
5
|
require 'dentaku/token'
|
6
6
|
require 'dentaku/dependency_resolver'
|
7
7
|
|
8
8
|
module Dentaku
|
9
9
|
class Calculator
|
10
|
-
attr_reader :result
|
10
|
+
attr_reader :result, :rule_set
|
11
11
|
|
12
12
|
def initialize
|
13
13
|
clear
|
14
|
+
@rule_set = RuleSet.new
|
14
15
|
end
|
15
16
|
|
16
17
|
def add_function(fn)
|
17
|
-
|
18
|
+
rule_set.add_function(fn)
|
18
19
|
self
|
19
20
|
end
|
20
21
|
|
21
22
|
def add_functions(fns)
|
22
|
-
fns.each { |fn|
|
23
|
+
fns.each { |fn| add_function(fn) }
|
23
24
|
self
|
24
25
|
end
|
25
26
|
|
@@ -33,7 +34,7 @@ module Dentaku
|
|
33
34
|
store(data) do
|
34
35
|
expr = Expression.new(expression, @memory)
|
35
36
|
raise UnboundVariableError.new(expr.identifiers) if expr.unbound?
|
36
|
-
@evaluator ||= Evaluator.new
|
37
|
+
@evaluator ||= Evaluator.new(rule_set)
|
37
38
|
@result = @evaluator.evaluate(expr.tokens)
|
38
39
|
end
|
39
40
|
end
|
data/lib/dentaku/evaluator.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
|
-
require 'dentaku/
|
1
|
+
require 'dentaku/rule_set'
|
2
2
|
require 'dentaku/binary_operation'
|
3
3
|
|
4
4
|
module Dentaku
|
5
5
|
class Evaluator
|
6
|
+
attr_reader :rule_set
|
7
|
+
|
8
|
+
def initialize(rule_set)
|
9
|
+
@rule_set = rule_set
|
10
|
+
end
|
11
|
+
|
6
12
|
def evaluate(tokens)
|
7
13
|
evaluate_token_stream(tokens).value
|
8
14
|
end
|
@@ -24,7 +30,8 @@ module Dentaku
|
|
24
30
|
|
25
31
|
def match_rule_pattern(tokens)
|
26
32
|
matched = false
|
27
|
-
|
33
|
+
|
34
|
+
rule_set.filter(tokens).each do |pattern, evaluator|
|
28
35
|
pos, match = find_rule_match(pattern, tokens)
|
29
36
|
|
30
37
|
if pos
|
@@ -71,7 +78,7 @@ module Dentaku
|
|
71
78
|
end
|
72
79
|
|
73
80
|
def user_defined_function(evaluator, tokens)
|
74
|
-
function =
|
81
|
+
function = rule_set.function(evaluator)
|
75
82
|
raise "unknown function '#{ evaluator }'" unless function
|
76
83
|
|
77
84
|
arguments = extract_arguments_from_function_call(tokens).map { |t| t.value }
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'dentaku/external_function'
|
2
|
+
|
3
|
+
module Dentaku
|
4
|
+
class RuleSet
|
5
|
+
def initialize
|
6
|
+
self.custom_rules = []
|
7
|
+
self.custom_functions = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def rules
|
11
|
+
custom_rules + core_rules
|
12
|
+
end
|
13
|
+
|
14
|
+
def each
|
15
|
+
rules.each { |r| yield r }
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_function(function)
|
19
|
+
fn = ExternalFunction.new(function[:name], function[:type], function[:signature], function[:body])
|
20
|
+
|
21
|
+
custom_rules.push [
|
22
|
+
function_token_matchers(fn.name, *fn.tokens),
|
23
|
+
fn.name
|
24
|
+
]
|
25
|
+
|
26
|
+
custom_functions[fn.name] = fn
|
27
|
+
clear_cache
|
28
|
+
end
|
29
|
+
|
30
|
+
def filter(tokens)
|
31
|
+
categories = tokens.map(&:category).uniq
|
32
|
+
values = tokens.map { |token| token.value.is_a?(Numeric) ? 0 : token.value }
|
33
|
+
.reject { |token| [:fopen, :close].include?(token) }
|
34
|
+
select(categories, values)
|
35
|
+
end
|
36
|
+
|
37
|
+
def select(categories, values)
|
38
|
+
@cache ||= {}
|
39
|
+
return @cache[categories + values] if @cache.has_key?(categories + values)
|
40
|
+
|
41
|
+
@cache[categories + values] = rules.select do |pattern, _|
|
42
|
+
categories_intersection = matcher_categories[pattern] & categories
|
43
|
+
values_intersection = matcher_values[pattern] & values
|
44
|
+
categories_intersection.length > 0 && (values_intersection.length > 0 || matcher_values[pattern].empty?)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def function(name)
|
49
|
+
custom_functions.fetch(name)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
attr_accessor :custom_rules, :custom_functions
|
54
|
+
|
55
|
+
def matcher_categories
|
56
|
+
@matcher_categories ||= rules.each_with_object({}) do |(pattern, _), h|
|
57
|
+
h[pattern] = pattern.map(&:categories).reduce { |a,b| a.merge(b) }.keys
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def matcher_values
|
62
|
+
@matcher_values ||= rules.each_with_object({}) do |(pattern, _), h|
|
63
|
+
h[pattern] = pattern.map(&:values).reduce { |a,b| a.merge(b) }.keys
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def clear_cache
|
68
|
+
@cache = nil
|
69
|
+
@matcher_categories = nil
|
70
|
+
@matcher_values = nil
|
71
|
+
end
|
72
|
+
|
73
|
+
def core_rules
|
74
|
+
@core_rules ||= [
|
75
|
+
[ pattern(:if), :if ],
|
76
|
+
[ pattern(:round), :round ],
|
77
|
+
[ pattern(:roundup), :round_int ],
|
78
|
+
[ pattern(:rounddown), :round_int ],
|
79
|
+
[ pattern(:not), :not ],
|
80
|
+
|
81
|
+
[ pattern(:group), :evaluate_group ],
|
82
|
+
[ pattern(:start_neg), :negate ],
|
83
|
+
[ pattern(:math_pow), :apply ],
|
84
|
+
[ pattern(:math_neg_pow), :pow_negate ],
|
85
|
+
[ pattern(:math_mod), :apply ],
|
86
|
+
[ pattern(:math_mul), :apply ],
|
87
|
+
[ pattern(:math_neg_mul), :mul_negate ],
|
88
|
+
[ pattern(:math_add), :apply ],
|
89
|
+
[ pattern(:percentage), :percentage ],
|
90
|
+
[ pattern(:negation), :negate ],
|
91
|
+
[ pattern(:range_asc), :expand_range ],
|
92
|
+
[ pattern(:range_desc), :expand_range ],
|
93
|
+
[ pattern(:num_comp), :apply ],
|
94
|
+
[ pattern(:str_comp), :apply ],
|
95
|
+
[ pattern(:combine), :apply ]
|
96
|
+
]
|
97
|
+
end
|
98
|
+
|
99
|
+
def pattern(name)
|
100
|
+
@patterns ||= {
|
101
|
+
group: token_matchers(:open, :non_group_star, :close),
|
102
|
+
math_add: token_matchers(:numeric, :addsub, :numeric),
|
103
|
+
math_mul: token_matchers(:numeric, :muldiv, :numeric),
|
104
|
+
math_neg_mul: token_matchers(:numeric, :muldiv, :subtract, :numeric),
|
105
|
+
math_pow: token_matchers(:numeric, :pow, :numeric),
|
106
|
+
math_neg_pow: token_matchers(:numeric, :pow, :subtract, :numeric),
|
107
|
+
math_mod: token_matchers(:numeric, :mod, :numeric),
|
108
|
+
negation: token_matchers(:subtract, :numeric),
|
109
|
+
start_neg: token_matchers(:anchored_minus, :numeric),
|
110
|
+
percentage: token_matchers(:numeric, :mod),
|
111
|
+
range_asc: token_matchers(:numeric, :comp_lt, :numeric, :comp_lt, :numeric),
|
112
|
+
range_desc: token_matchers(:numeric, :comp_gt, :numeric, :comp_gt, :numeric),
|
113
|
+
num_comp: token_matchers(:numeric, :comparator, :numeric),
|
114
|
+
str_comp: token_matchers(:string, :comparator, :string),
|
115
|
+
combine: token_matchers(:logical, :combinator, :logical),
|
116
|
+
|
117
|
+
if: function_token_matchers(:if, :non_group, :comma, :non_group, :comma, :non_group),
|
118
|
+
round: function_token_matchers(:round, :arguments),
|
119
|
+
roundup: function_token_matchers(:roundup, :arguments),
|
120
|
+
rounddown: function_token_matchers(:rounddown, :arguments),
|
121
|
+
not: function_token_matchers(:not, :arguments)
|
122
|
+
}
|
123
|
+
|
124
|
+
@patterns[name]
|
125
|
+
end
|
126
|
+
|
127
|
+
def token_matchers(*symbols)
|
128
|
+
symbols.map { |s| matcher(s) }
|
129
|
+
end
|
130
|
+
|
131
|
+
def function_token_matchers(function_name, *symbols)
|
132
|
+
token_matchers(:fopen, *symbols, :close).unshift(
|
133
|
+
TokenMatcher.send(function_name)
|
134
|
+
)
|
135
|
+
end
|
136
|
+
|
137
|
+
def matcher(symbol)
|
138
|
+
@matchers ||= [
|
139
|
+
:numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
|
140
|
+
:comparator, :comp_gt, :comp_lt, :fopen, :open, :close, :comma,
|
141
|
+
:non_close_plus, :non_group, :non_group_star, :arguments,
|
142
|
+
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
|
143
|
+
:anchored_minus, :math_neg_pow, :math_neg_mul
|
144
|
+
].each_with_object({}) do |name, matchers|
|
145
|
+
matchers[name] = TokenMatcher.send(name)
|
146
|
+
end
|
147
|
+
|
148
|
+
@matchers.fetch(symbol) do
|
149
|
+
raise "Unknown token symbol #{ symbol }"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -2,7 +2,7 @@ require 'dentaku/token'
|
|
2
2
|
|
3
3
|
module Dentaku
|
4
4
|
class TokenMatcher
|
5
|
-
attr_reader :children
|
5
|
+
attr_reader :children, :categories, :values
|
6
6
|
|
7
7
|
def initialize(categories=nil, values=nil, children=[])
|
8
8
|
# store categories and values as hash to optimize key lookup, h/t @jan-mangs
|
data/lib/dentaku/version.rb
CHANGED
data/spec/benchmark.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'dentaku'
|
4
|
+
require 'allocation_stats'
|
5
|
+
require 'benchmark'
|
6
|
+
|
7
|
+
puts "Dentaku version #{Dentaku::VERSION}"
|
8
|
+
puts "Ruby version #{RUBY_VERSION}"
|
9
|
+
|
10
|
+
with_duplicate_variables = [
|
11
|
+
"R1+R2+R3+R4+R5+R6",
|
12
|
+
{"R1"=>100000, "R2"=>0, "R3"=>200000, "R4"=>0, "R5"=>500000, "R6"=>0, "r1"=>100000, "r2"=>0, "r3"=>200000, "r4"=>0, "r5"=>500000, "r6"=>0}
|
13
|
+
]
|
14
|
+
|
15
|
+
without_duplicate_variables = [
|
16
|
+
"R1+R2+R3+R4+R5+R6",
|
17
|
+
{"R1"=>100000, "R2"=>0, "R3"=>200000, "R4"=>0, "R5"=>500000, "R6"=>0}
|
18
|
+
]
|
19
|
+
|
20
|
+
def test(args, custom_function: true)
|
21
|
+
calls = [ args ] * 100
|
22
|
+
|
23
|
+
10.times do |i|
|
24
|
+
|
25
|
+
stats = nil
|
26
|
+
bm = Benchmark.measure do
|
27
|
+
stats = AllocationStats.trace do
|
28
|
+
|
29
|
+
calls.each do |formule, bound|
|
30
|
+
|
31
|
+
calculator = Dentaku::Calculator.new
|
32
|
+
|
33
|
+
if custom_function
|
34
|
+
calculator.add_function(
|
35
|
+
name: :sum,
|
36
|
+
type: :numeric,
|
37
|
+
signature: [:arguments],
|
38
|
+
body: ->(numbers) { numbers.inject(:+) },
|
39
|
+
)
|
40
|
+
end
|
41
|
+
|
42
|
+
calculator.evaluate(formule, bound)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
puts " run #{i}: #{bm.total}"
|
48
|
+
puts stats.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
case ARGV[0]
|
53
|
+
when '1'
|
54
|
+
puts "with duplicate (downcased) variables, with a custom function:"
|
55
|
+
test(with_duplicate_variables, custom_function: true)
|
56
|
+
|
57
|
+
when '2'
|
58
|
+
puts "with duplicate (downcased) variables, without a custom function:"
|
59
|
+
test(with_duplicate_variables, custom_function: false)
|
60
|
+
|
61
|
+
when '3'
|
62
|
+
puts "without duplicate (downcased) variables, with a custom function:"
|
63
|
+
test(without_duplicate_variables, custom_function: true)
|
64
|
+
|
65
|
+
when '4'
|
66
|
+
puts "with duplicate (downcased) variables, without a custom function:"
|
67
|
+
test(without_duplicate_variables, custom_function: false)
|
68
|
+
|
69
|
+
else
|
70
|
+
puts "select a run option (1-4)"
|
71
|
+
end
|
data/spec/calculator_spec.rb
CHANGED
@@ -2,7 +2,7 @@ require 'dentaku/calculator'
|
|
2
2
|
|
3
3
|
describe Dentaku::Calculator do
|
4
4
|
let(:calculator) { described_class.new }
|
5
|
-
let(:with_memory) { described_class.new.store(:
|
5
|
+
let(:with_memory) { described_class.new.store(apples: 3) }
|
6
6
|
|
7
7
|
it 'evaluates an expression' do
|
8
8
|
expect(calculator.evaluate('7+3')).to eq(10)
|
@@ -20,8 +20,8 @@ describe Dentaku::Calculator do
|
|
20
20
|
expect(calculator.evaluate('(-2 + 3) - 1')).to eq(0)
|
21
21
|
expect(calculator.evaluate('(-2 - 3) - 1')).to eq(-6)
|
22
22
|
expect(calculator.evaluate('1 + -2 ^ 2')).to eq(-3)
|
23
|
-
expect(calculator.evaluate('3 + -num', :
|
24
|
-
expect(calculator.evaluate('-num + 3', :
|
23
|
+
expect(calculator.evaluate('3 + -num', num: 2)).to eq(1)
|
24
|
+
expect(calculator.evaluate('-num + 3', num: 2)).to eq(1)
|
25
25
|
expect(calculator.evaluate('10 ^ 2')).to eq(100)
|
26
26
|
expect(calculator.evaluate('0 * 10 ^ -5')).to eq(0)
|
27
27
|
expect(calculator.evaluate('3 + 0 * -3')).to eq(3)
|
@@ -34,7 +34,7 @@ describe Dentaku::Calculator do
|
|
34
34
|
it { expect(with_memory.clear).to be_empty }
|
35
35
|
|
36
36
|
it 'discards local values' do
|
37
|
-
expect(calculator.evaluate('pears * 2', :
|
37
|
+
expect(calculator.evaluate('pears * 2', pears: 5)).to eq(10)
|
38
38
|
expect(calculator).to be_empty
|
39
39
|
end
|
40
40
|
|
@@ -54,6 +54,7 @@ describe Dentaku::Calculator do
|
|
54
54
|
it "finds dependencies in a generic statement" do
|
55
55
|
expect(calculator.dependencies("bob + dole / 3")).to eq(['bob', 'dole'])
|
56
56
|
end
|
57
|
+
|
57
58
|
it "doesn't consider variables in memory as dependencies" do
|
58
59
|
expect(with_memory.dependencies("apples + oranges")).to eq(['oranges'])
|
59
60
|
end
|
@@ -81,6 +82,11 @@ describe Dentaku::Calculator do
|
|
81
82
|
calculator.solve!(health: "happiness", happiness: "health")
|
82
83
|
end.to raise_error (TSort::Cyclic)
|
83
84
|
end
|
85
|
+
|
86
|
+
it 'is case-insensitive' do
|
87
|
+
result = with_memory.solve!(total_fruit: "Apples + pears", pears: 10)
|
88
|
+
expect(result[:total_fruit]).to eq 13
|
89
|
+
end
|
84
90
|
end
|
85
91
|
|
86
92
|
it 'evaluates a statement with no variables' do
|
@@ -100,54 +106,54 @@ describe Dentaku::Calculator do
|
|
100
106
|
end
|
101
107
|
|
102
108
|
it 'evaluates unbound statements given a binding in memory' do
|
103
|
-
expect(calculator.evaluate('foo * 1.5', :
|
104
|
-
expect(calculator.bind(:
|
109
|
+
expect(calculator.evaluate('foo * 1.5', foo: 2)).to eq(3)
|
110
|
+
expect(calculator.bind(monkeys: 3).evaluate('monkeys < 7')).to be_truthy
|
105
111
|
expect(calculator.evaluate('monkeys / 1.5')).to eq(2)
|
106
112
|
end
|
107
113
|
|
108
114
|
it 'rebinds for each evaluation' do
|
109
|
-
expect(calculator.evaluate('foo * 2', :
|
110
|
-
expect(calculator.evaluate('foo * 2', :
|
115
|
+
expect(calculator.evaluate('foo * 2', foo: 2)).to eq(4)
|
116
|
+
expect(calculator.evaluate('foo * 2', foo: 4)).to eq(8)
|
111
117
|
end
|
112
118
|
|
113
119
|
it 'accepts strings or symbols for binding keys' do
|
114
|
-
expect(calculator.evaluate('foo * 2', :
|
120
|
+
expect(calculator.evaluate('foo * 2', foo: 2)).to eq(4)
|
115
121
|
expect(calculator.evaluate('foo * 2', 'foo' => 4)).to eq(8)
|
116
122
|
end
|
117
123
|
|
118
124
|
it 'accepts digits in identifiers' do
|
119
|
-
expect(calculator.evaluate('foo1 * 2', :
|
125
|
+
expect(calculator.evaluate('foo1 * 2', foo1: 2)).to eq(4)
|
120
126
|
expect(calculator.evaluate('foo1 * 2', 'foo1' => 4)).to eq(8)
|
121
127
|
expect(calculator.evaluate('1foo * 2', '1foo' => 2)).to eq(4)
|
122
|
-
expect(calculator.evaluate('fo1o * 2', :
|
128
|
+
expect(calculator.evaluate('fo1o * 2', fo1o: 4)).to eq(8)
|
123
129
|
end
|
124
130
|
|
125
131
|
it 'compares string literals with string variables' do
|
126
|
-
expect(calculator.evaluate('fruit = "apple"', :
|
127
|
-
expect(calculator.evaluate('fruit = "apple"', :
|
132
|
+
expect(calculator.evaluate('fruit = "apple"', fruit: 'apple')).to be_truthy
|
133
|
+
expect(calculator.evaluate('fruit = "apple"', fruit: 'pear')).to be_falsey
|
128
134
|
end
|
129
135
|
|
130
136
|
it 'performs case-sensitive comparison' do
|
131
|
-
expect(calculator.evaluate('fruit = "Apple"', :
|
132
|
-
expect(calculator.evaluate('fruit = "Apple"', :
|
137
|
+
expect(calculator.evaluate('fruit = "Apple"', fruit: 'apple')).to be_falsey
|
138
|
+
expect(calculator.evaluate('fruit = "Apple"', fruit: 'Apple')).to be_truthy
|
133
139
|
end
|
134
140
|
|
135
141
|
it 'allows binding logical values' do
|
136
|
-
expect(calculator.evaluate('some_boolean AND 7 > 5', :
|
137
|
-
expect(calculator.evaluate('some_boolean AND 7 < 5', :
|
138
|
-
expect(calculator.evaluate('some_boolean AND 7 > 5', :
|
142
|
+
expect(calculator.evaluate('some_boolean AND 7 > 5', some_boolean: true)).to be_truthy
|
143
|
+
expect(calculator.evaluate('some_boolean AND 7 < 5', some_boolean: true)).to be_falsey
|
144
|
+
expect(calculator.evaluate('some_boolean AND 7 > 5', some_boolean: false)).to be_falsey
|
139
145
|
|
140
|
-
expect(calculator.evaluate('some_boolean OR 7 > 5', :
|
141
|
-
expect(calculator.evaluate('some_boolean OR 7 < 5', :
|
142
|
-
expect(calculator.evaluate('some_boolean OR 7 < 5', :
|
146
|
+
expect(calculator.evaluate('some_boolean OR 7 > 5', some_boolean: true)).to be_truthy
|
147
|
+
expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: true)).to be_truthy
|
148
|
+
expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: false)).to be_falsey
|
143
149
|
end
|
144
150
|
|
145
151
|
describe 'functions' do
|
146
152
|
it 'include IF' do
|
147
|
-
expect(calculator.evaluate('if(foo < 8, 10, 20)', :
|
148
|
-
expect(calculator.evaluate('if(foo < 8, 10, 20)', :
|
149
|
-
expect(calculator.evaluate('if (foo < 8, 10, 20)', :
|
150
|
-
expect(calculator.evaluate('if (foo < 8, 10, 20)', :
|
153
|
+
expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 2)).to eq(10)
|
154
|
+
expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 9)).to eq(20)
|
155
|
+
expect(calculator.evaluate('if (foo < 8, 10, 20)', foo: 2)).to eq(10)
|
156
|
+
expect(calculator.evaluate('if (foo < 8, 10, 20)', foo: 9)).to eq(20)
|
151
157
|
end
|
152
158
|
|
153
159
|
it 'include ROUND' do
|
@@ -155,22 +161,22 @@ describe Dentaku::Calculator do
|
|
155
161
|
expect(calculator.evaluate('round(8.8)')).to eq(9)
|
156
162
|
expect(calculator.evaluate('round(8.75, 1)')).to eq(BigDecimal.new('8.8'))
|
157
163
|
|
158
|
-
expect(calculator.evaluate('ROUND(apples * 0.93)', { :
|
164
|
+
expect(calculator.evaluate('ROUND(apples * 0.93)', { apples: 10 })).to eq(9)
|
159
165
|
end
|
160
166
|
|
161
167
|
it 'include NOT' do
|
162
|
-
expect(calculator.evaluate('NOT(some_boolean)', :
|
163
|
-
expect(calculator.evaluate('NOT(some_boolean)', :
|
168
|
+
expect(calculator.evaluate('NOT(some_boolean)', some_boolean: true)).to be_falsey
|
169
|
+
expect(calculator.evaluate('NOT(some_boolean)', some_boolean: false)).to be_truthy
|
164
170
|
|
165
|
-
expect(calculator.evaluate('NOT(some_boolean) AND 7 > 5', :
|
166
|
-
expect(calculator.evaluate('NOT(some_boolean) OR 7 < 5', :
|
171
|
+
expect(calculator.evaluate('NOT(some_boolean) AND 7 > 5', some_boolean: true)).to be_falsey
|
172
|
+
expect(calculator.evaluate('NOT(some_boolean) OR 7 < 5', some_boolean: false)).to be_truthy
|
167
173
|
end
|
168
174
|
|
169
175
|
it 'evaluates functions with negative numbers' do
|
170
176
|
expect(calculator.evaluate('if (-1 < 5, -1, 5)')).to eq(-1)
|
171
177
|
expect(calculator.evaluate('if (-1 = -1, -1, 5)')).to eq(-1)
|
172
178
|
expect(calculator.evaluate('round(-1.23, 1)')).to eq(BigDecimal.new('-1.2'))
|
173
|
-
expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', :
|
179
|
+
expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
|
174
180
|
end
|
175
181
|
end
|
176
182
|
end
|
data/spec/dentaku_spec.rb
CHANGED
data/spec/evaluator_spec.rb
CHANGED
@@ -2,7 +2,8 @@ require 'spec_helper'
|
|
2
2
|
require 'dentaku/evaluator'
|
3
3
|
|
4
4
|
describe Dentaku::Evaluator do
|
5
|
-
let(:
|
5
|
+
let(:rule_set) { Dentaku::RuleSet.new }
|
6
|
+
let(:evaluator) { Dentaku::Evaluator.new(rule_set) }
|
6
7
|
|
7
8
|
describe 'rule scanning' do
|
8
9
|
it 'finds a matching rule' do
|
@@ -81,7 +82,7 @@ describe Dentaku::Evaluator do
|
|
81
82
|
|
82
83
|
describe 'find_rule_match' do
|
83
84
|
it 'matches a function call' do
|
84
|
-
if_pattern, _ = *
|
85
|
+
if_pattern, _ = *rule_set.rules.first
|
85
86
|
position, tokens = evaluator.find_rule_match(if_pattern, token_stream(:if, :fopen, true, :comma, 1, :comma, 2, :close))
|
86
87
|
expect(position).to eq 0
|
87
88
|
expect(tokens.length).to eq 8
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'dentaku/rule_set'
|
2
|
+
|
3
|
+
describe Dentaku::RuleSet do
|
4
|
+
subject { described_class.new }
|
5
|
+
|
6
|
+
it 'yields core rules' do
|
7
|
+
functions = []
|
8
|
+
subject.each { |pattern, function| functions << function }
|
9
|
+
expect(functions).to eq [:if,
|
10
|
+
:round,
|
11
|
+
:round_int,
|
12
|
+
:round_int,
|
13
|
+
:not,
|
14
|
+
:evaluate_group,
|
15
|
+
:negate,
|
16
|
+
:apply,
|
17
|
+
:pow_negate,
|
18
|
+
:apply,
|
19
|
+
:apply,
|
20
|
+
:mul_negate,
|
21
|
+
:apply,
|
22
|
+
:percentage,
|
23
|
+
:negate,
|
24
|
+
:expand_range,
|
25
|
+
:expand_range,
|
26
|
+
:apply,
|
27
|
+
:apply,
|
28
|
+
:apply,
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'adds custom function patterns' do
|
33
|
+
functions = []
|
34
|
+
subject.add_function(
|
35
|
+
name: :min,
|
36
|
+
type: :numeric,
|
37
|
+
signature: [ :arguments ],
|
38
|
+
body: ->(*args) { args.min }
|
39
|
+
)
|
40
|
+
subject.each { |pattern, function| functions << function }
|
41
|
+
expect(functions).to include('min')
|
42
|
+
end
|
43
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.2.
|
4
|
+
version: 1.2.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-05-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|
@@ -60,18 +60,20 @@ files:
|
|
60
60
|
- lib/dentaku/exceptions.rb
|
61
61
|
- lib/dentaku/expression.rb
|
62
62
|
- lib/dentaku/external_function.rb
|
63
|
-
- lib/dentaku/
|
63
|
+
- lib/dentaku/rule_set.rb
|
64
64
|
- lib/dentaku/token.rb
|
65
65
|
- lib/dentaku/token_matcher.rb
|
66
66
|
- lib/dentaku/token_scanner.rb
|
67
67
|
- lib/dentaku/tokenizer.rb
|
68
68
|
- lib/dentaku/version.rb
|
69
|
+
- spec/benchmark.rb
|
69
70
|
- spec/binary_operation_spec.rb
|
70
71
|
- spec/calculator_spec.rb
|
71
72
|
- spec/dentaku_spec.rb
|
72
73
|
- spec/evaluator_spec.rb
|
73
74
|
- spec/expression_spec.rb
|
74
75
|
- spec/external_function_spec.rb
|
76
|
+
- spec/rule_set_spec.rb
|
75
77
|
- spec/spec_helper.rb
|
76
78
|
- spec/token_matcher_spec.rb
|
77
79
|
- spec/token_scanner_spec.rb
|
@@ -97,19 +99,22 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
97
99
|
version: '0'
|
98
100
|
requirements: []
|
99
101
|
rubyforge_project: dentaku
|
100
|
-
rubygems_version: 2.
|
102
|
+
rubygems_version: 2.4.5
|
101
103
|
signing_key:
|
102
104
|
specification_version: 4
|
103
105
|
summary: A formula language parser and evaluator
|
104
106
|
test_files:
|
107
|
+
- spec/benchmark.rb
|
105
108
|
- spec/binary_operation_spec.rb
|
106
109
|
- spec/calculator_spec.rb
|
107
110
|
- spec/dentaku_spec.rb
|
108
111
|
- spec/evaluator_spec.rb
|
109
112
|
- spec/expression_spec.rb
|
110
113
|
- spec/external_function_spec.rb
|
114
|
+
- spec/rule_set_spec.rb
|
111
115
|
- spec/spec_helper.rb
|
112
116
|
- spec/token_matcher_spec.rb
|
113
117
|
- spec/token_scanner_spec.rb
|
114
118
|
- spec/token_spec.rb
|
115
119
|
- spec/tokenizer_spec.rb
|
120
|
+
has_rdoc:
|
data/lib/dentaku/rules.rb
DELETED
@@ -1,115 +0,0 @@
|
|
1
|
-
require 'dentaku/external_function'
|
2
|
-
require 'dentaku/token'
|
3
|
-
require 'dentaku/token_matcher'
|
4
|
-
|
5
|
-
module Dentaku
|
6
|
-
class Rules
|
7
|
-
def self.core_rules
|
8
|
-
[
|
9
|
-
[ p(:if), :if ],
|
10
|
-
[ p(:round), :round ],
|
11
|
-
[ p(:roundup), :round_int ],
|
12
|
-
[ p(:rounddown), :round_int ],
|
13
|
-
[ p(:not), :not ],
|
14
|
-
|
15
|
-
[ p(:group), :evaluate_group ],
|
16
|
-
[ p(:start_neg), :negate ],
|
17
|
-
[ p(:math_pow), :apply ],
|
18
|
-
[ p(:math_neg_pow), :pow_negate ],
|
19
|
-
[ p(:math_mod), :apply ],
|
20
|
-
[ p(:math_mul), :apply ],
|
21
|
-
[ p(:math_neg_mul), :mul_negate ],
|
22
|
-
[ p(:math_add), :apply ],
|
23
|
-
[ p(:percentage), :percentage ],
|
24
|
-
[ p(:negation), :negate ],
|
25
|
-
[ p(:range_asc), :expand_range ],
|
26
|
-
[ p(:range_desc), :expand_range ],
|
27
|
-
[ p(:num_comp), :apply ],
|
28
|
-
[ p(:str_comp), :apply ],
|
29
|
-
[ p(:combine), :apply ]
|
30
|
-
]
|
31
|
-
end
|
32
|
-
|
33
|
-
def self.each
|
34
|
-
@rules ||= core_rules
|
35
|
-
@rules.each { |r| yield r }
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.add_function(f)
|
39
|
-
ext = ExternalFunction.new(f[:name], f[:type], f[:signature], f[:body])
|
40
|
-
|
41
|
-
@rules ||= core_rules
|
42
|
-
@funcs ||= {}
|
43
|
-
|
44
|
-
## rules need to be added to the beginning of @rules for precedence
|
45
|
-
@rules.unshift [
|
46
|
-
[
|
47
|
-
TokenMatcher.send(ext.name),
|
48
|
-
t(:fopen),
|
49
|
-
*pattern(*ext.tokens),
|
50
|
-
t(:close)
|
51
|
-
],
|
52
|
-
ext.name
|
53
|
-
]
|
54
|
-
@funcs[ext.name] = ext
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.func(name)
|
58
|
-
@funcs ||= {}
|
59
|
-
@funcs.fetch(name)
|
60
|
-
end
|
61
|
-
|
62
|
-
def self.t(name)
|
63
|
-
@matchers ||= generate_matchers
|
64
|
-
@matchers.fetch(name)
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.generate_matchers
|
68
|
-
[
|
69
|
-
:numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
|
70
|
-
:comparator, :comp_gt, :comp_lt, :fopen, :open, :close, :comma,
|
71
|
-
:non_close_plus, :non_group, :non_group_star, :arguments,
|
72
|
-
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
|
73
|
-
:anchored_minus, :math_neg_pow, :math_neg_mul
|
74
|
-
].each_with_object({}) do |name, matchers|
|
75
|
-
matchers[name] = TokenMatcher.send(name)
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
def self.p(name)
|
80
|
-
@patterns ||= {
|
81
|
-
group: pattern(:open, :non_group_star, :close),
|
82
|
-
math_add: pattern(:numeric, :addsub, :numeric),
|
83
|
-
math_mul: pattern(:numeric, :muldiv, :numeric),
|
84
|
-
math_neg_mul: pattern(:numeric, :muldiv, :subtract, :numeric),
|
85
|
-
math_pow: pattern(:numeric, :pow, :numeric),
|
86
|
-
math_neg_pow: pattern(:numeric, :pow, :subtract, :numeric),
|
87
|
-
math_mod: pattern(:numeric, :mod, :numeric),
|
88
|
-
negation: pattern(:subtract, :numeric),
|
89
|
-
start_neg: pattern(:anchored_minus, :numeric),
|
90
|
-
percentage: pattern(:numeric, :mod),
|
91
|
-
range_asc: pattern(:numeric, :comp_lt, :numeric, :comp_lt, :numeric),
|
92
|
-
range_desc: pattern(:numeric, :comp_gt, :numeric, :comp_gt, :numeric),
|
93
|
-
num_comp: pattern(:numeric, :comparator, :numeric),
|
94
|
-
str_comp: pattern(:string, :comparator, :string),
|
95
|
-
combine: pattern(:logical, :combinator, :logical),
|
96
|
-
|
97
|
-
if: func_pattern(:if, :non_group, :comma, :non_group, :comma, :non_group),
|
98
|
-
round: func_pattern(:round, :arguments),
|
99
|
-
roundup: func_pattern(:roundup, :arguments),
|
100
|
-
rounddown: func_pattern(:rounddown, :arguments),
|
101
|
-
not: func_pattern(:not, :arguments)
|
102
|
-
}
|
103
|
-
|
104
|
-
@patterns[name]
|
105
|
-
end
|
106
|
-
|
107
|
-
def self.pattern(*symbols)
|
108
|
-
symbols.map { |s| t(s) }
|
109
|
-
end
|
110
|
-
|
111
|
-
def self.func_pattern(func, *tokens)
|
112
|
-
pattern(func, :fopen, *tokens, :close)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
end
|