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 +4 -4
- data/CHANGELOG.md +7 -1
- data/lib/dentaku/ast/arithmetic.rb +5 -3
- data/lib/dentaku/ast/combinators.rb +3 -1
- data/lib/dentaku/ast/function.rb +3 -1
- data/lib/dentaku/ast/identifier.rb +4 -3
- data/lib/dentaku/ast/negation.rb +2 -2
- data/lib/dentaku/bulk_expression_solver.rb +13 -2
- data/lib/dentaku/exceptions.rb +9 -0
- data/lib/dentaku/parser.rb +9 -6
- data/lib/dentaku/token_scanner.rb +5 -0
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/addition_spec.rb +1 -1
- data/spec/ast/and_spec.rb +1 -1
- data/spec/ast/division_spec.rb +1 -1
- data/spec/ast/function_spec.rb +3 -1
- data/spec/bulk_expression_solver_spec.rb +19 -1
- data/spec/calculator_spec.rb +22 -0
- data/spec/parser_spec.rb +16 -0
- data/spec/token_scanner_spec.rb +1 -1
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c299535113d559ed61d819be474ced8128bf7e0f
|
4
|
+
data.tar.gz: 80aa950604c573fcbee95cfa6bb820a1fa6af94b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 616133f23a73885b22d1e53722d8482f9522e249ca03a76fe7a8685d1d365eebbde7b1677248ea6758cbf29e60f54504f9c4a4a9ee43735ab54cffc8cb9d11ac
|
7
|
+
data.tar.gz: aea6a8919d66c838c704ba09b19787ddeb301f72ead03883ffc481ffe4ff7a54afa92108ed73794feaec4d83ea93cc17476dc108d0273401dba9c6077a603ec0
|
data/CHANGELOG.md
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
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
|
data/lib/dentaku/ast/function.rb
CHANGED
@@ -12,7 +12,9 @@ module Dentaku
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def self.get(name)
|
15
|
-
registry.fetch(function_name(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
|
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
|
data/lib/dentaku/ast/negation.rb
CHANGED
@@ -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
|
-
|
47
|
-
|
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
|
data/lib/dentaku/exceptions.rb
CHANGED
@@ -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
|
data/lib/dentaku/parser.rb
CHANGED
@@ -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 "
|
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
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/addition_spec.rb
CHANGED
@@ -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(
|
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)
|
data/spec/ast/and_spec.rb
CHANGED
@@ -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(
|
20
|
+
}.to raise_error(Dentaku::ParseError, /requires logical operands/)
|
21
21
|
|
22
22
|
expression = Dentaku::AST::LessThanOrEqual.new(five, five)
|
23
23
|
expect {
|
data/spec/ast/division_spec.rb
CHANGED
@@ -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(
|
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)
|
data/spec/ast/function_spec.rb
CHANGED
@@ -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 {
|
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
|
data/spec/calculator_spec.rb
CHANGED
@@ -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
|
data/spec/parser_spec.rb
CHANGED
@@ -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
|
data/spec/token_scanner_spec.rb
CHANGED
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.
|
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-
|
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:
|
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.
|
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:
|