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 +4 -4
- data/lib/dentaku/ast/arithmetic.rb +27 -1
- data/lib/dentaku/ast/comparators.rb +1 -1
- data/lib/dentaku/parser.rb +44 -79
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/addition_spec.rb +3 -3
- data/spec/ast/division_spec.rb +3 -3
- data/spec/calculator_spec.rb +7 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fc1bbfde891d6c98eda08b9176f9ed791744b2043176f07d3aac997fae34a3e9
|
|
4
|
+
data.tar.gz: b8658f3d74364672f1a4538928e40d7b4d7aff7489273b573dbd1970303a60b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
|
data/lib/dentaku/parser.rb
CHANGED
|
@@ -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
|
|
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
|
|
164
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/addition_spec.rb
CHANGED
|
@@ -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)
|
|
26
|
-
}.to raise_error(Dentaku::
|
|
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)
|
|
32
|
+
described_class.new(group, five)
|
|
33
33
|
}.not_to raise_error
|
|
34
34
|
end
|
|
35
35
|
|
data/spec/ast/division_spec.rb
CHANGED
|
@@ -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)
|
|
26
|
-
}.to raise_error(Dentaku::
|
|
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)
|
|
32
|
+
described_class.new(group, five)
|
|
33
33
|
}.not_to raise_error
|
|
34
34
|
end
|
|
35
35
|
|
data/spec/calculator_spec.rb
CHANGED
|
@@ -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.
|
|
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-
|
|
10
|
+
date: 2025-10-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bigdecimal
|