dentaku 3.5.8 → 4.0.0
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/.github/workflows/rspec.yml +2 -4
- data/.github/workflows/rubocop.yml +1 -1
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +74 -1
- data/Gemfile +1 -1
- data/README.md +87 -0
- data/dentaku.gemspec +15 -8
- data/lib/dentaku/ast/access.rb +18 -1
- data/lib/dentaku/ast/arithmetic.rb +3 -4
- data/lib/dentaku/ast/array.rb +6 -0
- data/lib/dentaku/ast/bitwise.rb +6 -4
- data/lib/dentaku/ast/case/case_conditional.rb +6 -0
- data/lib/dentaku/ast/case/case_else.rb +6 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
- data/lib/dentaku/ast/case/case_then.rb +6 -0
- data/lib/dentaku/ast/case/case_when.rb +7 -0
- data/lib/dentaku/ast/case.rb +30 -3
- data/lib/dentaku/ast/combinators.rb +49 -4
- data/lib/dentaku/ast/comparators.rb +2 -3
- data/lib/dentaku/ast/function.rb +15 -2
- data/lib/dentaku/ast/function_registry.rb +10 -1
- data/lib/dentaku/ast/functions/and.rb +4 -4
- data/lib/dentaku/ast/functions/avg.rb +2 -2
- data/lib/dentaku/ast/functions/duration.rb +1 -1
- data/lib/dentaku/ast/functions/if.rb +9 -1
- data/lib/dentaku/ast/functions/intercept.rb +2 -2
- data/lib/dentaku/ast/functions/mul.rb +2 -2
- data/lib/dentaku/ast/functions/or.rb +4 -4
- data/lib/dentaku/ast/functions/pluck.rb +1 -1
- data/lib/dentaku/ast/functions/string_functions.rb +1 -1
- data/lib/dentaku/ast/functions/sum.rb +2 -2
- data/lib/dentaku/ast/functions/xor.rb +4 -4
- data/lib/dentaku/ast/grouping.rb +6 -0
- data/lib/dentaku/ast/negation.rb +5 -0
- data/lib/dentaku/ast/node.rb +25 -0
- data/lib/dentaku/ast/operation.rb +7 -0
- data/lib/dentaku/bulk_expression_solver.rb +9 -10
- data/lib/dentaku/calculator.rb +45 -17
- data/lib/dentaku/date_arithmetic.rb +2 -2
- data/lib/dentaku/exceptions.rb +105 -13
- data/lib/dentaku/numeric_parser.rb +1 -1
- data/lib/dentaku/parser.rb +24 -48
- data/lib/dentaku/token_matcher.rb +31 -37
- data/lib/dentaku/token_matchers.rb +3 -3
- data/lib/dentaku/tokenizer.rb +1 -15
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/case_spec.rb +7 -0
- data/spec/bulk_expression_solver_spec.rb +13 -3
- data/spec/calculator_spec.rb +67 -0
- data/spec/exceptions_spec.rb +87 -0
- data/spec/gemspec_spec.rb +10 -0
- data/spec/identifiers_spec.rb +66 -0
- data/spec/parser_spec.rb +35 -0
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/volatile_functions_spec.rb +121 -0
- metadata +21 -63
- data/.travis.yml +0 -10
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku'
|
|
3
|
+
require 'dentaku/calculator'
|
|
4
|
+
|
|
5
|
+
describe Dentaku::Calculator do
|
|
6
|
+
describe 'volatile functions' do
|
|
7
|
+
let(:calculator) { described_class.new }
|
|
8
|
+
let(:call_count) { { count: 0 } }
|
|
9
|
+
|
|
10
|
+
def register_counter(volatile:)
|
|
11
|
+
counter = call_count
|
|
12
|
+
calculator.add_function(:vol, :numeric, ->(x) { counter[:count] += 1; x }, volatile: volatile)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
describe 'registration' do
|
|
16
|
+
it 'marks generated function classes volatile' do
|
|
17
|
+
registry = Dentaku::AST::FunctionRegistry.new
|
|
18
|
+
registry.register(:vol, :numeric, ->(x) { x }, volatile: true)
|
|
19
|
+
expect(registry.get(:vol).volatile?).to be true
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'defaults to non-volatile' do
|
|
23
|
+
registry = Dentaku::AST::FunctionRegistry.new
|
|
24
|
+
registry.register(:plain, :numeric, ->(x) { x })
|
|
25
|
+
expect(registry.get(:plain).volatile?).to be_falsy
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'reports built-in function classes as non-volatile' do
|
|
29
|
+
expect(Dentaku::AST::If.volatile?).to be false
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'accepts a fifth tuple element in add_functions' do
|
|
33
|
+
calculator.add_functions([
|
|
34
|
+
[:vol5, :numeric, ->(x) { x }, nil, true],
|
|
35
|
+
[:plain5, :numeric, ->(x) { x }],
|
|
36
|
+
])
|
|
37
|
+
expect(calculator.dependencies('IF(vol5(1) > 0, a, b)')).to eq(%w(a b))
|
|
38
|
+
expect(calculator.dependencies('IF(plain5(1) > 0, a, b)')).to eq(['a'])
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
describe 'dependency resolution' do
|
|
43
|
+
it 'does not evaluate a volatile IF predicate and returns both branches' do
|
|
44
|
+
register_counter(volatile: true)
|
|
45
|
+
expect(calculator.dependencies('IF(vol(1) > 0, a, b)')).to eq(%w(a b))
|
|
46
|
+
expect(call_count[:count]).to eq(0)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'still prunes when the predicate uses a pure custom function' do
|
|
50
|
+
register_counter(volatile: false)
|
|
51
|
+
expect(calculator.dependencies('IF(vol(1) > 0, a, b)')).to eq(['a'])
|
|
52
|
+
expect(call_count[:count]).to eq(1)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'treats volatility as recursive through the guard subtree' do
|
|
56
|
+
register_counter(volatile: true)
|
|
57
|
+
expect(calculator.dependencies('IF(1 + vol(1) > 2, a, b)')).to eq(%w(a b))
|
|
58
|
+
expect(call_count[:count]).to eq(0)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'does not prune CASE with a volatile switch' do
|
|
62
|
+
register_counter(volatile: true)
|
|
63
|
+
deps = calculator.dependencies('CASE vol(1) WHEN 1 THEN a ELSE b END')
|
|
64
|
+
expect(deps).to eq(%w(a b))
|
|
65
|
+
expect(call_count[:count]).to eq(0)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it 'does not prune CASE with a volatile when-clause' do
|
|
69
|
+
register_counter(volatile: true)
|
|
70
|
+
deps = calculator.dependencies('CASE 1 WHEN vol(1) THEN a ELSE b END')
|
|
71
|
+
expect(deps).to eq(%w(a b))
|
|
72
|
+
expect(call_count[:count]).to eq(0)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'does not treat a volatile AND operand as decisive' do
|
|
76
|
+
register_counter(volatile: true)
|
|
77
|
+
expect(calculator.dependencies('vol(0) > 1 AND x')).to eq(['x'])
|
|
78
|
+
expect(call_count[:count]).to eq(0)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it 'still prunes AND on a pure decisive operand without running the volatile side' do
|
|
82
|
+
register_counter(volatile: true)
|
|
83
|
+
expect(calculator.dependencies('1 > 2 AND vol(1) > 0')).to eq([])
|
|
84
|
+
expect(call_count[:count]).to eq(0)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
describe 'evaluation' do
|
|
89
|
+
it 'evaluates a volatile predicate exactly once per evaluate!' do
|
|
90
|
+
register_counter(volatile: true)
|
|
91
|
+
expect(calculator.evaluate!('IF(vol(2) > 1, a, b)', a: 1, b: 2)).to eq(1)
|
|
92
|
+
expect(call_count[:count]).to eq(1)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'requires variables from both branches when the predicate is volatile' do
|
|
96
|
+
register_counter(volatile: true)
|
|
97
|
+
expect {
|
|
98
|
+
calculator.evaluate!('IF(vol(2) > 1, a, b)', a: 1)
|
|
99
|
+
}.to raise_error(Dentaku::UnboundVariableError) { |error|
|
|
100
|
+
expect(error.unbound_variables).to eq(['b'])
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
it 'does not change runtime short-circuit behavior for direct AST evaluation' do
|
|
105
|
+
register_counter(volatile: true)
|
|
106
|
+
node = calculator.ast('IF(vol(2) > 1, a, b)')
|
|
107
|
+
expect(node.value('a' => 1)).to eq(1)
|
|
108
|
+
expect(call_count[:count]).to eq(1)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
describe 'solve' do
|
|
113
|
+
it 'resolves expressions with volatile guards when all branch inputs are provided' do
|
|
114
|
+
register_counter(volatile: true)
|
|
115
|
+
calculator.store(a: 1, b: 2)
|
|
116
|
+
result = calculator.solve!(c: 'IF(vol(2) > 1, a, b)', d: 'c + 1')
|
|
117
|
+
expect(result[:d]).to eq(2)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dentaku
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Solomon White
|
|
@@ -15,42 +15,42 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - ">="
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: '
|
|
18
|
+
version: '3.1'
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: '
|
|
25
|
+
version: '3.1'
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: concurrent-ruby
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
30
|
+
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '
|
|
32
|
+
version: '1.1'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
|
-
- - "
|
|
37
|
+
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '
|
|
39
|
+
version: '1.1'
|
|
40
40
|
- !ruby/object:Gem::Dependency
|
|
41
41
|
name: tsort
|
|
42
42
|
requirement: !ruby/object:Gem::Requirement
|
|
43
43
|
requirements:
|
|
44
44
|
- - ">="
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version:
|
|
46
|
+
version: 0.1.1
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - ">="
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version:
|
|
53
|
+
version: 0.1.1
|
|
54
54
|
description: " Dentaku is a parser and evaluator for mathematical formulas\n"
|
|
55
55
|
email:
|
|
56
56
|
- rubysolo@gmail.com
|
|
@@ -63,7 +63,6 @@ files:
|
|
|
63
63
|
- ".gitignore"
|
|
64
64
|
- ".pryrc"
|
|
65
65
|
- ".rubocop.yml"
|
|
66
|
-
- ".travis.yml"
|
|
67
66
|
- CHANGELOG.md
|
|
68
67
|
- Gemfile
|
|
69
68
|
- LICENSE
|
|
@@ -180,6 +179,8 @@ files:
|
|
|
180
179
|
- spec/dependency_resolver_spec.rb
|
|
181
180
|
- spec/exceptions_spec.rb
|
|
182
181
|
- spec/external_function_spec.rb
|
|
182
|
+
- spec/gemspec_spec.rb
|
|
183
|
+
- spec/identifiers_spec.rb
|
|
183
184
|
- spec/numeric_parser_spec.rb
|
|
184
185
|
- spec/parser_spec.rb
|
|
185
186
|
- spec/print_visitor_spec.rb
|
|
@@ -190,10 +191,16 @@ files:
|
|
|
190
191
|
- spec/tokenizer_spec.rb
|
|
191
192
|
- spec/visitor/infix_spec.rb
|
|
192
193
|
- spec/visitor_spec.rb
|
|
193
|
-
|
|
194
|
+
- spec/volatile_functions_spec.rb
|
|
195
|
+
homepage: https://github.com/rubysolo/dentaku
|
|
194
196
|
licenses:
|
|
195
197
|
- MIT
|
|
196
|
-
metadata:
|
|
198
|
+
metadata:
|
|
199
|
+
homepage_uri: https://github.com/rubysolo/dentaku
|
|
200
|
+
source_code_uri: https://github.com/rubysolo/dentaku
|
|
201
|
+
changelog_uri: https://github.com/rubysolo/dentaku/blob/main/CHANGELOG.md
|
|
202
|
+
bug_tracker_uri: https://github.com/rubysolo/dentaku/issues
|
|
203
|
+
rubygems_mfa_required: 'true'
|
|
197
204
|
rdoc_options: []
|
|
198
205
|
require_paths:
|
|
199
206
|
- lib
|
|
@@ -201,7 +208,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
201
208
|
requirements:
|
|
202
209
|
- - ">="
|
|
203
210
|
- !ruby/object:Gem::Version
|
|
204
|
-
version: '
|
|
211
|
+
version: '3.2'
|
|
205
212
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
206
213
|
requirements:
|
|
207
214
|
- - ">="
|
|
@@ -211,53 +218,4 @@ requirements: []
|
|
|
211
218
|
rubygems_version: 3.6.9
|
|
212
219
|
specification_version: 4
|
|
213
220
|
summary: A formula language parser and evaluator
|
|
214
|
-
test_files:
|
|
215
|
-
- spec/ast/abs_spec.rb
|
|
216
|
-
- spec/ast/addition_spec.rb
|
|
217
|
-
- spec/ast/all_spec.rb
|
|
218
|
-
- spec/ast/and_function_spec.rb
|
|
219
|
-
- spec/ast/and_spec.rb
|
|
220
|
-
- spec/ast/any_spec.rb
|
|
221
|
-
- spec/ast/arithmetic_spec.rb
|
|
222
|
-
- spec/ast/avg_spec.rb
|
|
223
|
-
- spec/ast/case_spec.rb
|
|
224
|
-
- spec/ast/comparator_spec.rb
|
|
225
|
-
- spec/ast/count_spec.rb
|
|
226
|
-
- spec/ast/division_spec.rb
|
|
227
|
-
- spec/ast/filter_spec.rb
|
|
228
|
-
- spec/ast/function_spec.rb
|
|
229
|
-
- spec/ast/intercept_spec.rb
|
|
230
|
-
- spec/ast/map_spec.rb
|
|
231
|
-
- spec/ast/max_spec.rb
|
|
232
|
-
- spec/ast/min_spec.rb
|
|
233
|
-
- spec/ast/mul_spec.rb
|
|
234
|
-
- spec/ast/negation_spec.rb
|
|
235
|
-
- spec/ast/node_spec.rb
|
|
236
|
-
- spec/ast/numeric_spec.rb
|
|
237
|
-
- spec/ast/or_spec.rb
|
|
238
|
-
- spec/ast/pluck_spec.rb
|
|
239
|
-
- spec/ast/reduce_spec.rb
|
|
240
|
-
- spec/ast/round_spec.rb
|
|
241
|
-
- spec/ast/rounddown_spec.rb
|
|
242
|
-
- spec/ast/roundup_spec.rb
|
|
243
|
-
- spec/ast/string_functions_spec.rb
|
|
244
|
-
- spec/ast/sum_spec.rb
|
|
245
|
-
- spec/ast/switch_spec.rb
|
|
246
|
-
- spec/ast/xor_spec.rb
|
|
247
|
-
- spec/benchmark.rb
|
|
248
|
-
- spec/bulk_expression_solver_spec.rb
|
|
249
|
-
- spec/calculator_spec.rb
|
|
250
|
-
- spec/dentaku_spec.rb
|
|
251
|
-
- spec/dependency_resolver_spec.rb
|
|
252
|
-
- spec/exceptions_spec.rb
|
|
253
|
-
- spec/external_function_spec.rb
|
|
254
|
-
- spec/numeric_parser_spec.rb
|
|
255
|
-
- spec/parser_spec.rb
|
|
256
|
-
- spec/print_visitor_spec.rb
|
|
257
|
-
- spec/spec_helper.rb
|
|
258
|
-
- spec/token_matcher_spec.rb
|
|
259
|
-
- spec/token_scanner_spec.rb
|
|
260
|
-
- spec/token_spec.rb
|
|
261
|
-
- spec/tokenizer_spec.rb
|
|
262
|
-
- spec/visitor/infix_spec.rb
|
|
263
|
-
- spec/visitor_spec.rb
|
|
221
|
+
test_files: []
|