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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 2b6c2a647d615b07bb92995438da601259cb87f3
4
- data.tar.gz: 75c685525c0eb619726c0d03ded7401573eaaf33
3
+ metadata.gz: cca185ca8ad75d5c0c0cd13241ad8356a6d046f0
4
+ data.tar.gz: 469a8af2de92efdb3b75297ec7de268e6b066492
5
5
  SHA512:
6
- metadata.gz: 7035e118512d9c98dbcc31cb510358e8ca5fee3b98b9bb2242671a7ede8c8ae0c1e4028869367615b9c91a9ff82cece3c13e70032547ef432539c9d0b8986be1
7
- data.tar.gz: fb40309c2d88b7036462a4acb372753350b6655a519fa165a0f534a5464a50088cd53aa908c33e59c06bb957e69305190bd33361805d6346961c8a04126a39a7
6
+ metadata.gz: b0348e2c7b06c318ab67f88bfddc5f6c6e5ac44efdaeacd53d35a23764f3d961530524c7929673ce3737375f0b93671dedadccb7ab5d0d363f4916b218ce0b30
7
+ data.tar.gz: 36043cafe22a9f2dd60f926a13832dc3175e3f7000c6eac081925d234501f287c6289adeed51b45334d9c7353a4a29a5458b7414cd612ecbb05039e852590914
data/.travis.yml CHANGED
@@ -4,5 +4,8 @@ rvm:
4
4
  - 2.0.0
5
5
  - 2.1.0
6
6
  - 2.1.1
7
+ - 2.2.0
8
+ - 2.2.1
9
+ - 2.2.2
7
10
  - jruby-19mode
8
11
  - rbx-2
@@ -1,25 +1,26 @@
1
1
  require 'dentaku/evaluator'
2
2
  require 'dentaku/exceptions'
3
3
  require 'dentaku/expression'
4
- require 'dentaku/rules'
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
- Rules.add_function(fn)
18
+ rule_set.add_function(fn)
18
19
  self
19
20
  end
20
21
 
21
22
  def add_functions(fns)
22
- fns.each { |fn| Rules.add_function(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
@@ -18,7 +18,7 @@ module Dentaku
18
18
  end
19
19
 
20
20
  def tsort_each_child(node, &block)
21
- @vars_to_deps[node.to_s].each(&block)
21
+ @vars_to_deps.fetch(node.to_s, []).each(&block)
22
22
  end
23
23
  end
24
24
  end
@@ -1,8 +1,14 @@
1
- require 'dentaku/rules'
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
- Rules.each do |pattern, evaluator|
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 = Rules.func(evaluator)
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
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "1.2.4"
2
+ VERSION = "1.2.5"
3
3
  end
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
@@ -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(:apples => 3) }
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', :num => 2)).to eq(1)
24
- expect(calculator.evaluate('-num + 3', :num => 2)).to eq(1)
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', :pears => 5)).to eq(10)
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', :foo => 2)).to eq(3)
104
- expect(calculator.bind(:monkeys => 3).evaluate('monkeys < 7')).to be_truthy
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', :foo => 2)).to eq(4)
110
- expect(calculator.evaluate('foo * 2', :foo => 4)).to eq(8)
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', :foo => 2)).to eq(4)
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', :foo1 => 2)).to eq(4)
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', :fo1o => 4)).to eq(8)
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"', :fruit => 'apple')).to be_truthy
127
- expect(calculator.evaluate('fruit = "apple"', :fruit => 'pear')).to be_falsey
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"', :fruit => 'apple')).to be_falsey
132
- expect(calculator.evaluate('fruit = "Apple"', :fruit => 'Apple')).to be_truthy
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', :some_boolean => true)).to be_truthy
137
- expect(calculator.evaluate('some_boolean AND 7 < 5', :some_boolean => true)).to be_falsey
138
- expect(calculator.evaluate('some_boolean AND 7 > 5', :some_boolean => false)).to be_falsey
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', :some_boolean => true)).to be_truthy
141
- expect(calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => true)).to be_truthy
142
- expect(calculator.evaluate('some_boolean OR 7 < 5', :some_boolean => false)).to be_falsey
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)', :foo => 2)).to eq(10)
148
- expect(calculator.evaluate('if(foo < 8, 10, 20)', :foo => 9)).to eq(20)
149
- expect(calculator.evaluate('if (foo < 8, 10, 20)', :foo => 2)).to eq(10)
150
- expect(calculator.evaluate('if (foo < 8, 10, 20)', :foo => 9)).to eq(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)', { :apples => 10 })).to eq(9)
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)', :some_boolean => true)).to be_falsey
163
- expect(calculator.evaluate('NOT(some_boolean)', :some_boolean => false)).to be_truthy
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', :some_boolean => true)).to be_falsey
166
- expect(calculator.evaluate('NOT(some_boolean) OR 7 < 5', :some_boolean => false)).to be_truthy
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', :some_boolean => true)).to be_falsey
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
@@ -6,7 +6,7 @@ describe Dentaku do
6
6
  end
7
7
 
8
8
  it 'binds values to variables' do
9
- expect(Dentaku('oranges > 7', {:oranges => 10})).to be_truthy
9
+ expect(Dentaku('oranges > 7', oranges: 10)).to be_truthy
10
10
  end
11
11
 
12
12
  it 'evaulates a nested function' do
@@ -2,7 +2,8 @@ require 'spec_helper'
2
2
  require 'dentaku/evaluator'
3
3
 
4
4
  describe Dentaku::Evaluator do
5
- let(:evaluator) { Dentaku::Evaluator.new }
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, _ = *Dentaku::Rules.core_rules.first
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
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-01-22 00:00:00.000000000 Z
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/rules.rb
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.2.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