dentaku 2.0.3 → 2.0.4

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: 34a03b8d224b161ba7b6cdbfd0d5f1fd344f932c
4
- data.tar.gz: 20e97d8a754981e1597748723afd4f54285a0e19
3
+ metadata.gz: 568297af6c05c99fdd87b6a2d70cd78b4d1f5828
4
+ data.tar.gz: 9e7668dd3998237a5fe6aaf50370dadfb6941bee
5
5
  SHA512:
6
- metadata.gz: 510851224ae2e3e6b6a19b229cfe6095a28eaf9c6ac978edd6ea40295f4dc9a37b2fdb03d6f0072ef79b9fcfe1bf34f715f3f910d21b79491ce72643e66cddaf
7
- data.tar.gz: b27e2dfc0c821f04475310eb47e6d30d908c02a77ba7da7512ed703a21b99c62e6a85a21be25d34db8540d9e10d131971ca45dfd54be554aed0a29e71e9b1b50
6
+ metadata.gz: 51685ec73f241afaed1bf66961b55837792100a0ecf06c2d5b283eca227fdb3129c76045fe676b8416fa40d54e11d1ef0da8ef298c504faeace2f77caf168955
7
+ data.tar.gz: a9a6914b90950b7616e3c82c704ab58921d0107045a3f440f5de2848acbf056ee95fefd8f2f63b663cb33f71f72f3733d94f13b7dd1f0895c1285f5bcab9181c
@@ -8,5 +8,4 @@ rvm:
8
8
  - 2.2.1
9
9
  - 2.2.2
10
10
  - 2.2.3
11
- - jruby-19mode
12
11
  - rbx-2
@@ -1,5 +1,10 @@
1
1
  # Change Log
2
2
 
3
+ ## [v2.0.4] 2015-09-03
4
+ - fix BigDecimal conversion bug
5
+ - add caching for bulk expression solving dependency order
6
+ - allow for custom configuration for token scanners
7
+
3
8
  ## [v2.0.3] 2015-08-25
4
9
  - bug fixes
5
10
  - performance enhancements
@@ -80,6 +85,7 @@
80
85
  ## [v0.1.0] 2012-01-20
81
86
  - initial release
82
87
 
88
+ [v2.0.4]: https://github.com/rubysolo/dentaku/compare/v2.0.3...v2.0.4
83
89
  [v2.0.3]: https://github.com/rubysolo/dentaku/compare/v2.0.1...v2.0.3
84
90
  [v2.0.1]: https://github.com/rubysolo/dentaku/compare/v2.0.0...v2.0.1
85
91
  [v2.0.0]: https://github.com/rubysolo/dentaku/compare/v1.2.6...v2.0.0
@@ -7,6 +7,11 @@ module Dentaku
7
7
  calculator.evaluate(expression, data)
8
8
  end
9
9
 
10
+ def self.enable_caching!
11
+ enable_ast_cache!
12
+ enable_dependency_order_cache!
13
+ end
14
+
10
15
  def self.enable_ast_cache!
11
16
  @enable_ast_caching = true
12
17
  end
@@ -15,6 +20,14 @@ module Dentaku
15
20
  @enable_ast_caching
16
21
  end
17
22
 
23
+ def self.enable_dependency_order_cache!
24
+ @enable_dependency_order_caching = true
25
+ end
26
+
27
+ def self.cache_dependency_order?
28
+ @enable_dependency_order_caching
29
+ end
30
+
18
31
  private
19
32
 
20
33
  def self.calculator
@@ -1,5 +1,6 @@
1
1
  require_relative './operation'
2
2
  require 'bigdecimal'
3
+ require 'bigdecimal/util'
3
4
 
4
5
  module Dentaku
5
6
  module AST
@@ -13,16 +14,28 @@ module Dentaku
13
14
  :numeric
14
15
  end
15
16
 
17
+ def value(context={})
18
+ l = cast(left.value(context))
19
+ r = cast(right.value(context))
20
+ l.public_send(operator, r)
21
+ end
22
+
16
23
  private
17
24
 
25
+ def cast(value, prefer_integer=true)
26
+ v = BigDecimal.new(value, Float::DIG+1)
27
+ v = v.to_i if prefer_integer && v.frac.zero?
28
+ v
29
+ end
30
+
18
31
  def valid_node?(node)
