dentaku 2.0.6 → 2.0.7

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