dentaku 3.5.7 → 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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +2 -6
  3. data/.github/workflows/rubocop.yml +1 -1
  4. data/.rubocop.yml +1 -1
  5. data/CHANGELOG.md +91 -0
  6. data/Gemfile +14 -1
  7. data/README.md +87 -0
  8. data/dentaku.gemspec +14 -14
  9. data/lib/dentaku/ast/access.rb +18 -1
  10. data/lib/dentaku/ast/arithmetic.rb +5 -17
  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 +3 -7
  21. data/lib/dentaku/ast/function.rb +12 -13
  22. data/lib/dentaku/ast/function_registry.rb +11 -2
  23. data/lib/dentaku/ast/functions/abs.rb +1 -1
  24. data/lib/dentaku/ast/functions/and.rb +4 -4
  25. data/lib/dentaku/ast/functions/avg.rb +3 -3
  26. data/lib/dentaku/ast/functions/duration.rb +1 -1
  27. data/lib/dentaku/ast/functions/if.rb +9 -1
  28. data/lib/dentaku/ast/functions/intercept.rb +4 -4
  29. data/lib/dentaku/ast/functions/max.rb +1 -1
  30. data/lib/dentaku/ast/functions/min.rb +1 -1
  31. data/lib/dentaku/ast/functions/mul.rb +3 -3
  32. data/lib/dentaku/ast/functions/or.rb +4 -4
  33. data/lib/dentaku/ast/functions/pluck.rb +1 -1
  34. data/lib/dentaku/ast/functions/round.rb +1 -1
  35. data/lib/dentaku/ast/functions/rounddown.rb +2 -2
  36. data/lib/dentaku/ast/functions/roundup.rb +2 -2
  37. data/lib/dentaku/ast/functions/ruby_math.rb +5 -2
  38. data/lib/dentaku/ast/functions/string_functions.rb +5 -5
  39. data/lib/dentaku/ast/functions/sum.rb +3 -3
  40. data/lib/dentaku/ast/functions/xor.rb +4 -4
  41. data/lib/dentaku/ast/grouping.rb +6 -0
  42. data/lib/dentaku/ast/negation.rb +5 -0
  43. data/lib/dentaku/ast/node.rb +25 -0
  44. data/lib/dentaku/ast/operation.rb +7 -0
  45. data/lib/dentaku/bulk_expression_solver.rb +22 -16
  46. data/lib/dentaku/calculator.rb +45 -17
  47. data/lib/dentaku/date_arithmetic.rb +2 -2
  48. data/lib/dentaku/dependency_resolver.rb +12 -5
  49. data/lib/dentaku/exceptions.rb +105 -13
  50. data/lib/dentaku/numeric_parser.rb +68 -0
  51. data/lib/dentaku/parser.rb +24 -48
  52. data/lib/dentaku/print_visitor.rb +1 -0
  53. data/lib/dentaku/token_matcher.rb +31 -37
  54. data/lib/dentaku/token_matchers.rb +3 -3
  55. data/lib/dentaku/token_scanner.rb +3 -4
  56. data/lib/dentaku/tokenizer.rb +2 -16
  57. data/lib/dentaku/version.rb +1 -1
  58. data/spec/ast/case_spec.rb +7 -0
  59. data/spec/ast/function_spec.rb +11 -25
  60. data/spec/bulk_expression_solver_spec.rb +48 -3
  61. data/spec/calculator_spec.rb +77 -0
  62. data/spec/dependency_resolver_spec.rb +14 -0
  63. data/spec/exceptions_spec.rb +87 -0
  64. data/spec/gemspec_spec.rb +10 -0
  65. data/spec/identifiers_spec.rb +66 -0
  66. data/spec/numeric_parser_spec.rb +66 -0
  67. data/spec/parser_spec.rb +35 -0
  68. data/spec/print_visitor_spec.rb +5 -0
  69. data/spec/tokenizer_spec.rb +6 -0
  70. data/spec/volatile_functions_spec.rb +121 -0
  71. metadata +27 -150
  72. 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,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dentaku
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.7
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-12-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal
@@ -15,126 +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
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: '0'
40
- - !ruby/object:Gem::Dependency
41
- name: pry
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- type: :development
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- - !ruby/object:Gem::Dependency
55
- name: pry-byebug
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :development
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- - !ruby/object:Gem::Dependency
69
- name: pry-stack_explorer
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
- type: :development
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - ">="
80
- - !ruby/object:Gem::Version
81
- version: '0'
82
- - !ruby/object:Gem::Dependency
83
- name: rake
84
- requirement: !ruby/object:Gem::Requirement
85
- requirements:
86
- - - ">="
87
- - !ruby/object:Gem::Version
88
- version: '0'
89
- type: :development
90
- prerelease: false
91
- version_requirements: !ruby/object:Gem::Requirement
92
- requirements:
93
- - - ">="
94
- - !ruby/object:Gem::Version
95
- version: '0'
96
- - !ruby/object:Gem::Dependency
97
- name: rspec
98
- requirement: !ruby/object:Gem::Requirement
99
- requirements:
100
- - - ">="
101
- - !ruby/object:Gem::Version
102
- version: '0'
103
- type: :development
104
- prerelease: false
105
- version_requirements: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - ">="
108
- - !ruby/object:Gem::Version
109
- version: '0'
110
- - !ruby/object:Gem::Dependency
111
- name: rubocop
112
- requirement: !ruby/object:Gem::Requirement
113
- requirements:
114
- - - ">="
115
- - !ruby/object:Gem::Version
116
- version: '0'
117
- type: :development
118
- prerelease: false
119
- version_requirements: !ruby/object:Gem::Requirement
120
- requirements:
121
- - - ">="
37
+ - - "~>"
122
38
  - !ruby/object:Gem::Version
123
- version: '0'
39
+ version: '1.1'
124
40
  - !ruby/object:Gem::Dependency
125
- name: simplecov
41
+ name: tsort
126
42
  requirement: !ruby/object:Gem::Requirement
127
43
  requirements:
128
44
  - - ">="
129
45
  - !ruby/object:Gem::Version
130
- version: '0'
131
- type: :development
46
+ version: 0.1.1
47
+ type: :runtime
132
48
  prerelease: false
133
49
  version_requirements: !ruby/object:Gem::Requirement
134
50
  requirements:
135
51
  - - ">="
136
52
  - !ruby/object:Gem::Version
137
- version: '0'
53
+ version: 0.1.1
138
54
  description: " Dentaku is a parser and evaluator for mathematical formulas\n"
139
55
  email:
140
56
  - rubysolo@gmail.com
@@ -147,7 +63,6 @@ files:
147
63
  - ".gitignore"
148
64
  - ".pryrc"
149
65
  - ".rubocop.yml"
150
- - ".travis.yml"
151
66
  - CHANGELOG.md
152
67
  - Gemfile
153
68
  - LICENSE
@@ -214,6 +129,7 @@ files:
214
129
  - lib/dentaku/dependency_resolver.rb
215
130
  - lib/dentaku/exceptions.rb
216
131
  - lib/dentaku/flat_hash.rb
132
+ - lib/dentaku/numeric_parser.rb
217
133
  - lib/dentaku/parser.rb
218
134
  - lib/dentaku/print_visitor.rb
219
135
  - lib/dentaku/string_casing.rb
@@ -263,6 +179,9 @@ files:
263
179
  - spec/dependency_resolver_spec.rb
264
180
  - spec/exceptions_spec.rb
265
181
  - spec/external_function_spec.rb
182
+ - spec/gemspec_spec.rb
183
+ - spec/identifiers_spec.rb
184
+ - spec/numeric_parser_spec.rb
266
185
  - spec/parser_spec.rb
267
186
  - spec/print_visitor_spec.rb
268
187
  - spec/spec_helper.rb
@@ -272,10 +191,16 @@ files:
272
191
  - spec/tokenizer_spec.rb
273
192
  - spec/visitor/infix_spec.rb
274
193
  - spec/visitor_spec.rb
275
- homepage: http://github.com/rubysolo/dentaku
194
+ - spec/volatile_functions_spec.rb
195
+ homepage: https://github.com/rubysolo/dentaku
276
196
  licenses:
277
197
  - MIT
278
- 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'
279
204
  rdoc_options: []
280
205
  require_paths:
281
206
  - lib
@@ -283,62 +208,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
283
208
  requirements:
284
209
  - - ">="
285
210
  - !ruby/object:Gem::Version
286
- version: '0'
211
+ version: '3.2'
287
212
  required_rubygems_version: !ruby/object:Gem::Requirement
288
213
  requirements:
289
214
  - - ">="
290
215
  - !ruby/object:Gem::Version
291
216
  version: '0'
292
217
  requirements: []
293
- rubygems_version: 3.6.2
218
+ rubygems_version: 3.6.9
294
219
  specification_version: 4
295
220
  summary: A formula language parser and evaluator
296
- test_files:
297
- - spec/ast/abs_spec.rb
298
- - spec/ast/addition_spec.rb
299
- - spec/ast/all_spec.rb
300
- - spec/ast/and_function_spec.rb
301
- - spec/ast/and_spec.rb
302
- - spec/ast/any_spec.rb
303
- - spec/ast/arithmetic_spec.rb
304
- - spec/ast/avg_spec.rb
305
- - spec/ast/case_spec.rb
306
- - spec/ast/comparator_spec.rb
307
- - spec/ast/count_spec.rb
308
- - spec/ast/division_spec.rb
309
- - spec/ast/filter_spec.rb
310
- - spec/ast/function_spec.rb
311
- - spec/ast/intercept_spec.rb
312
- - spec/ast/map_spec.rb
313
- - spec/ast/max_spec.rb
314
- - spec/ast/min_spec.rb
315
- - spec/ast/mul_spec.rb
316
- - spec/ast/negation_spec.rb
317
- - spec/ast/node_spec.rb
318
- - spec/ast/numeric_spec.rb
319
- - spec/ast/or_spec.rb
320
- - spec/ast/pluck_spec.rb
321
- - spec/ast/reduce_spec.rb
322
- - spec/ast/round_spec.rb
323
- - spec/ast/rounddown_spec.rb
324
- - spec/ast/roundup_spec.rb
325
- - spec/ast/string_functions_spec.rb
326
- - spec/ast/sum_spec.rb
327
- - spec/ast/switch_spec.rb
328
- - spec/ast/xor_spec.rb
329
- - spec/benchmark.rb
330
- - spec/bulk_expression_solver_spec.rb
331
- - spec/calculator_spec.rb
332
- - spec/dentaku_spec.rb
333
- - spec/dependency_resolver_spec.rb
334
- - spec/exceptions_spec.rb
335
- - spec/external_function_spec.rb
336
- - spec/parser_spec.rb
337
- - spec/print_visitor_spec.rb
338
- - spec/spec_helper.rb
339
- - spec/token_matcher_spec.rb
340
- - spec/token_scanner_spec.rb
341
- - spec/token_spec.rb
342
- - spec/tokenizer_spec.rb
343
- - spec/visitor/infix_spec.rb
344
- - 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