dentaku 2.0.6 → 2.0.7

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: 813c0e0ea4c6a021ca324515d26386d2f2f28962
4
- data.tar.gz: 943958582e98916af345553a343b261b978c97c5
3
+ metadata.gz: c299535113d559ed61d819be474ced8128bf7e0f
4
+ data.tar.gz: 80aa950604c573fcbee95cfa6bb820a1fa6af94b
5
5
  SHA512:
6
- metadata.gz: d281ac5672cb9badc8ba74033d5624bd2e23d12f37785a6fc47595be0de51c8da9a2419eb0701de273e6ed710212988f47a16a72fef9fcbc119a8c39b38ebad4
7
- data.tar.gz: 8f3ca27e26d5a603c99d2473a3115fb87c00407127b7d1613f0c0634057f37318583de7a7f8192d47db0ea8236648e916ea6dbc6084add0b591b4939e95c48f1
6
+ metadata.gz: 616133f23a73885b22d1e53722d8482f9522e249ca03a76fe7a8685d1d365eebbde7b1677248ea6758cbf29e60f54504f9c4a4a9ee43735ab54cffc8cb9d11ac
7
+ data.tar.gz: aea6a8919d66c838c704ba09b19787ddeb301f72ead03883ffc481ffe4ff7a54afa92108ed73794feaec4d83ea93cc17476dc108d0273401dba9c6077a603ec0
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [v2.0.7] 2016-02-25
6
+ - fail with gem-specific error for parsing issues
7
+ - support NULL literals and nil variables
8
+ - keep reference to variable that caused failure when bulk-solving
9
+
5
10
  ## [v2.0.6] 2016-01-26
6
11
  - support array parameters for external functions
7
12
  - support case statements
@@ -98,7 +103,8 @@
98
103
  ## [v0.1.0] 2012-01-20
99
104
  - initial release
100
105
 
101
- [Unreleased]: https://github.com/rubysolo/dentaku/compare/v2.0.6...HEAD
106
+ [Unreleased]: https://github.com/rubysolo/dentaku/compare/v2.0.7...HEAD
107
+ [v2.0.7]: https://github.com/rubysolo/dentaku/compare/v2.0.6...v2.0.7
102
108
  [v2.0.6]: https://github.com/rubysolo/dentaku/compare/v2.0.5...v2.0.6
103
109
  [v2.0.5]: https://github.com/rubysolo/dentaku/compare/v2.0.4...v2.0.5
104
110
  [v2.0.4]: https://github.com/rubysolo/dentaku/compare/v2.0.3...v2.0.4
@@ -7,7 +7,9 @@ module Dentaku
7
7
  class Arithmetic < Operation
8
8
  def initialize(*)
9
9
  super
10
- fail "#{ self.class } requires numeric operands" unless valid_node?(left) && valid_node?(right)
10
+ unless valid_node?(left) && valid_node?(right)
11
+ fail ParseError, "#{ self.class } requires numeric operands"
12
+ end
11
13
  end
12
14
 
13
15
  def type
@@ -29,7 +31,7 @@ module Dentaku
29
31
  end
30
32
 
31
33
  def valid_node?(node)
32
- node.dependencies.any? || node.type == :numeric
34
+ node && (node.dependencies.any? || node.type == :numeric)
33
35
  end
34
36
  end
35
37
 
@@ -66,7 +68,7 @@ module Dentaku
66
68
  class Division < Arithmetic
67
69
  def value(context={})
68
70
  r = cast(right.value(context), false)
69
- raise ZeroDivisionError if r.zero?
71
+ raise Dentaku::ZeroDivisionError if r.zero?
70
72
 
71
73
  cast(cast(left.value(context)) / r)
72
74
  end
@@ -5,7 +5,9 @@ module Dentaku
5
5
  class Combinator < Operation
6
6
  def initialize(*)
7
7
  super
8
- fail "#{ self.class } requires logical operands" unless valid_node?(left) && valid_node?(right)
8
+ unless valid_node?(left) && valid_node?(right)
9
+ fail ParseError, "#{ self.class } requires logical operands"
10
+ end
9
11
  end
10
12
 
11
13
  def type
@@ -12,7 +12,9 @@ module Dentaku
12
12
  end