19
32
  node.is_a?(Identifier) || node.type == :numeric
20
33
  end
21
34
  end
22
35
 
23
36
  class Addition < Arithmetic
24
- def value(context={})
25
- left.value(context) + right.value(context)
37
+ def operator
38
+ :+
26
39
  end
27
40
 
28
41
  def self.precedence
@@ -31,8 +44,8 @@ module Dentaku
31
44
  end
32
45
 
33
46
  class Subtraction < Arithmetic
34
- def value(context={})
35
- left.value(context) - right.value(context)
47
+ def operator
48
+ :-
36
49
  end
37
50
 
38
51
  def self.precedence
@@ -41,8 +54,8 @@ module Dentaku
41
54
  end
42
55
 
43
56
  class Multiplication < Arithmetic
44
- def value(context={})
45
- left.value(context) * right.value(context)
57
+ def operator
58
+ :*
46
59
  end
47
60
 
48
61
  def self.precedence
@@ -52,12 +65,10 @@ module Dentaku
52
65
 
53
66
  class Division < Arithmetic
54
67
  def value(context={})
55
- r = BigDecimal.new(right.value(context))
68
+ r = cast(right.value(context), false)
56
69
  raise ZeroDivisionError if r.zero?
57
70
 
58
- v = BigDecimal.new(left.value(context)) / r
59
- v = v.to_i if v.frac.zero?
60
- v
71
+ cast(cast(left.value(context)) / r)
61
72
  end
62
73
 
63
74
  def self.precedence
@@ -66,8 +77,8 @@ module Dentaku
66
77
  end
67
78
 
68
79
  class Modulo < Arithmetic
69
- def value(context={})
70
- left.value(context) % right.value(context)
80
+ def operator
81
+ :%
71
82
  end
72
83
 
73
84
  def self.precedence
@@ -76,8 +87,8 @@ module Dentaku
76
87
  end
77
88
 
78
89
  class Exponentiation < Arithmetic
79
- def value(context={})
80
- left.value(context) ** right.value(context)
90
+ def operator
91
+ :**
81
92
  end
82
93
 
83
94
  def self.precedence
@@ -13,7 +13,7 @@ module Dentaku
13
13
  v = context[identifier]
14
14
  case v
15
15
  when Node
16
- v.value
16
+ v.value(context)
17
17
  when NilClass
18
18
  raise UnboundVariableError.new([identifier])
19
19
  else
@@ -22,7 +22,13 @@ module Dentaku
22
22
  end
23
23
 
24
24
  def dependencies(context={})
25
- context.has_key?(identifier) ? [] : [identifier]
25
+ context.has_key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
26
+ end
27
+
28
+ private
29
+
30
+ def dependencies_of(node)
31
+ node.respond_to?(:dependencies) ? node.dependencies : []
26
32
  end
27
33
  end
28
34
  end
@@ -6,9 +6,9 @@ require 'dentaku/tokenizer'
6
6
 
7
7
  module Dentaku
8
8
  class BulkExpressionSolver
9
- def initialize(expression_hash, memory)
9
+ def initialize(expression_hash, calculator)
10
10
  self.expression_hash = expression_hash
11
- self.calculator = Calculator.new.store(memory)
11
+ self.calculator = calculator
12
12
  end
13
13
 
14
14
  def solve!
@@ -26,6 +26,10 @@ module Dentaku
26
26
 
27
27
  private
28
28
 
29
+ def self.dependency_cache
30
+ @dep_cache ||= {}
31
+ end
32
+
29
33
  attr_accessor :expression_hash, :calculator
30
34
 
31
35
  def return_undefined_handler
@@ -39,28 +43,42 @@ module Dentaku
39
43
  def load_results(&block)
40
44
  variables_in_resolve_order.each_with_object({}) do |var_name, r|
41
45
  begin
42
- r[var_name] = calculator.memory[var_name] || evaluate!(expressions[var_name], r)
46
+ r[var_name] = calculator.memory[var_name] ||
47
+ evaluate!(expressions[var_name], expressions.merge(r))
43
48
  rescue Dentaku::UnboundVariableError, ZeroDivisionError => ex
