dentaku 1.2.4 → 1.2.5
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 +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
|