13
13
 
14
14
  def self.get(name)
15
- registry.fetch(function_name(name)) { fail "Undefined function #{ name } "}
15
+ registry.fetch(function_name(name)) {
16
+ fail ParseError, "Undefined function #{ name }"
17
+ }
16
18
  end
17
19
 
18
20
  def self.register(name, type, implementation)
@@ -10,12 +10,13 @@ module Dentaku
10
10
  end
11
11
 
12
12
  def value(context={})
13
- v = context[identifier]
13
+ v = context.fetch(identifier) do
14
+ raise UnboundVariableError.new([identifier])
15
+ end
16
+
14
17
  case v
15
18
  when Node
16
19
  v.value(context)
17
- when NilClass
18
- raise UnboundVariableError.new([identifier])
19
20
  else
20
21
  v
21
22
  end
@@ -3,7 +3,7 @@ module Dentaku
3
3
  class Negation < Operation
4
4
  def initialize(node)
5
5
  @node = node
6
- fail "Negation requires numeric operand" unless valid_node?(node)
6
+ fail ParseError, "Negation requires numeric operand" unless valid_node?(node)
7
7
  end
8
8
 
9
9
  def value(context={})
@@ -33,7 +33,7 @@ module Dentaku
33
33
  private
34
34
 
35
35
  def valid_node?(node)
36
- node.dependencies.any? || node.type == :numeric
36
+ node && (node.dependencies.any? || node.type == :numeric)
37
37
  end
38
38
  end
39
39
  end
@@ -43,9 +43,20 @@ module Dentaku
43
43
  def load_results(&block)
44
44
  variables_in_resolve_order.each_with_object({}) do |var_name, r|
45
45
  begin
46
- r[var_name] = calculator.memory[var_name] ||
47
- evaluate!(expressions[var_name], expressions.merge(r))
46
+ value_from_memory = calculator.memory[var_name]
47
+
48
+ if value_from_memory.nil? &&
49
+ expressions[var_name].nil? &&
50
+ !calculator.memory.has_key?(var_name)
51
+ next
52
+ end
53
+
54
+ value = value_from_memory ||
55
+ evaluate!(expressions[var_name], expressions.merge(r))
56
+
57
+ r[var_name] = value
48
58
  rescue Dentaku::UnboundVariableError, ZeroDivisionError => ex
59
+ ex.recipient_variable = var_name
49
60
  r[var_name] = block.call(ex)
50
61
  end
51
62
  end
@@ -1,5 +1,7 @@
1
1
  module Dentaku
2
2
  class UnboundVariableError < StandardError
3
+ attr_accessor :recipient_variable
4
+
3
5
  attr_reader :unbound_variables
4
6
 
5
7
  def initialize(unbound_variables)
@@ -7,4 +9,11 @@ module Dentaku
7
9
  super("no value provided for variables: #{ unbound_variables.join(', ') }")
8
10
  end
9
11
  end
12
+
13
+ class ParseError < StandardError
14
+ end
15
+
16
+ class ZeroDivisionError < ::ZeroDivisionError
17
+ attr_accessor :recipient_variable
18
+ end
10
19
  end
@@ -54,6 +54,9 @@ module Dentaku
54
54
  operations.push op_class
55
55
  end
56
56
 
57
+ when :null
58
+ output.push AST::Nil.new
59
+
57
60
  when :function
58
61
  arities.push 0
59
62
  operations.push function(token)
@@ -102,7 +105,7 @@ module Dentaku
102
105
  end
103
106
 
104
107
  unless operations.count == 1 && operations.last == AST::Case
105
- fail "Unprocessed token #{ token.value }"
108
+ fail ParseError, "Unprocessed token #{ token.value }"
106
109
  end
107
110
  consume(arities.pop.succ)
108
111
  when :when
@@ -139,7 +142,7 @@ module Dentaku
139
142
 
140
143
  operations.push(AST::CaseElse)
141
144
  else
142
- fail "Unknown case token #{ token.value }"
145
+ fail ParseError, "Unknown case token #{ token.value }"
143
146
  end
144
147
 
145
148
  when :grouping
@@ -158,7 +161,7 @@ module Dentaku
158
161
  end
159
162
 