44
49
  r[var_name] = block.call(ex)
45
50
  end
46
51
  end
47
52
  end
48
53
 
49
- def dependencies(expression)
50
- Parser.new(Tokenizer.new.tokenize(expression)).parse.dependencies
51
- end
52
-
53
54
  def expressions
54
55
  @expressions ||= Hash[expression_hash.map { |k,v| [k.to_s, v] }]
55
56
  end
56
57
 
57
58
  def expression_dependencies
58
- Hash[expressions.map { |var, expr| [var, dependencies(expr)] }]
59
+ Hash[expressions.map { |var, expr| [var, calculator.dependencies(expr)] }].tap do |d|
60
+ d.values.each do |deps|
61
+ unresolved = deps.reject { |ud| d.has_key?(ud) }
62
+ unresolved.each { |u| add_dependencies(d, u) }
63
+ end
64
+ end
65
+ end
66
+
67
+ def add_dependencies(current_dependencies, variable)
68
+ node = calculator.memory[variable]
69
+ if node.respond_to?(:dependencies)
70
+ current_dependencies[variable] = node.dependencies
71
+ node.dependencies.each { |d| add_dependencies(current_dependencies, d) }
72
+ end
59
73
  end
60
74
 
61
75
  def variables_in_resolve_order
62
- @variables_in_resolve_order ||=
63
- DependencyResolver::find_resolve_order(expression_dependencies)
76
+ cache_key = expressions.keys.map(&:to_s).sort.join("|")
77
+ @ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
78
+ DependencyResolver.find_resolve_order(expression_dependencies).tap do |d|
79
+ self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
80
+ end
81
+ }
64
82
  end
65
83
 
66
84
  def evaluate!(expression, results)
@@ -1,3 +1,4 @@
1
+ require 'dentaku'
1
2
  require 'dentaku/bulk_expression_solver'
2
3
  require 'dentaku/exceptions'
3
4
  require 'dentaku/token'
@@ -31,7 +32,7 @@ module Dentaku
31
32
  end
32
33
 
33
34
  def evaluate!(expression, data={})
34
- memory[expression] || store(data) do
35
+ store(data) do
35
36
  node = expression
36
37
  node = ast(node) unless node.is_a?(AST::Node)
37
38
  node.value(memory)
@@ -39,11 +40,11 @@ module Dentaku
39
40
  end
40
41
 
41
42
  def solve!(expression_hash)
42
- BulkExpressionSolver.new(expression_hash, memory).solve!
43
+ BulkExpressionSolver.new(expression_hash, self).solve!
43
44
  end
44
45
 
45
46
  def solve(expression_hash, &block)
46
- BulkExpressionSolver.new(expression_hash, memory).solve(&block)
47
+ BulkExpressionSolver.new(expression_hash, self).solve(&block)
47
48
  end
48
49
 
49
50
  def dependencies(expression)
@@ -79,6 +80,10 @@ module Dentaku
79
80
  end
80
81
  alias_method :bind, :store
81
82
 
83
+ def store_formula(key, formula)
84
+ store(key, ast(formula))
85
+ end
86
+
82
87
  def clear
83
88
  @memory = {}
84
89
  end
@@ -4,6 +4,7 @@ module Dentaku
4
4
 
5
5
  def initialize(unbound_variables)
6
6
  @unbound_variables = unbound_variables
7
+ super("no value provided for variables: #{ unbound_variables.join(', ') }")
7
8
  end
8
9
  end
9
10
  end
@@ -24,23 +24,35 @@ module Dentaku
24
24
  end
25
25
 
26
26
  class << self
