dentaku 3.5.5 → 3.5.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42f22227c2d7aef33e71f5c177f65ca3e7855115466d4d04851e6286bcbe1955
4
- data.tar.gz: e779f142e5f458aed8437539bfe98ee73b4bf86a0ddb280cceec586ebcd62190
3
+ metadata.gz: fc1bbfde891d6c98eda08b9176f9ed791744b2043176f07d3aac997fae34a3e9
4
+ data.tar.gz: b8658f3d74364672f1a4538928e40d7b4d7aff7489273b573dbd1970303a60b3
5
5
  SHA512:
6
- metadata.gz: 8c4305e2d7f5c8289edcc5f530dbb0d98cbc95f6db59a8ba98561506f2ec7e41ef89c6ab7271d8754f0a0dfc5ade6325833269f38595593c39eca44c8dcce7c5
7
- data.tar.gz: 9a186d4164b031bbe27fb1490184d9c23387849267d4b1067c6bec6a39afdee4e6bf97d8bbbb364f36474fdad0e37e59f4ef63dae1454055244bc06f6791905b
6
+ metadata.gz: bc53fba03a05ddf47875ffd08aa54266b3f6894a91d292f18236aba4db4c4c270bfeb4f039f84f10d4ac7ce2ca0f6a86580d65c1f5a0169b74bd7c2dfacf9363
7
+ data.tar.gz: 88a65a6e103b40fbf3ef0dfc89173ced19e3df7f568514f4a12b451f7d4a91c69b1e25af89b5f9f1c9cadf34b71c200fc4a14ea11ef49aeeb2f2d4be6e36da2e
@@ -9,6 +9,20 @@ module Dentaku
9
9
  DECIMAL = /\A-?\d*\.\d+\z/.freeze
10
10
  INTEGER = /\A-?\d+\z/.freeze
11
11
 
12
+ def initialize(*)
13
+ super
14
+
15
+ unless valid_left?
16
+ raise NodeError.new(:incompatible, left.type, :left),
17
+ "#{self.class} requires operands that are numeric or compatible types, not #{left.type}"
18
+ end
19
+
20
+ unless valid_right?
21
+ raise NodeError.new(:incompatible, right.type, :right),
22
+ "#{self.class} requires operands that are numeric or compatible types, not #{right.type}"
23
+ end
24
+ end
25
+
12
26
  def type
13
27
  :numeric
14
28
  end
@@ -61,7 +75,19 @@ module Dentaku
61
75
  end
62
76
 
63
77
  def valid_node?(node)
64
- node && (node.type == :numeric || node.type == :integer || node.dependencies.any?)
78
+ return false unless node
79
+
80
+ # Allow nodes with dependencies (identifiers that will be resolved later)
81
+ return true if node.dependencies.any?
82
+
83
+ # Allow compatible types
84
+ return true if [:numeric, :integer, :array].include?(node.type)
85
+
86
+ # Allow nodes without a type (operations, groupings)
87
+ return true if node.type.nil?
88
+
89
+ # Reject incompatible types
90
+ false
65
91
  end
66
92
 
67
93
  def valid_left?
@@ -20,7 +20,7 @@ module Dentaku
20
20
  r = validate_value(cast(right.value(context)))
21
21
 
22
22
  l.public_send(operator, r)
23
- rescue ::ArgumentError => e
23
+ rescue ::ArgumentError, ::TypeError => e
24
24
  raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
25
25
  end
26
26
 
@@ -37,6 +37,7 @@ module Dentaku
37
37
  @arities = options.fetch(:arities, [])
38
38
  @function_registry = options.fetch(:function_registry, nil)
39
39
  @case_sensitive = options.fetch(:case_sensitive, false)
40
+ @skip_indices = []
40
41
  end
41
42
 
42
43
  def consume(count = 2)
@@ -82,19 +83,19 @@ module Dentaku
82
83
 
83
84
  i = 0
84
85
  while i < input.length
86
+ if @skip_indices.include?(i)
87
+ i += 1
88
+ next
89
+ end
85
90
  token = input[i]
86
91
  lookahead = input[i + 1]
87
92
  process_token(token, lookahead, i, input)
88
93
  i += 1
89
94
  end
90
95
 
91
- while operations.any?
92
- consume
93
- end
96
+ consume while operations.any?
94
97
 
95
- unless output.count == 1
96
- fail! :invalid_statement
97
- end
98
+ fail! :invalid_statement unless output.count == 1
98
99
 
99
100
  output.first
100
101
  end
@@ -127,7 +128,7 @@ module Dentaku
127
128
  when :function
128
129
  handle_function(token)
129
130
  when :case
130
- handle_case(token, index, tokens)
131
+ handle_case(token)
131
132
  when :access
132
133
  handle_access(token)
133
134
  when :array
@@ -142,13 +143,9 @@ module Dentaku
142
143
  def handle_operator(token, lookahead)
143
144
  op_class = operation(token).resolve_class(lookahead)
