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 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