dentaku 3.5.3 → 3.5.5
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/.github/workflows/rspec.yml +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/CHANGELOG.md +22 -6
- data/README.md +1 -4
- data/dentaku.gemspec +1 -1
- data/lib/dentaku/ast/access.rb +0 -3
- data/lib/dentaku/ast/arithmetic.rb +36 -50
- data/lib/dentaku/ast/array.rb +1 -4
- data/lib/dentaku/ast/case.rb +12 -0
- data/lib/dentaku/ast/functions/all.rb +1 -5
- data/lib/dentaku/ast/functions/any.rb +1 -5
- data/lib/dentaku/ast/functions/enum.rb +13 -0
- data/lib/dentaku/ast/functions/map.rb +1 -5
- data/lib/dentaku/ast/functions/pluck.rb +6 -2
- data/lib/dentaku/ast/node.rb +2 -1
- data/lib/dentaku/ast/operation.rb +5 -0
- data/lib/dentaku/ast.rb +1 -1
- data/lib/dentaku/bulk_expression_solver.rb +37 -7
- data/lib/dentaku/calculator.rb +21 -5
- data/lib/dentaku/date_arithmetic.rb +24 -15
- data/lib/dentaku/dependency_resolver.rb +9 -4
- data/lib/dentaku/parser.rb +206 -213
- data/lib/dentaku/print_visitor.rb +2 -2
- data/lib/dentaku/token.rb +12 -0
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +1 -1
- data/spec/ast/addition_spec.rb +12 -7
- data/spec/ast/all_spec.rb +13 -0
- data/spec/ast/any_spec.rb +13 -0
- data/spec/ast/arithmetic_spec.rb +7 -0
- data/spec/ast/division_spec.rb +10 -6
- data/spec/ast/map_spec.rb +13 -0
- data/spec/ast/pluck_spec.rb +17 -0
- data/spec/bulk_expression_solver_spec.rb +24 -1
- data/spec/calculator_spec.rb +21 -3
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/external_function_spec.rb +1 -1
- data/spec/parser_spec.rb +11 -2
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/visitor_spec.rb +1 -1
- metadata +10 -9
@@ -4,13 +4,18 @@ module Dentaku
|
|
4
4
|
class DependencyResolver
|
5
5
|
include TSort
|
6
6
|
|
7
|
-
def self.find_resolve_order(vars_to_dependencies_hash)
|
8
|
-
self.new(vars_to_dependencies_hash).
|
7
|
+
def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
|
8
|
+
self.new(vars_to_dependencies_hash).sort
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize(vars_to_dependencies_hash)
|
12
|
-
|
13
|
-
|
12
|
+
@key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [k.downcase, k] }]
|
13
|
+
# ensure variables are normalized strings
|
14
|
+
@vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.downcase.to_s, v] }]
|
15
|
+
end
|
16
|
+
|
17
|
+
def sort
|
18
|
+
tsort.map { |k| @key_mapping.fetch(k, k) }
|
14
19
|
end
|
15
20
|
|
16
21
|
def tsort_each_node(&block)
|
data/lib/dentaku/parser.rb
CHANGED
@@ -43,8 +43,6 @@ module Dentaku
|
|
43
43
|
operator = operations.pop
|
44
44
|
fail! :invalid_statement if operator.nil?
|
45
45
|
|
46
|
-
operator.peek(output)
|
47
|
-
|
48
46
|
output_size = output.length
|
49
47
|
args_size = operator.arity || count
|
50
48
|
min_size = operator.arity || operator.min_param_count || count
|
@@ -55,17 +53,18 @@ module Dentaku
|
|
55
53
|
fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
|
56
54
|
end
|
57
55
|
|
58
|
-
if output_size > max_size && operations.empty? || args_size > max_size
|
56
|
+
if (output_size > max_size && operations.empty?) || args_size > max_size
|
59
57
|
expect = min_size == max_size ? min_size : min_size..max_size
|
60
58
|
fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
|
61
59
|
end
|
62
60
|
|
61
|
+
args = []
|
63
62
|
if operator == AST::Array && output.empty?
|
64
|
-
|
63
|
+
# special case: empty array literal '{}'
|
64
|
+
output.push(operator.new)
|
65
65
|
else
|
66
66
|
fail! :invalid_statement if output_size < args_size
|
67
67
|
args = Array.new(args_size) { output.pop }.reverse
|
68
|
-
|
69
68
|
output.push operator.new(*args)
|
70
69
|
end
|
71
70
|
|
@@ -81,250 +80,244 @@ module Dentaku
|
|
81
80
|
def parse
|
82
81
|
return AST::Nil.new if input.empty?
|
83
82
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
83
|
+
i = 0
|
84
|
+
while i < input.length
|
85
|
+
token = input[i]
|
86
|
+
lookahead = input[i + 1]
|
87
|
+
process_token(token, lookahead, i, input)
|
88
|
+
i += 1
|
89
|
+
end
|
88
90
|
|
89
|
-
|
90
|
-
|
91
|
+
while operations.any?
|
92
|
+
consume
|
93
|
+
end
|
91
94
|
|
92
|
-
|
93
|
-
|
95
|
+
unless output.count == 1
|
96
|
+
fail! :invalid_statement
|
97
|
+
end
|
94
98
|
|
95
|
-
|
96
|
-
|
99
|
+
output.first
|
100
|
+
end
|
97
101
|
|
98
|
-
|
99
|
-
|
102
|
+
def operation(token)
|
103
|
+
AST_OPERATIONS.fetch(token.value)
|
104
|
+
end
|
100
105
|
|
101
|
-
|
102
|
-
|
106
|
+
def function(token)
|
107
|
+
function_registry.get(token.value)
|
108
|
+
end
|
103
109
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
end
|
110
|
+
def function_registry
|
111
|
+
@function_registry ||= Dentaku::AST::FunctionRegistry.new
|
112
|
+
end
|
108
113
|
|
109
|
-
|
110
|
-
else
|
111
|
-
while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
112
|
-
consume
|
113
|
-
end
|
114
|
+
private
|
114
115
|
|
115
|
-
|
116
|
-
|
116
|
+
def process_token(token, lookahead, index, tokens)
|
117
|
+
case token.category
|
118
|
+
when :datetime then output << AST::DateTime.new(token)
|
119
|
+
when :numeric then output << AST::Numeric.new(token)
|
120
|
+
when :logical then output << AST::Logical.new(token)
|
121
|
+
when :string then output << AST::String.new(token)
|
122
|
+
when :identifier then output << AST::Identifier.new(token, case_sensitive: case_sensitive)
|
123
|
+
when :operator, :comparator, :combinator
|
124
|
+
handle_operator(token, lookahead)
|
125
|
+
when :null
|
126
|
+
output << AST::Nil.new
|
127
|
+
when :function
|
128
|
+
handle_function(token)
|
129
|
+
when :case
|
130
|
+
handle_case(token, index, tokens)
|
131
|
+
when :access
|
132
|
+
handle_access(token)
|
133
|
+
when :array
|
134
|
+
handle_array(token)
|
135
|
+
when :grouping
|
136
|
+
handle_grouping(token, lookahead, tokens)
|
137
|
+
else
|
138
|
+
fail! :not_implemented_token_category, token_category: token.category
|
139
|
+
end
|
140
|
+
end
|
117
141
|
|
118
|
-
|
119
|
-
|
142
|
+
def handle_operator(token, lookahead)
|
143
|
+
op_class = operation(token).resolve_class(lookahead)
|
144
|
+
if op_class.right_associative?
|
145
|
+
while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
146
|
+
consume
|
147
|
+
end
|
148
|
+
else
|
149
|
+
while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
|
150
|
+
consume
|
151
|
+
end
|
152
|
+
end
|
153
|
+
operations.push op_class
|
154
|
+
end
|
120
155
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
156
|
+
def handle_function(token)
|
157
|
+
func = function(token)
|
158
|
+
fail! :undefined_function, function_name: token.value if func.nil?
|
159
|
+
arities.push 0
|
160
|
+
operations.push func
|
161
|
+
end
|
126
162
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
case
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
if open_cases > 0
|
150
|
-
open_cases -= 1
|
151
|
-
else
|
152
|
-
case_end_index = index
|
153
|
-
break
|
154
|
-
end
|
155
|
-
end
|
163
|
+
def handle_case(token, index, tokens)
|
164
|
+
case_index = operations.index { |o| o == AST::Case } || -1
|
165
|
+
token_index = case_index + 1
|
166
|
+
|
167
|
+
case token.value
|
168
|
+
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
|
156
185
|
end
|
157
186
|
end
|
158
|
-
inner_case_inputs = input.slice!(0..case_end_index)
|
159
|
-
subparser = Parser.new(
|
160
|
-
inner_case_inputs,
|
161
|
-
operations: [AST::Case],
|
162
|
-
arities: [0],
|
163
|
-
function_registry: @function_registry,
|
164
|
-
case_sensitive: case_sensitive
|
165
|
-
)
|
166
|
-
subparser.parse
|
167
|
-
output.concat(subparser.output)
|
168
|
-
else
|
169
|
-
operations.push AST::Case
|
170
|
-
arities.push(0)
|
171
|
-
end
|
172
|
-
when :close
|
173
|
-
if operations[token_index] == AST::CaseThen
|
174
|
-
while operations.last != AST::Case
|
175
|
-
consume
|
176
|
-
end
|
177
|
-
|
178
|
-
operations.push(AST::CaseConditional)
|
179
|
-
consume(2)
|
180
|
-
arities[-1] += 1
|
181
|
-
elsif operations[token_index] == AST::CaseElse
|
182
|
-
while operations.last != AST::Case
|
183
|
-
consume
|
184
|
-
end
|
185
|
-
|
186
|
-
arities[-1] += 1
|
187
|
-
end
|
188
|
-
|
189
|
-
unless operations.count >= 1 && operations.last == AST::Case
|
190
|
-
fail! :unprocessed_token, token_name: token.value
|
191
187
|
end
|
192
|
-
|
193
|
-
when :when
|
194
|
-
if operations[token_index] == AST::CaseThen
|
195
|
-
while ![AST::CaseWhen, AST::Case].include?(operations.last)
|
196
|
-
consume
|
197
|
-
end
|
198
|
-
operations.push(AST::CaseConditional)
|
199
|
-
consume(2)
|
200
|
-
arities[-1] += 1
|
201
|
-
elsif operations.last == AST::Case
|
202
|
-
operations.push(AST::CaseSwitchVariable)
|
203
|
-
consume
|
204
|
-
end
|
205
|
-
|
206
|
-
operations.push(AST::CaseWhen)
|
207
|
-
when :then
|
208
|
-
if operations[token_index] == AST::CaseWhen
|
209
|
-
while ![AST::CaseThen, AST::Case].include?(operations.last)
|
210
|
-
consume
|
211
|
-
end
|
212
|
-
end
|
213
|
-
operations.push(AST::CaseThen)
|
214
|
-
when :else
|
215
|
-
if operations[token_index] == AST::CaseThen
|
216
|
-
while operations.last != AST::Case
|
217
|
-
consume
|
218
|
-
end
|
219
|
-
|
220
|
-
operations.push(AST::CaseConditional)
|
221
|
-
consume(2)
|
222
|
-
arities[-1] += 1
|
223
|
-
end
|
224
|
-
|
225
|
-
operations.push(AST::CaseElse)
|
226
|
-
else
|
227
|
-
fail! :unknown_case_token, token_name: token.value
|
188
|
+
j += 1
|
228
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
|
229
204
|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
operations.push AST::Access
|
234
|
-
when :rbracket
|
235
|
-
while operations.any? && operations.last != AST::Access
|
236
|
-
consume
|
237
|
-
end
|
238
|
-
|
239
|
-
unless operations.last == AST::Access
|
240
|
-
fail! :unbalanced_bracket, token: token
|
241
|
-
end
|
205
|
+
when :close
|
206
|
+
if operations[token_index] == AST::CaseThen
|
207
|
+
while operations.last != AST::Case
|
242
208
|
consume
|
243
209
|
end
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
when :array_end
|
251
|
-
while operations.any? && operations.last != AST::Array
|
252
|
-
consume
|
253
|
-
end
|
254
|
-
|
255
|
-
unless operations.last == AST::Array
|
256
|
-
fail! :unbalanced_bracket, token: token
|
257
|
-
end
|
258
|
-
|
259
|
-
consume(arities.pop.succ)
|
210
|
+
operations.push(AST::CaseConditional)
|
211
|
+
consume(2)
|
212
|
+
arities[-1] += 1
|
213
|
+
elsif operations[token_index] == AST::CaseElse
|
214
|
+
while operations.last != AST::Case
|
215
|
+
consume
|
260
216
|
end
|
217
|
+
arities[-1] += 1
|
218
|
+
end
|
219
|
+
fail! :unprocessed_token, token_name: token.value unless operations.count >= 1 && operations.last == AST::Case
|
220
|
+
consume(arities.pop.succ)
|
261
221
|
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
consume
|
276
|
-
end
|
277
|
-
|
278
|
-
lparen = operations.pop
|
279
|
-
unless lparen == AST::Grouping
|
280
|
-
fail! :unbalanced_parenthesis, token
|
281
|
-
end
|
282
|
-
|
283
|
-
if operations.last && operations.last < AST::Function
|
284
|
-
consume(arities.pop.succ)
|
285
|
-
end
|
286
|
-
|
287
|
-
when :comma
|
288
|
-
fail! :invalid_statement if arities.empty?
|
289
|
-
arities[-1] += 1
|
290
|
-
while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
|
291
|
-
consume
|
292
|
-
end
|
222
|
+
when :when
|
223
|
+
if operations[token_index] == AST::CaseThen
|
224
|
+
while ![AST::CaseWhen, AST::Case].include?(operations.last)
|
225
|
+
consume
|
226
|
+
end
|
227
|
+
operations.push(AST::CaseConditional)
|
228
|
+
consume(2)
|
229
|
+
arities[-1] += 1
|
230
|
+
elsif operations.last == AST::Case
|
231
|
+
operations.push(AST::CaseSwitchVariable)
|
232
|
+
consume
|
233
|
+
end
|
234
|
+
operations.push(AST::CaseWhen)
|
293
235
|
|
294
|
-
|
295
|
-
|
236
|
+
when :then
|
237
|
+
if operations[token_index] == AST::CaseWhen
|
238
|
+
while ![AST::CaseThen, AST::Case].include?(operations.last)
|
239
|
+
consume
|
296
240
|
end
|
241
|
+
end
|
242
|
+
operations.push(AST::CaseThen)
|
297
243
|
|
298
|
-
|
299
|
-
|
244
|
+
when :else
|
245
|
+
if operations[token_index] == AST::CaseThen
|
246
|
+
while operations.last != AST::Case
|
247
|
+
consume
|
248
|
+
end
|
249
|
+
operations.push(AST::CaseConditional)
|
250
|
+
consume(2)
|
251
|
+
arities[-1] += 1
|
300
252
|
end
|
253
|
+
operations.push(AST::CaseElse)
|
254
|
+
else
|
255
|
+
fail! :unknown_case_token, token_name: token.value
|
301
256
|
end
|
257
|
+
end
|
302
258
|
|
303
|
-
|
259
|
+
def handle_access(token)
|
260
|
+
case token.value
|
261
|
+
when :lbracket
|
262
|
+
operations.push AST::Access
|
263
|
+
|
264
|
+
when :rbracket
|
265
|
+
while operations.any? && operations.last != AST::Access
|
266
|
+
consume
|
267
|
+
end
|
268
|
+
fail! :unbalanced_bracket, token: token unless operations.last == AST::Access
|
304
269
|
consume
|
305
270
|
end
|
271
|
+
end
|
306
272
|
|
307
|
-
|
308
|
-
|
309
|
-
|
273
|
+
def handle_array(token)
|
274
|
+
case token.value
|
275
|
+
when :array_start
|
276
|
+
operations.push AST::Array
|
277
|
+
arities.push 0
|
310
278
|
|
311
|
-
|
279
|
+
when :array_end
|
280
|
+
while operations.any? && operations.last != AST::Array
|
281
|
+
consume
|
282
|
+
end
|
283
|
+
fail! :unbalanced_bracket, token: token unless operations.last == AST::Array
|
284
|
+
consume(arities.pop.succ)
|
285
|
+
end
|
312
286
|
end
|
313
287
|
|
314
|
-
def
|
315
|
-
|
316
|
-
|
288
|
+
def handle_grouping(token, lookahead, tokens)
|
289
|
+
case token.value
|
290
|
+
when :open
|
291
|
+
if lookahead && lookahead.value == :close
|
292
|
+
# 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
|
294
|
+
arities.pop
|
295
|
+
consume(0)
|
296
|
+
else
|
297
|
+
operations.push AST::Grouping
|
298
|
+
end
|
317
299
|
|
318
|
-
|
319
|
-
|
320
|
-
|
300
|
+
when :close
|
301
|
+
while operations.any? && operations.last != AST::Grouping
|
302
|
+
consume
|
303
|
+
end
|
304
|
+
lparen = operations.pop
|
305
|
+
fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
|
306
|
+
if operations.last && operations.last < AST::Function
|
307
|
+
consume(arities.pop.succ)
|
308
|
+
end
|
321
309
|
|
322
|
-
|
323
|
-
|
310
|
+
when :comma
|
311
|
+
fail! :invalid_statement if arities.empty?
|
312
|
+
arities[-1] += 1
|
313
|
+
while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
|
314
|
+
consume
|
315
|
+
end
|
316
|
+
else
|
317
|
+
fail! :unknown_grouping_token, token_name: token.value
|
318
|
+
end
|
324
319
|
end
|
325
320
|
|
326
|
-
private
|
327
|
-
|
328
321
|
def fail!(reason, **meta)
|
329
322
|
message =
|
330
323
|
case reason
|
@@ -7,13 +7,13 @@ module Dentaku
|
|
7
7
|
|
8
8
|
def visit_operation(node)
|
9
9
|
if node.left
|
10
|
-
visit_operand(node.left, node.class.precedence, suffix:
|
10
|
+
visit_operand(node.left, node.class.precedence, suffix: node.operator_spacing, dir: :left)
|
11
11
|
end
|
12
12
|
|
13
13
|
@output << node.display_operator
|
14
14
|
|
15
15
|
if node.right
|
16
|
-
visit_operand(node.right, node.class.precedence, prefix:
|
16
|
+
visit_operand(node.right, node.class.precedence, prefix: node.operator_spacing, dir: :right)
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
data/lib/dentaku/token.rb
CHANGED
@@ -20,10 +20,22 @@ module Dentaku
|
|
20
20
|
length.zero?
|
21
21
|
end
|
22
22
|
|
23
|
+
def operator?
|
24
|
+
is?(:operator)
|
25
|
+
end
|
26
|
+
|
23
27
|
def grouping?
|
24
28
|
is?(:grouping)
|
25
29
|
end
|
26
30
|
|
31
|
+
def open?
|
32
|
+
grouping? && value == :open
|
33
|
+
end
|
34
|
+
|
35
|
+
def close?
|
36
|
+
grouping? && value == :close
|
37
|
+
end
|
38
|
+
|
27
39
|
def is?(c)
|
28
40
|
category == c
|
29
41
|
end
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/addition_spec.rb
CHANGED
@@ -20,22 +20,21 @@ describe Dentaku::AST::Addition do
|
|
20
20
|
expect(node.value).to eq(11)
|
21
21
|
end
|
22
22
|
|
23
|
-
it 'requires
|
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).value
|
26
|
+
}.to raise_error(Dentaku::ArgumentError, /requires operands that respond to +/)
|
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).value
|
33
33
|
}.not_to raise_error
|
34
34
|
end
|
35
35
|
|
36
36
|
it 'allows operands that respond to addition' do
|
37
37
|
# Sample struct that has a custom definition for addition
|
38
|
-
|
39
38
|
Addable = Struct.new(:value) do
|
40
39
|
def +(other)
|
41
40
|
case other
|
@@ -51,12 +50,18 @@ describe Dentaku::AST::Addition do
|
|
51
50
|
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(6))
|
52
51
|
|
53
52
|
expect {
|
54
|
-
described_class.new(operand_five, operand_six)
|
53
|
+
described_class.new(operand_five, operand_six).value
|
55
54
|
}.not_to raise_error
|
56
55
|
|
57
56
|
expect {
|
58
|
-
described_class.new(operand_five, six)
|
57
|
+
described_class.new(operand_five, six).value
|
59
58
|
}.not_to raise_error
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'does not try to parse nested string as date' do
|
62
|
+
a = ['2017-01-01', '2017-01-02']
|
63
|
+
b = ['2017-01-01']
|
60
64
|
|
65
|
+
expect(Dentaku('a + b', a: a, b: b)).to eq(['2017-01-01', '2017-01-02', '2017-01-01'])
|
61
66
|
end
|
62
67
|
end
|
data/spec/ast/all_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
|
|
4
4
|
|
5
5
|
describe Dentaku::AST::All do
|
6
6
|
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
7
8
|
it 'performs ALL operation' do
|
8
9
|
result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
|
9
10
|
expect(result).to eq(false)
|
@@ -22,4 +23,16 @@ describe Dentaku::AST::All do
|
|
22
23
|
Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
|
23
24
|
)
|
24
25
|
end
|
26
|
+
|
27
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
28
|
+
expect(
|
29
|
+
calculator.evaluate('ALL(items, item, item.value)', items: [{value: 1}, {}])
|
30
|
+
).to be_falsy
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
34
|
+
expect {
|
35
|
+
calculator.evaluate!('ALL(items, item, item.value)', items: [{value: 1}, {}])
|
36
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
37
|
+
end
|
25
38
|
end
|
data/spec/ast/any_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
|
|
4
4
|
|
5
5
|
describe Dentaku::AST::Any do
|
6
6
|
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
7
8
|
it 'performs ANY operation' do
|
8
9
|
result = Dentaku('ANY(vals, val, val > 1)', vals: [1, 2, 3])
|
9
10
|
expect(result).to eq(true)
|
@@ -20,4 +21,16 @@ describe Dentaku::AST::Any do
|
|
20
21
|
it 'raises argument error if a string is passed as identifier' do
|
21
22
|
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
|
22
23
|
end
|
24
|
+
|
25
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
26
|
+
expect(
|
27
|
+
calculator.evaluate('ANY(items, item, item.value)', items: [{value: 1}, {}])
|
28
|
+
).to be_truthy
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
32
|
+
expect {
|
33
|
+
calculator.evaluate!('ANY(items, item, item.value)', items: [{}, {value: 1}])
|
34
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
35
|
+
end
|
23
36
|
end
|
data/spec/ast/arithmetic_spec.rb
CHANGED
@@ -111,6 +111,13 @@ describe Dentaku::AST::Arithmetic do
|
|
111
111
|
end
|
112
112
|
end
|
113
113
|
|
114
|
+
it 'does not try to parse nested string as date' do
|
115
|
+
a = ['2017-01-01', '2017-01-02']
|
116
|
+
b = ['2017-01-01']
|
117
|
+
|
118
|
+
expect(Dentaku('a - b', a: a, b: b)).to eq(['2017-01-02'])
|
119
|
+
end
|
120
|
+
|
114
121
|
it 'raises ArgumentError if given individually valid but incompatible arguments' do
|
115
122
|
expect { add(one, date) }.to raise_error(Dentaku::ArgumentError)
|
116
123
|
expect { add(x, one, 'x' => [1]) }.to raise_error(Dentaku::ArgumentError)
|