dentaku 2.0.3 → 2.0.4

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