27
- def scanners
28
- @scanners ||= [
29
- whitespace,
30
- numeric,
31
- double_quoted_string,
32
- single_quoted_string,
33
- negate,
34
- operator,
35
- grouping,
36
- comparator,
37
- combinator,
38
- boolean,
39
- function,
40
- identifier
27
+ def available_scanners
28
+ [
29
+ :whitespace,
30
+ :numeric,
31
+ :double_quoted_string,
32
+ :single_quoted_string,
33
+ :negate,
34
+ :operator,
35
+ :grouping,
36
+ :comparator,
37
+ :combinator,
38
+ :boolean,
39
+ :function,
40
+ :identifier
41
41
  ]
42
42
  end
43
43
 
44
+ def scanners=(token_scanners)
45
+ @scanners = (token_scanners & available_scanners).map { |scanner|
46
+ self.send(scanner)
47
+ }
48
+ end
49
+
50
+ def scanners
51
+ @scanners ||= available_scanners.map { |scanner|
52
+ self.send(scanner)
53
+ }
54
+ end
55
+
44
56
  def whitespace
45
57
  new(:whitespace, '\s+')
46
58
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "2.0.3"
2
+ VERSION = "2.0.4"
3
3
  end
@@ -2,6 +2,8 @@ require 'spec_helper'
2
2
  require 'dentaku/bulk_expression_solver'
3
3
 
4
4
  RSpec.describe Dentaku::BulkExpressionSolver do
5
+ let(:calculator) { Dentaku::Calculator.new }
6
+
5
7
  describe "#solve!" do
6
8
  it "evaluates properly with variables, even if some in memory" do
7
9
  expressions = {
@@ -9,8 +11,7 @@ RSpec.describe Dentaku::BulkExpressionSolver do
9
11
  weekly_apple_budget: "apples * 7",
10
12
  pear: "1"
11
13
  }
12
- memory = {apples: 3}
13
- solver = described_class.new(expressions, memory)
14
+ solver = described_class.new(expressions, calculator.store(apples: 3))
14
15
  expect(solver.solve!)
15
16
  .to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
16
17
  end
@@ -18,20 +19,20 @@ RSpec.describe Dentaku::BulkExpressionSolver do
18
19
  it "lets you know if a variable is unbound" do
19
20
  expressions = {more_apples: "apples + 1"}
20
21
  expect {
21
- described_class.new(expressions, {}).solve!
22
+ described_class.new(expressions, calculator).solve!
22
23
  }.to raise_error(Dentaku::UnboundVariableError)
23
24
  end
24
25
 
25
26
  it "lets you know if the result is a div/0 error" do
26
27
  expressions = {more_apples: "1/0"}
27
28
  expect {
28
- described_class.new(expressions, {}).solve!
29
+ described_class.new(expressions, calculator).solve!
29
30
  }.to raise_error(ZeroDivisionError)
30
31
  end
31
32
 
32
33
  it "does not require keys to be parseable" do
33
34
  expressions = { "the value of x, incremented" => "x + 1" }
34
- solver = described_class.new(expressions, "x" => 3)
35
+ solver = described_class.new(expressions, calculator.store("x" => 3))
35
36
  expect(solver.solve!).to eq({ "the value of x, incremented" => 4 })
36
37
  end
37
38
  end
@@ -39,19 +40,19 @@ RSpec.describe Dentaku::BulkExpressionSolver do
39
40
  describe "#solve" do
40
41
  it "returns :undefined when variables are unbound" do
41
42
  expressions = {more_apples: "apples + 1"}
42
- expect(described_class.new(expressions, {}).solve)
43
+ expect(described_class.new(expressions, calculator).solve)
43
44
  .to eq(more_apples: :undefined)
44
45
  end
45
46
 
46
47
  it "allows passing in a custom value to an error handler when a variable is unbound" do
47
48
  expressions = {more_apples: "apples + 1"}
48
- expect(described_class.new(expressions, {}).solve { :foo })
49
+ expect(described_class.new(expressions, calculator).solve { :foo })
49
50
  .to eq(more_apples: :foo)
50
51
  end
51
52
 
52
53
  it "allows passing in a custom value to an error handler when there is a div/0 error" do
53
54
  expressions = {more_apples: "1/0"}
54
- expect(described_class.new(expressions, {}).solve { :foo })
55
+ expect(described_class.new(expressions, calculator).solve { :foo })
55
56
  .to eq(more_apples: :foo)
56
57
  end
57
58
  end
@@ -29,6 +29,8 @@ describe Dentaku::Calculator do
29
29
  expect(calculator.evaluate('3 + 0 / -3')).to eq(3)
30
30
  expect(calculator.evaluate('15 % 8')).to eq(7)
31
31
  expect(calculator.evaluate('(((695759/735000)^(1/(1981-1991)))-1)*1000').round(4)).to eq(5.5018)
32
+ expect(calculator.evaluate('0.253/0.253')).to eq(1)
33
+ expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
32
34
  end
33
35
 
34
36
  describe 'memory' do
@@ -51,6 +53,11 @@ describe Dentaku::Calculator do
51
53
  expect(calculator.evaluate!('first')).to eq 1
52
54
  expect(calculator.evaluate!('second')).to eq 2
53
55
  end
56
+
57
+ it 'stores formulas' do
58
+ calculator.store_formula('area', 'length * width')
59
+ expect(calculator.evaluate!('area', length: 5, width: 5)).to eq 25
60
+ end
54
61
  end
55
62
 
56
63
  describe 'dependencies' do
@@ -68,15 +75,15 @@ describe Dentaku::Calculator do
68
75
  expect(with_memory.solve!(
69
76
  weekly_fruit_budget: "weekly_apple_budget + pear * 4",
70
77
  weekly_apple_budget: "apples * 7",
71
- pear: "1"
78
+ pear: "1"
72
79
  )).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
73
80
  end
74
81
 
75
82
  it "preserves hash keys" do
76
83
  expect(calculator.solve!(
77
84
  'meaning_of_life' => 'age + kids',
78
- 'age' => 40,
79
- 'kids' => 2
85
+ 'age' => 40,
86
+ 'kids' => 2
80
87
  )).to eq('age' => 40, 'kids' => 2, 'meaning_of_life' => 42)
81
88
  end
82
89
 
@@ -96,6 +103,20 @@ describe Dentaku::Calculator do
96
103
  calculator.solve!(more_apples: "apples + 1")
97
104
  }.to raise_error(Dentaku::UnboundVariableError)
98
105
  end
106
+
107
+ it 'can reference stored formulas' do
108
+ calculator.store_formula("base_area", "length * width")
109
+ calculator.store_formula("volume", "base_area * height")
110
+
111
+ result = calculator.solve!(
112
+ weight: "volume * 5.432",
113
+ height: "3",
114
+ length: "2",
115
+ width: "length * 2",
116
+ )
117
+
118
+ expect(result[:weight]).to eq 130.368
119
+ end
99
120
  end
100
121
 
101
122
  describe 'solve' do
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/exceptions'
3
+
4
+ describe Dentaku::UnboundVariableError do
5
+ it 'includes variable name(s) in message' do
6
+ exception = described_class.new(['length'])
7
+ expect(exception.message).to match /length/
8
+ end
9
+ end
@@ -25,4 +25,16 @@ describe Dentaku::TokenScanner do
25
25
  it 'returns a list of all configured scanners' do
26
26
  expect(described_class.scanners.length).to eq 12
27
27
  end
28
+
29
+ it 'allows customizing available scanners' do
30
+ described_class.scanners = [:whitespace, :numeric]
31
+ expect(described_class.scanners.length).to eq 2
32
+ described_class.scanners = described_class.available_scanners
33
+ end
34
+
35
+ it 'ignores invalid scanners' do
36
+ described_class.scanners = [:whitespace, :numeric, :fake]
37
+ expect(described_class.scanners.length).to eq 2
38
+ described_class.scanners = described_class.available_scanners
39
+ end
28
40
  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: 2.0.3
4
+ version: 2.0.4
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-08-25 00:00:00.000000000 Z
11
+ date: 2015-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -113,6 +113,7 @@ files:
113
113
  - spec/bulk_expression_solver_spec.rb
114
114
  - spec/calculator_spec.rb
115
115
  - spec/dentaku_spec.rb
116
+ - spec/exceptions_spec.rb
116
117
  - spec/external_function_spec.rb
117
118
  - spec/parser_spec.rb
118
119
  - spec/spec_helper.rb
@@ -155,6 +156,7 @@ test_files:
155
156
  - spec/bulk_expression_solver_spec.rb
156
157
  - spec/calculator_spec.rb
157
158
  - spec/dentaku_spec.rb
159
+ - spec/exceptions_spec.rb
158
160
  - spec/external_function_spec.rb
159
161
  - spec/parser_spec.rb
160
162
  - spec/spec_helper.rb