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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +2 -4
  3. data/.github/workflows/rubocop.yml +1 -1
  4. data/.rubocop.yml +1 -1
  5. data/CHANGELOG.md +74 -1
  6. data/Gemfile +1 -1
  7. data/README.md +87 -0
  8. data/dentaku.gemspec +15 -8
  9. data/lib/dentaku/ast/access.rb +18 -1
  10. data/lib/dentaku/ast/arithmetic.rb +3 -4
  11. data/lib/dentaku/ast/array.rb +6 -0
  12. data/lib/dentaku/ast/bitwise.rb +6 -4
  13. data/lib/dentaku/ast/case/case_conditional.rb +6 -0
  14. data/lib/dentaku/ast/case/case_else.rb +6 -0
  15. data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
  16. data/lib/dentaku/ast/case/case_then.rb +6 -0
  17. data/lib/dentaku/ast/case/case_when.rb +7 -0
  18. data/lib/dentaku/ast/case.rb +30 -3
  19. data/lib/dentaku/ast/combinators.rb +49 -4
  20. data/lib/dentaku/ast/comparators.rb +2 -3
  21. data/lib/dentaku/ast/function.rb +15 -2
  22. data/lib/dentaku/ast/function_registry.rb +10 -1
  23. data/lib/dentaku/ast/functions/and.rb +4 -4
  24. data/lib/dentaku/ast/functions/avg.rb +2 -2
  25. data/lib/dentaku/ast/functions/duration.rb +1 -1
  26. data/lib/dentaku/ast/functions/if.rb +9 -1
  27. data/lib/dentaku/ast/functions/intercept.rb +2 -2
  28. data/lib/dentaku/ast/functions/mul.rb +2 -2
  29. data/lib/dentaku/ast/functions/or.rb +4 -4
  30. data/lib/dentaku/ast/functions/pluck.rb +1 -1
  31. data/lib/dentaku/ast/functions/string_functions.rb +1 -1
  32. data/lib/dentaku/ast/functions/sum.rb +2 -2
  33. data/lib/dentaku/ast/functions/xor.rb +4 -4
  34. data/lib/dentaku/ast/grouping.rb +6 -0
  35. data/lib/dentaku/ast/negation.rb +5 -0
  36. data/lib/dentaku/ast/node.rb +25 -0
  37. data/lib/dentaku/ast/operation.rb +7 -0
  38. data/lib/dentaku/bulk_expression_solver.rb +9 -10
  39. data/lib/dentaku/calculator.rb +45 -17
  40. data/lib/dentaku/date_arithmetic.rb +2 -2
  41. data/lib/dentaku/exceptions.rb +105 -13
  42. data/lib/dentaku/numeric_parser.rb +1 -1
  43. data/lib/dentaku/parser.rb +24 -48
  44. data/lib/dentaku/token_matcher.rb +31 -37
  45. data/lib/dentaku/token_matchers.rb +3 -3
  46. data/lib/dentaku/tokenizer.rb +1 -15
  47. data/lib/dentaku/version.rb +1 -1
  48. data/spec/ast/case_spec.rb +7 -0
  49. data/spec/bulk_expression_solver_spec.rb +13 -3
  50. data/spec/calculator_spec.rb +67 -0
  51. data/spec/exceptions_spec.rb +87 -0
  52. data/spec/gemspec_spec.rb +10 -0
  53. data/spec/identifiers_spec.rb +66 -0
  54. data/spec/parser_spec.rb +35 -0
  55. data/spec/print_visitor_spec.rb +5 -0
  56. data/spec/volatile_functions_spec.rb +121 -0
  57. metadata +21 -63
  58. 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: 3.5.8
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: '0'
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: '0'
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: '0'
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: '0'
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: '0'
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: '0'
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
- homepage: http://github.com/rubysolo/dentaku
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: '0'
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: []
data/.travis.yml DELETED
@@ -1,10 +0,0 @@
1
- language: ruby
2
- sudo: false
3
- rvm:
4
- - 2.5.9
5
- - 2.6.7
6
- - 2.7.3
7
- - 3.0.1
8
- before_install:
9
- - gem update bundler
10
- - gem update --system