144
145
  if op_class.right_associative?
145
- while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
146
- consume
147
- end
146
+ consume while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
148
147
  else
149
- while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
150
- consume
151
- end
148
+ consume while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
152
149
  end
153
150
  operations.push op_class
154
151
  end
@@ -160,74 +157,40 @@ module Dentaku
160
157
  operations.push func
161
158
  end
162
159
 
163
- def handle_case(token, index, tokens)
164
- case_index = operations.index { |o| o == AST::Case } || -1
160
+ def handle_case(token)
161
+ # We always operate on the innermost (most recent) CASE on the stack.
162
+ case_index = operations.rindex(AST::Case) || -1
165
163
  token_index = case_index + 1
166
164
 
167
165
  case token.value
168
166
  when :open
169
- if operations.include?(AST::Case)
170
- # nested case extraction (non-recursive outer parser)
171
- open_cases = 0
172
- case_end_index = nil
173
- j = index + 1
174
- while j < tokens.length
175
- t = tokens[j]
176
- if t.category == :case
177
- if t.value == :open
178
- open_cases += 1
179
- elsif t.value == :close
180
- if open_cases > 0
181
- open_cases -= 1
182
- else
183
- case_end_index = j
184
- break
185
- end
186
- end
187
- end
188
- j += 1
189
- end
190
- inner_case_inputs = tokens.slice!(index + 1, case_end_index - index) || []
191
- subparser = Parser.new(
192
- inner_case_inputs,
193
- operations: [AST::Case],
194
- arities: [0],
195
- function_registry: @function_registry,
196
- case_sensitive: case_sensitive
197
- )
198
- subparser.parse
199
- output.concat(subparser.output)
200
- else
201
- operations.push AST::Case
202
- arities.push(0)
203
- end
167
+ # Start a new CASE context.
168
+ operations.push AST::Case
169
+ arities.push(0)
204
170
 
205
171
  when :close
172
+ # Finalize any trailing THEN/ELSE expression still on the stack.
206
173
  if operations[token_index] == AST::CaseThen
207
- while operations.last != AST::Case
208
- consume
209
- end
174
+ consume_until(AST::Case)
210
175
  operations.push(AST::CaseConditional)
211
176
  consume(2)
212
177
  arities[-1] += 1
213
178
  elsif operations[token_index] == AST::CaseElse
214
- while operations.last != AST::Case
215
- consume
216
- end
179
+ consume_until(AST::Case)
217
180
  arities[-1] += 1
218
181
  end
219
- fail! :unprocessed_token, token_name: token.value unless operations.count >= 1 && operations.last == AST::Case
182
+ fail! :unprocessed_token, token_name: token.value unless operations.last == AST::Case
220
183
  consume(arities.pop.succ)
221
184
 
222
185
  when :when
223
186
  if operations[token_index] == AST::CaseThen
224
- while ![AST::CaseWhen, AST::Case].include?(operations.last)
225
- consume
226
- end
187
+ # Close out previous WHEN/THEN pair.
188
+ consume_until([AST::CaseWhen, AST::Case])
227
189
  operations.push(AST::CaseConditional)
228
190
  consume(2)
229
191
  arities[-1] += 1
230
192
  elsif operations.last == AST::Case
193
+ # First WHEN: finalize switch variable expression.
231
194
  operations.push(AST::CaseSwitchVariable)
232
195
  consume
233
196
  end
@@ -235,36 +198,41 @@ module Dentaku
235
198
 
236
199
  when :then
237
200
  if operations[token_index] == AST::CaseWhen
238
- while ![AST::CaseThen, AST::Case].include?(operations.last)
239
- consume
240
- end
201
+ consume_until([AST::CaseThen, AST::Case])
241
202
  end
242
203
  operations.push(AST::CaseThen)
243
204
 
244
205
  when :else
245
206
  if operations[token_index] == AST::CaseThen
246
- while operations.last != AST::Case
247
- consume
248
- end
207
+ consume_until(AST::Case)
249
208
  operations.push(AST::CaseConditional)
250
209
  consume(2)
251
210
  arities[-1] += 1
252
211
  end
253
212
  operations.push(AST::CaseElse)
213
+
254
214
  else
255
215
  fail! :unknown_case_token, token_name: token.value
256
216
  end
257
217
  end
258
218
 
219
+ def consume_until(target)
220
+ matcher =
221
+ case target
222
+ when Array then ->(op) { target.include?(op) }
223
+ else ->(op) { op == target }
224
+ end
225
+
226
+ consume while operations.any? && !matcher.call(operations.last)
227
+ end
228
+
259
229
  def handle_access(token)
260
230
  case token.value
261
231
  when :lbracket
262
232
  operations.push AST::Access
263
233
 
264
234
  when :rbracket
265
- while operations.any? && operations.last != AST::Access
266
- consume
267
- end
235
+ consume while operations.any? && operations.last != AST::Access
268
236
  fail! :unbalanced_bracket, token: token unless operations.last == AST::Access
269
237
  consume
270
238
  end
@@ -277,9 +245,7 @@ module Dentaku
277
245
  arities.push 0
278
246
 
279
247
  when :array_end
280
- while operations.any? && operations.last != AST::Array
281
- consume
282
- end
248
+ consume while operations.any? && operations.last != AST::Array
283
249
  fail! :unbalanced_bracket, token: token unless operations.last == AST::Array
284
250
  consume(arities.pop.succ)
285
251
  end
@@ -290,7 +256,9 @@ module Dentaku
290
256
  when :open
291
257
  if lookahead && lookahead.value == :close
292
258
  # empty grouping (e.g. function with zero arguments) — we trigger consume later
293
- tokens.delete_at(tokens.index(lookahead)) # remove the close to mimic previous shift behavior
259
+ # skip to the end
260
+ lookahead_index = tokens.index(lookahead)
261
+ @skip_indices << lookahead_index if lookahead_index
294
262
  arities.pop
295
263
  consume(0)
296
264
  else
@@ -298,9 +266,7 @@ module Dentaku
298
266
  end
299
267
 
300
268
  when :close
301
- while operations.any? && operations.last != AST::Grouping
302
- consume
303
- end
269
+ consume while operations.any? && operations.last != AST::Grouping
304
270
  lparen = operations.pop
305
271
  fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
306
272
  if operations.last && operations.last < AST::Function
@@ -310,9 +276,8 @@ module Dentaku
310
276
  when :comma
311
277
  fail! :invalid_statement if arities.empty?
312
278
  arities[-1] += 1
313
- while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
314
- consume
315
- end
279
+ consume while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
280
+
316
281
  else
317
282
  fail! :unknown_grouping_token, token_name: token.value
318
283
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.5"
2
+ VERSION = "3.5.6"
3
3
  end
@@ -22,14 +22,14 @@ describe Dentaku::AST::Addition do
22
22
 
23
23
  it 'requires operands that respond to +' do
24
24
  expect {
25
- described_class.new(five, t).value
26
- }.to raise_error(Dentaku::ArgumentError, /requires operands that respond to +/)
25
+ described_class.new(five, t)
26
+ }.to raise_error(Dentaku::NodeError, /requires operands/)
27
27
 
28
28
  expression = Dentaku::AST::Multiplication.new(five, five)
29
29
  group = Dentaku::AST::Grouping.new(expression)
30
30
 
31
31
  expect {
32
- described_class.new(group, five).value
32
+ described_class.new(group, five)
33
33
  }.not_to raise_error
34
34
  end
35
35
 
@@ -22,14 +22,14 @@ describe Dentaku::AST::Division do
22
22
 
23
23
  it 'requires operands that respond to /' do
24
24
  expect {
25
- described_class.new(five, t).value
26
- }.to raise_error(Dentaku::ArgumentError, /requires operands that respond to \//)
25
+ described_class.new(five, t)
26
+ }.to raise_error(Dentaku::NodeError, /requires operands/)
27
27
 
28
28
  expression = Dentaku::AST::Multiplication.new(five, five)
29
29
  group = Dentaku::AST::Grouping.new(expression)
30
30
 
31
31
  expect {
32
- described_class.new(group, five).value
32
+ described_class.new(group, five)
33
33
  }.not_to raise_error
34
34
  end
35
35
 
@@ -130,6 +130,7 @@ describe Dentaku::Calculator do
130
130
  expect { calculator.evaluate!('"foo" & "bar"') }.to raise_error(Dentaku::ArgumentError)
131
131
  expect { calculator.evaluate!('1.0 & "bar"') }.to raise_error(Dentaku::ArgumentError)
132
132
  expect { calculator.evaluate!('1 & "bar"') }.to raise_error(Dentaku::ArgumentError)
133
+ expect { calculator.evaluate!('data < 1', data: { a: 5 }) }.to raise_error(Dentaku::ArgumentError)
133
134
  end
134
135
 
135
136
  it 'raises argument error if a function is called with incorrect arity' do
@@ -235,6 +236,12 @@ describe Dentaku::Calculator do
235
236
  expect(calculator.dependencies('MAP(vals, val, val + step)')).to eq(['vals', 'step'])
236
237
  expect(calculator.dependencies('ALL(people, person, person.age < adult)')).to eq(['people', 'adult'])
237
238
  end
239
+
240
+ it "raises an error when trying to find dependencies with invalid syntax" do
241
+ expect { calculator.dependencies('bob + / 3') }.to raise_error(Dentaku::ParseError)
242
+ expect { calculator.dependencies('123 * TRUE') }.to raise_error(Dentaku::ParseError)
243
+ expect { calculator.dependencies('4 + "asdf"') }.to raise_error(Dentaku::ParseError)
244
+ end
238
245
  end
239
246
 
240
247
  describe 'solve!' do
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.5
4
+ version: 3.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-08-21 00:00:00.000000000 Z
10
+ date: 2025-10-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal