dentaku 3.5.5 → 3.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 42f22227c2d7aef33e71f5c177f65ca3e7855115466d4d04851e6286bcbe1955
4
- data.tar.gz: e779f142e5f458aed8437539bfe98ee73b4bf86a0ddb280cceec586ebcd62190
3
+ metadata.gz: 6992ab2a13558e752be1db54e612dba4230c8e273ebb4bc11b51d655435ab463
4
+ data.tar.gz: fe53f64d1015b13ec909382679e3dbc93ec822fb97d8389a3bd83870fe2dacea
5
5
  SHA512:
6
- metadata.gz: 8c4305e2d7f5c8289edcc5f530dbb0d98cbc95f6db59a8ba98561506f2ec7e41ef89c6ab7271d8754f0a0dfc5ade6325833269f38595593c39eca44c8dcce7c5
7
- data.tar.gz: 9a186d4164b031bbe27fb1490184d9c23387849267d4b1067c6bec6a39afdee4e6bf97d8bbbb364f36474fdad0e37e59f4ef63dae1454055244bc06f6791905b
6
+ metadata.gz: cad7fc2694c626beb952b44c5014bda140ef5e7953de85e333e8482b35ac02ce87f8054e2e752f9f47a0ae2c5af7666224803d28b484c7c2defaa11aa782521f
7
+ data.tar.gz: 53a267501045fce0f6f4cb26bfb7308e65a9b6cb1a1e8d4386cafffbbb865976b0c320fae2553da30b07193a9126be05068c2b7ba7c5034a341698733cf88305
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Change Log
2
2
 
3
+ ## [v3.5.7] 2025-12-16
4
+ - fix misclassification of unary minus as subtraction
5
+ - fix parsing empty function call
6
+
7
+ ## [v3.5.6] 2025-10-20
8
+ - fix comparison of Hash with integer
9
+ - refactor case parsing
10
+ - remove input mutation
11
+ - fix bug with arithmetic node validation
12
+
3
13
  ## [v3.5.5] 2025-08-20
4
14
  - fix percentages in print visitor
5
15
  - repo cleanup
@@ -267,6 +277,7 @@
267
277
  ## [v0.1.0] 2012-01-20
268
278
  - initial release
269
279
 
280
+ [v3.5.6]: https://github.com/rubysolo/dentaku/compare/v3.5.5...v3.5.6
270
281
  [v3.5.5]: https://github.com/rubysolo/dentaku/compare/v3.5.4...v3.5.5
271
282
  [v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
272
283
  [v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
@@ -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,20 @@ 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
90
+
85
91
  token = input[i]
86
92
  lookahead = input[i + 1]
87
- process_token(token, lookahead, i, input)
93
+ process_token(token, lookahead, i)
88
94
  i += 1
89
95
  end
90
96
 
91
- while operations.any?
92
- consume
93
- end
97
+ consume while operations.any?
94
98
 
95
- unless output.count == 1
96
- fail! :invalid_statement
97
- end
99
+ fail! :invalid_statement unless output.count == 1
98
100
 
99
101
  output.first
100
102
  end
@@ -113,7 +115,7 @@ module Dentaku
113
115
 
114
116
  private
115
117
 
116
- def process_token(token, lookahead, index, tokens)
118
+ def process_token(token, lookahead, index)
117
119
  case token.category
118
120
  when :datetime then output << AST::DateTime.new(token)
119
121
  when :numeric then output << AST::Numeric.new(token)
@@ -127,13 +129,13 @@ module Dentaku
127
129
  when :function
128
130
  handle_function(token)
129
131
  when :case
130
- handle_case(token, index, tokens)
132
+ handle_case(token)
131
133
  when :access
132
134
  handle_access(token)
133
135
  when :array
134
136
  handle_array(token)
135
137
  when :grouping
136
- handle_grouping(token, lookahead, tokens)
138
+ handle_grouping(token, lookahead, index)
137
139
  else
138
140
  fail! :not_implemented_token_category, token_category: token.category
139
141
  end
@@ -142,13 +144,9 @@ module Dentaku
142
144
  def handle_operator(token, lookahead)
143
145
  op_class = operation(token).resolve_class(lookahead)
144
146
  if op_class.right_associative?
145
- while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
146
- consume
147
- end
147
+ consume while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
148
148
  else
149
- while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
150
- consume
151
- end
149
+ consume while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
152
150
  end
153
151
  operations.push op_class
154
152
  end
@@ -160,74 +158,40 @@ module Dentaku
160
158
  operations.push func
161
159
  end
162
160
 
163
- def handle_case(token, index, tokens)
164
- case_index = operations.index { |o| o == AST::Case } || -1
161
+ def handle_case(token)
162
+ # We always operate on the innermost (most recent) CASE on the stack.
163
+ case_index = operations.rindex(AST::Case) || -1
165
164
  token_index = case_index + 1
166
165
 
167
166
  case token.value
168
167
  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
168
+ # Start a new CASE context.
169
+ operations.push AST::Case
170
+ arities.push(0)
204
171
 
205
172
  when :close
173
+ # Finalize any trailing THEN/ELSE expression still on the stack.
206
174
  if operations[token_index] == AST::CaseThen
207
- while operations.last != AST::Case
208
- consume
209
- end
175
+ consume_until(AST::Case)
210
176
  operations.push(AST::CaseConditional)
211
177
  consume(2)
212
178
  arities[-1] += 1
213
179
  elsif operations[token_index] == AST::CaseElse
214
- while operations.last != AST::Case
215
- consume
216
- end
180
+ consume_until(AST::Case)
217
181
  arities[-1] += 1
218
182
  end
219
- fail! :unprocessed_token, token_name: token.value unless operations.count >= 1 && operations.last == AST::Case
183
+ fail! :unprocessed_token, token_name: token.value unless operations.last == AST::Case
220
184
  consume(arities.pop.succ)
221
185
 
222
186
  when :when
223
187
  if operations[token_index] == AST::CaseThen
224
- while ![AST::CaseWhen, AST::Case].include?(operations.last)
225
- consume
226
- end
188
+ # Close out previous WHEN/THEN pair.
189
+ consume_until([AST::CaseWhen, AST::Case])
227
190
  operations.push(AST::CaseConditional)
228
191
  consume(2)
229
192
  arities[-1] += 1
230
193
  elsif operations.last == AST::Case
194
+ # First WHEN: finalize switch variable expression.
231
195
  operations.push(AST::CaseSwitchVariable)
232
196
  consume
233
197
  end
@@ -235,36 +199,41 @@ module Dentaku
235
199
 
236
200
  when :then
237
201
  if operations[token_index] == AST::CaseWhen
238
- while ![AST::CaseThen, AST::Case].include?(operations.last)
239
- consume
240
- end
202
+ consume_until([AST::CaseThen, AST::Case])
241
203
  end
242
204
  operations.push(AST::CaseThen)
243
205
 
244
206
  when :else
245
207
  if operations[token_index] == AST::CaseThen
246
- while operations.last != AST::Case
247
- consume
248
- end
208
+ consume_until(AST::Case)
249
209
  operations.push(AST::CaseConditional)
250
210
  consume(2)
251
211
  arities[-1] += 1
252
212
  end
253
213
  operations.push(AST::CaseElse)
214
+
254
215
  else
255
216
  fail! :unknown_case_token, token_name: token.value
256
217
  end
257
218
  end
258
219
 
220
+ def consume_until(target)
221
+ matcher =
222
+ case target
223
+ when Array then ->(op) { target.include?(op) }
224
+ else ->(op) { op == target }
225
+ end
226
+
227
+ consume while operations.any? && !matcher.call(operations.last)
228
+ end
229
+
259
230
  def handle_access(token)
260
231
  case token.value
261
232
  when :lbracket
262
233
  operations.push AST::Access
263
234
 
264
235
  when :rbracket
265
- while operations.any? && operations.last != AST::Access
266
- consume
267
- end
236
+ consume while operations.any? && operations.last != AST::Access
268
237
  fail! :unbalanced_bracket, token: token unless operations.last == AST::Access
269
238
  consume
270
239
  end
@@ -277,20 +246,19 @@ module Dentaku
277
246
  arities.push 0
278
247
 
279
248
  when :array_end
280
- while operations.any? && operations.last != AST::Array
281
- consume
282
- end
249
+ consume while operations.any? && operations.last != AST::Array
283
250
  fail! :unbalanced_bracket, token: token unless operations.last == AST::Array
284
251
  consume(arities.pop.succ)
285
252
  end
286
253
  end
287
254
 
288
- def handle_grouping(token, lookahead, tokens)
255
+ def handle_grouping(token, lookahead, token_index)
289
256
  case token.value
290
257
  when :open
291
258
  if lookahead && lookahead.value == :close
292
259
  # 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
260
+ # skip to the end
261
+ @skip_indices << token_index + 1
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
@@ -114,12 +114,14 @@ module Dentaku
114
114
 
115
115
  def negate
116
116
  new(:operator, '-', lambda { |raw| :negate }, lambda { |last_token|
117
- last_token.nil? ||
118
- last_token.is?(:operator) ||
119
- last_token.is?(:comparator) ||
120
- last_token.is?(:combinator) ||
121
- last_token.value == :open ||
122
- last_token.value == :comma
117
+ last_token.nil? ||
118
+ last_token.is?(:operator) ||
119
+ last_token.is?(:comparator) ||
120
+ last_token.is?(:combinator) ||
121
+ last_token.value == :open ||
122
+ last_token.value == :comma ||
123
+ last_token.value == :lbracket ||
124
+ last_token.value == :array_start
123
125
  })
124
126
  end
125
127
 
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.5"
2
+ VERSION = "3.5.7"
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
data/spec/parser_spec.rb CHANGED
@@ -52,6 +52,11 @@ describe Dentaku::Parser do
52
52
  expect(node.value).to eq(2)
53
53
  end
54
54
 
55
+ it 'parses multiple zero-argument functions in sequence' do
56
+ node = parse('count() + count()') # count() without arguments returns 0
57
+ expect(node.value).to eq(0)
58
+ end
59
+
55
60
  it 'represents formulas with variables' do
56
61
  node = parse('5 * x')
57
62
  expect { node.value }.to raise_error(Dentaku::UnboundVariableError)
@@ -49,6 +49,20 @@ describe Dentaku::Tokenizer do
49
49
  expect(tokens.map(&:category)).to eq([:grouping, :operator, :numeric, :grouping])
50
50
  expect(tokens.map(&:value)).to eq([:open, :negate, 5, :close])
51
51
 
52
+ tokens = tokenizer.tokenize('{-5, -2}[-1]')
53
+ expect(tokens.map(&:category)).to eq([
54
+ :array, # {
55
+ :operator, :numeric, :grouping, # -5,
56
+ :operator, :numeric, :array, # -2}
57
+ :access, :operator, :numeric, :access # [-1]
58
+ ])
59
+ expect(tokens.map(&:value)).to eq([
60
+ :array_start, # {
61
+ :negate, 5, :comma, # -5,
62
+ :negate, 2, :array_end, # -2}
63
+ :lbracket, :negate, 1, :rbracket # [-1]
64
+ ])
65
+
52
66
  tokens = tokenizer.tokenize('if(-5 > x, -7, -8) - 9')
53
67
  expect(tokens.map(&:category)).to eq([
54
68
  :function, :grouping, # if(
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.7
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-12-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bigdecimal