160
163
  lparen = operations.pop
161
- fail "Unbalanced parenthesis" unless lparen == AST::Grouping
164
+ fail ParseError, "Unbalanced parenthesis" unless lparen == AST::Grouping
162
165
 
163
166
  if operations.last && operations.last < AST::Function
164
167
  consume(arities.pop.succ)
@@ -171,11 +174,11 @@ module Dentaku
171
174
  end
172
175
 
173
176
  else
174
- fail "Unknown grouping token #{ token.value }"
177
+ fail ParseError, "Unknown grouping token #{ token.value }"
175
178
  end
176
179
 
177
180
  else
178
- fail "Not implemented for tokens of category #{ token.category }"
181
+ fail ParseError, "Not implemented for tokens of category #{ token.category }"
179
182
  end
180
183
  end
181
184
 
@@ -184,7 +187,7 @@ module Dentaku
184
187
  end
185
188
 
186
189
  unless output.count == 1
187
- fail "Parse error"
190
+ fail ParseError, "Invalid statement"
188
191
  end
189
192
 
190
193
  output.first
@@ -26,6 +26,7 @@ module Dentaku
26
26
  class << self
27
27
  def available_scanners
28
28
  [
29
+ :null,
29
30
  :whitespace,
30
31
  :numeric,
31
32
  :double_quoted_string,
@@ -68,6 +69,10 @@ module Dentaku
68
69
  new(:whitespace, '\s+')
69
70
  end
70
71
 
72
+ def null
73
+ new(:null, 'null\b')
74
+ end
75
+
71
76
  def numeric
72
77
  new(:numeric, '(\d+(\.\d+)?|\.\d+)\b', lambda { |raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
73
78
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "2.0.6"
2
+ VERSION = "2.0.7"
3
3
  end
@@ -17,7 +17,7 @@ describe Dentaku::AST::Addition do
17
17
  it 'requires numeric operands' do
18
18
  expect {
19
19
  described_class.new(five, t)
20
- }.to raise_error(RuntimeError, /requires numeric operands/)
20
+ }.to raise_error(Dentaku::ParseError, /requires numeric operands/)
21
21
 
22
22
  expression = Dentaku::AST::Multiplication.new(five, five)
23
23
  group = Dentaku::AST::Grouping.new(expression)
@@ -17,7 +17,7 @@ describe Dentaku::AST::And do
17
17
  it 'requires logical operands' do
18
18
  expect {
19
19
  described_class.new(t, five)
20
- }.to raise_error(RuntimeError, /requires logical operands/)
20
+ }.to raise_error(Dentaku::ParseError, /requires logical operands/)
21
21
 
22
22
  expression = Dentaku::AST::LessThanOrEqual.new(five, five)
23
23
  expect {
@@ -17,7 +17,7 @@ describe Dentaku::AST::Division do
17
17
  it 'requires numeric operands' do
18
18
  expect {
19
19
  described_class.new(five, t)
20
- }.to raise_error(RuntimeError, /requires numeric operands/)
20
+ }.to raise_error(Dentaku::ParseError, /requires numeric operands/)
21
21
 
22
22
  expression = Dentaku::AST::Multiplication.new(five, five)
23
23
  group = Dentaku::AST::Grouping.new(expression)
@@ -7,7 +7,9 @@ describe Dentaku::AST::Function do
7
7
  end
8
8
 
9
9
  it 'raises an exception when trying to access an undefined function' do
10
- expect { described_class.get("flarble") }.to raise_error(RuntimeError, /undefined function/i)
10
+ expect {
11
+ described_class.get("flarble")
12
+ }.to raise_error(Dentaku::ParseError, /undefined function/i)
11
13
  end
12
14
 
13
15
  it 'registers a custom function' do
@@ -27,7 +27,7 @@ RSpec.describe Dentaku::BulkExpressionSolver do
27
27
  expressions = {more_apples: "1/0"}
28
28
  expect {
29
29
  described_class.new(expressions, calculator).solve!
30
- }.to raise_error(ZeroDivisionError)
30
+ }.to raise_error(Dentaku::ZeroDivisionError)
31
31
  end
32
32
 
33
33
  it "does not require keys to be parseable" do
@@ -55,5 +55,23 @@ RSpec.describe Dentaku::BulkExpressionSolver do
55
55
  expect(described_class.new(expressions, calculator).solve { :foo })
56
56
  .to eq(more_apples: :foo)
57
57
  end
58
+
59
+ it 'stores the recipient variable on the exception when there is a div/0 error' do
60
+ expressions = {more_apples: "1/0"}
61
+ exception = nil
62
+ described_class.new(expressions, calculator).solve do |ex|
63
+ exception = ex
64
+ end
65
+ expect(exception.recipient_variable).to eq('more_apples')
66
+ end
67
+
68
+ it 'stores the recipient variable on the exception when there is an unbound variable' do
69
+ expressions = {more_apples: "apples + 1"}
70
+ exception = nil
71
+ described_class.new(expressions, calculator).solve do |ex|
72
+ exception = ex
73
+ end
74
+ expect(exception.recipient_variable).to eq('more_apples')
75
+ end
58
76
  end
59
77
  end
@@ -260,6 +260,28 @@ describe Dentaku::Calculator do
260
260
  end
261
261
  end
262
262
 
263
+ describe 'explicit NULL' do
264
+ it 'can be used in IF statements' do
265
+ expect(calculator.evaluate('IF(null, 1, 2)')).to eq(2)
266
+ end
267
+
268
+ it 'can be used in IF statements when passed in' do
269
+ expect(calculator.evaluate('IF(foo, 1, 2)', foo: nil)).to eq(2)
270
+ end
271
+
272
+ it 'nil values are carried across middle terms' do
273
+ results = calculator.solve!(
274
+ choice: 'IF(bar, 1, 2)',
275
+ bar: 'foo',
276
+ foo: nil)
277
+ expect(results).to eq(
278
+ choice: 2,
279
+ bar: nil,
280
+ foo: nil
281
+ )
282
+ end
283
+ end
284
+
263
285
  describe 'case statements' do
264
286
  it 'handles complex then statements' do
265
287
  formula = <<-FORMULA
@@ -123,4 +123,20 @@ describe Dentaku::Parser do
123
123
  case_close]).parse
124
124
  expect(node.value(x: 3)).to eq(4)
125
125
  end
126
+
127
+ it 'raises an error on parse failure' do
128
+ five = Dentaku::Token.new(:numeric, 5)
129
+ times = Dentaku::Token.new(:operator, :multiply)
130
+ minus = Dentaku::Token.new(:operator, :subtract)
131
+
132
+ expect {
133
+ described_class.new([five, times, minus]).parse
134
+ }.to raise_error(Dentaku::ParseError)
135
+ end
136
+
137
+ it "evaluates explicit 'NULL' as a Nil" do
138
+ null = Dentaku::Token.new(:null, nil)
139
+ node = described_class.new([null]).parse
140
+ expect(node.value).to eq(nil)
141
+ end
126
142
  end
@@ -28,7 +28,7 @@ describe Dentaku::TokenScanner do
28
28
  end
29
29
 
30
30
  it 'returns a list of all configured scanners' do
31
- expect(described_class.scanners.length).to eq 13
31
+ expect(described_class.scanners.length).to eq 14
32
32
  end
33
33
 
34
34
  it 'allows customizing available scanners' do
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.6
4
+ version: 2.0.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-01-26 00:00:00.000000000 Z
11
+ date: 2016-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -52,8 +52,7 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
- description: |2
56
- Dentaku is a parser and evaluator for mathematical formulas
55
+ description: " Dentaku is a parser and evaluator for mathematical formulas\n"
57
56
  email:
58
57
  - rubysolo@gmail.com
59
58
  executables: []
@@ -148,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
148
147
  version: '0'
149
148
  requirements: []
150
149
  rubyforge_project: dentaku
151
- rubygems_version: 2.4.5.1
150
+ rubygems_version: 2.5.1
152
151
  signing_key:
153
152
  specification_version: 4
154
153
  summary: A formula language parser and evaluator
@@ -172,3 +171,4 @@ test_files:
172
171
  - spec/token_scanner_spec.rb
173
172
  - spec/token_spec.rb
174
173
  - spec/tokenizer_spec.rb
174
+ has_rdoc: