dentaku 3.4.2 → 3.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +3 -4
- data/lib/dentaku/ast/access.rb +6 -0
- data/lib/dentaku/ast/arithmetic.rb +5 -1
- data/lib/dentaku/ast/array.rb +4 -0
- data/lib/dentaku/ast/bitwise.rb +8 -0
- data/lib/dentaku/ast/case/case_conditional.rb +4 -0
- data/lib/dentaku/ast/case/case_else.rb +6 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
- data/lib/dentaku/ast/case/case_then.rb +6 -0
- data/lib/dentaku/ast/case/case_when.rb +10 -0
- data/lib/dentaku/ast/case.rb +6 -0
- data/lib/dentaku/ast/comparators.rb +25 -35
- data/lib/dentaku/ast/function.rb +6 -8
- data/lib/dentaku/ast/functions/all.rb +4 -17
- data/lib/dentaku/ast/functions/any.rb +4 -17
- data/lib/dentaku/ast/functions/duration.rb +2 -2
- data/lib/dentaku/ast/functions/enum.rb +37 -0
- data/lib/dentaku/ast/functions/filter.rb +4 -17
- data/lib/dentaku/ast/functions/if.rb +4 -0
- data/lib/dentaku/ast/functions/map.rb +3 -16
- data/lib/dentaku/ast/functions/pluck.rb +8 -7
- data/lib/dentaku/ast/functions/ruby_math.rb +3 -2
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/identifier.rb +8 -0
- data/lib/dentaku/ast/literal.rb +10 -0
- data/lib/dentaku/ast/negation.rb +4 -0
- data/lib/dentaku/ast/nil.rb +4 -0
- data/lib/dentaku/ast/node.rb +4 -0
- data/lib/dentaku/ast/operation.rb +9 -0
- data/lib/dentaku/ast/string.rb +7 -0
- data/lib/dentaku/ast.rb +2 -0
- data/lib/dentaku/parser.rb +5 -3
- data/lib/dentaku/print_visitor.rb +101 -0
- data/lib/dentaku/token_scanner.rb +1 -1
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +82 -0
- data/spec/ast/all_spec.rb +25 -0
- data/spec/ast/any_spec.rb +23 -0
- data/spec/ast/comparator_spec.rb +6 -9
- data/spec/ast/filter_spec.rb +7 -0
- data/spec/ast/function_spec.rb +5 -0
- data/spec/ast/map_spec.rb +12 -0
- data/spec/ast/pluck_spec.rb +32 -0
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/calculator_spec.rb +58 -2
- data/spec/parser_spec.rb +18 -3
- data/spec/print_visitor_spec.rb +66 -0
- data/spec/tokenizer_spec.rb +6 -0
- data/spec/visitor/infix_spec.rb +31 -0
- data/spec/visitor_spec.rb +137 -0
- metadata +24 -6
data/spec/parser_spec.rb
CHANGED
@@ -71,6 +71,11 @@ describe Dentaku::Parser do
|
|
71
71
|
expect(node.value("x" => 3)).to eq(4)
|
72
72
|
end
|
73
73
|
|
74
|
+
it 'evaluates a nested case statement with case-sensitivity' do
|
75
|
+
node = parse('CASE x WHEN 1 THEN CASE Y WHEN "A" THEN 2 WHEN "B" THEN 3 END END', { case_sensitive: true }, { case_sensitive: true })
|
76
|
+
expect(node.value("x" => 1, "y" => "A", "Y" => "B")).to eq(3)
|
77
|
+
end
|
78
|
+
|
74
79
|
it 'evaluates arrays' do
|
75
80
|
node = parse('{1, 2, 3}')
|
76
81
|
expect(node.value).to eq([1, 2, 3])
|
@@ -89,6 +94,16 @@ describe Dentaku::Parser do
|
|
89
94
|
}.to raise_error(Dentaku::ParseError)
|
90
95
|
end
|
91
96
|
|
97
|
+
it 'raises a parse error for too many operands' do
|
98
|
+
expect {
|
99
|
+
parse("IF(1, 0, IF(1, 2, 3, 4))")
|
100
|
+
}.to raise_error(Dentaku::ParseError)
|
101
|
+
|
102
|
+
expect {
|
103
|
+
parse("CASE a WHEN 1 THEN true ELSE THEN IF(1, 2, 3, 4) END")
|
104
|
+
}.to raise_error(Dentaku::ParseError)
|
105
|
+
end
|
106
|
+
|
92
107
|
it 'raises a parse error for bad grouping structure' do
|
93
108
|
expect {
|
94
109
|
parse(",")
|
@@ -144,8 +159,8 @@ describe Dentaku::Parser do
|
|
144
159
|
|
145
160
|
private
|
146
161
|
|
147
|
-
def parse(expr)
|
148
|
-
tokens = Dentaku::Tokenizer.new.tokenize(expr)
|
149
|
-
described_class.new(tokens).parse
|
162
|
+
def parse(expr, parser_options = {}, tokenizer_options = {})
|
163
|
+
tokens = Dentaku::Tokenizer.new.tokenize(expr, tokenizer_options)
|
164
|
+
described_class.new(tokens, parser_options).parse
|
150
165
|
end
|
151
166
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'dentaku/print_visitor'
|
2
|
+
require 'dentaku/tokenizer'
|
3
|
+
require 'dentaku/parser'
|
4
|
+
|
5
|
+
describe Dentaku::PrintVisitor do
|
6
|
+
it 'prints a representation of an AST' do
|
7
|
+
repr = roundtrip('5+4')
|
8
|
+
expect(repr).to eq('5 + 4')
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'quotes string literals' do
|
12
|
+
repr = roundtrip('Concat(\'a\', "B")')
|
13
|
+
expect(repr).to eq('CONCAT("a", "B")')
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'handles unary operations on literals' do
|
17
|
+
repr = roundtrip('- 4')
|
18
|
+
expect(repr).to eq('-4')
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'handles unary operations on trees' do
|
22
|
+
repr = roundtrip('- (5 + 5)')
|
23
|
+
expect(repr).to eq('-(5 + 5)')
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'handles a complex arithmetic expression' do
|
27
|
+
repr = roundtrip('(((1 + 7) * (8 ^ 2)) / - (3.0 - apples))')
|
28
|
+
expect(repr).to eq('(1 + 7) * 8 ^ 2 / -(3.0 - apples)')
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'handles a complex logical expression' do
|
32
|
+
repr = roundtrip('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
|
33
|
+
expect(repr).to eq('1 < 2 and 3 <= 4 or 5 > 6 and 7 >= 8 or 9 != 10 and true')
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'handles a function call' do
|
37
|
+
repr = roundtrip('IF(a[0] = NULL, "five", \'seven\')')
|
38
|
+
expect(repr).to eq('IF(a[0] = NULL, "five", "seven")')
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'handles a case statement' do
|
42
|
+
repr = roundtrip('case (a % 5) when 0 then a else b end')
|
43
|
+
expect(repr).to eq('CASE a % 5 WHEN 0 THEN a ELSE b END')
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'handles a bitwise operators' do
|
47
|
+
repr = roundtrip('0xCAFE & 0xDECAF | 0xBEEF')
|
48
|
+
expect(repr).to eq('0xCAFE & 0xDECAF | 0xBEEF')
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'handles a datetime literal' do
|
52
|
+
repr = roundtrip('2017-12-24 23:59:59')
|
53
|
+
expect(repr).to eq('2017-12-24 23:59:59')
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def roundtrip(string)
|
59
|
+
described_class.new(parsed(string)).to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
def parsed(string)
|
63
|
+
tokens = Dentaku::Tokenizer.new.tokenize(string)
|
64
|
+
Dentaku::Parser.new(tokens).parse
|
65
|
+
end
|
66
|
+
end
|
data/spec/tokenizer_spec.rb
CHANGED
@@ -25,6 +25,12 @@ describe Dentaku::Tokenizer do
|
|
25
25
|
expect(tokens.map(&:category)).to eq([:numeric])
|
26
26
|
expect(tokens.map(&:value)).to eq([6.02e23])
|
27
27
|
end
|
28
|
+
|
29
|
+
tokens = tokenizer.tokenize('6E23')
|
30
|
+
expect(tokens.map(&:value)).to eq([0.6e24])
|
31
|
+
|
32
|
+
tokens = tokenizer.tokenize('6e-23')
|
33
|
+
expect(tokens.map(&:value)).to eq([0.6e-22])
|
28
34
|
end
|
29
35
|
|
30
36
|
it 'tokenizes addition' do
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'dentaku/visitor/infix'
|
4
|
+
|
5
|
+
class ArrayProcessor
|
6
|
+
attr_reader :expression
|
7
|
+
include Dentaku::Visitor::Infix
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@expression = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def process(node)
|
14
|
+
@expression << node.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
RSpec.describe Dentaku::Visitor::Infix do
|
19
|
+
it 'generates array representation of operation' do
|
20
|
+
processor = ArrayProcessor.new
|
21
|
+
processor.visit(ast('5 + 3'))
|
22
|
+
expect(processor.expression).to eq ['5', '+', '3']
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def ast(expression)
|
28
|
+
tokens = Dentaku::Tokenizer.new.tokenize(expression)
|
29
|
+
Dentaku::Parser.new(tokens).parse
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'set'
|
3
|
+
|
4
|
+
class TestVisitor
|
5
|
+
attr_reader :visited
|
6
|
+
|
7
|
+
def initialize(node)
|
8
|
+
@visited = Set.new
|
9
|
+
node.accept(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
def mark_visited(node)
|
13
|
+
@visited.add(node.class.to_s.split("::").last.to_sym)
|
14
|
+
end
|
15
|
+
|
16
|
+
def visit_operation(node)
|
17
|
+
mark_visited(node)
|
18
|
+
|
19
|
+
node.left.accept(self) if node.left
|
20
|
+
node.right.accept(self) if node.right
|
21
|
+
end
|
22
|
+
|
23
|
+
def visit_function(node)
|
24
|
+
mark_visited(node)
|
25
|
+
node.args.each { |a| a.accept(self) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def visit_array(node)
|
29
|
+
mark_visited(node)
|
30
|
+
end
|
31
|
+
|
32
|
+
def visit_case(node)
|
33
|
+
mark_visited(node)
|
34
|
+
node.switch.accept(self)
|
35
|
+
node.conditions.each { |c| c.accept(self) }
|
36
|
+
node.else && node.else.accept(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
def visit_switch(node)
|
40
|
+
mark_visited(node)
|
41
|
+
node.node.accept(self)
|
42
|
+
end
|
43
|
+
|
44
|
+
def visit_case_conditional(node)
|
45
|
+
mark_visited(node)
|
46
|
+
node.when.accept(self)
|
47
|
+
node.then.accept(self)
|
48
|
+
end
|
49
|
+
|
50
|
+
def visit_when(node)
|
51
|
+
mark_visited(node)
|
52
|
+
node.node.accept(self)
|
53
|
+
end
|
54
|
+
|
55
|
+
def visit_then(node)
|
56
|
+
mark_visited(node)
|
57
|
+
node.node.accept(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def visit_else(node)
|
61
|
+
mark_visited(node)
|
62
|
+
node.node.accept(self)
|
63
|
+
end
|
64
|
+
|
65
|
+
def visit_negation(node)
|
66
|
+
mark_visited(node)
|
67
|
+
node.node.accept(self)
|
68
|
+
end
|
69
|
+
|
70
|
+
def visit_access(node)
|
71
|
+
mark_visited(node)
|
72
|
+
node.structure.accept(self)
|
73
|
+
node.index.accept(self)
|
74
|
+
end
|
75
|
+
|
76
|
+
def visit_literal(node)
|
77
|
+
mark_visited(node)
|
78
|
+
end
|
79
|
+
|
80
|
+
def visit_identifier(node)
|
81
|
+
mark_visited(node)
|
82
|
+
end
|
83
|
+
|
84
|
+
def visit_nil(node)
|
85
|
+
mark_visited(node)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe TestVisitor do
|
90
|
+
def generic_subclasses
|
91
|
+
[
|
92
|
+
:Arithmetic,
|
93
|
+
:Combinator,
|
94
|
+
:Comparator,
|
95
|
+
:Function,
|
96
|
+
:FunctionRegistry,
|
97
|
+
:Grouping,
|
98
|
+
:Literal,
|
99
|
+
:Node,
|
100
|
+
:Operation,
|
101
|
+
:StringFunctions,
|
102
|
+
:RubyMath,
|
103
|
+
:Enum,
|
104
|
+
]
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'visits all concrete AST node types' do
|
108
|
+
@visited = Set.new
|
109
|
+
|
110
|
+
visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples')
|
111
|
+
visit_nodes('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
|
112
|
+
visit_nodes('IF(a[0] = NULL, "five", \'seven\')')
|
113
|
+
visit_nodes('case (a % 5) when 0 then a else b end')
|
114
|
+
visit_nodes('0xCAFE & 0xDECAF | 0xBEEF')
|
115
|
+
visit_nodes('2017-12-24 23:59:59')
|
116
|
+
visit_nodes('ALL({1, 2, 3}, "val", val % 2 == 0)')
|
117
|
+
visit_nodes('ANY(vals, val, val > 1)')
|
118
|
+
visit_nodes('COUNT({1, 2, 3})')
|
119
|
+
visit_nodes('PLUCK(users, age)')
|
120
|
+
visit_nodes('XOR(false, false)')
|
121
|
+
visit_nodes('duration(1, day)')
|
122
|
+
visit_nodes('MAP(vals, val, val + 1)')
|
123
|
+
visit_nodes('FILTER(vals, val, val > 1)')
|
124
|
+
|
125
|
+
@expected = Set.new(Dentaku::AST::constants - generic_subclasses)
|
126
|
+
expect(@visited.sort).to eq(@expected.sort)
|
127
|
+
end
|
128
|
+
|
129
|
+
private
|
130
|
+
|
131
|
+
def visit_nodes(string)
|
132
|
+
tokens = Dentaku::Tokenizer.new.tokenize(string)
|
133
|
+
node = Dentaku::Parser.new(tokens).parse
|
134
|
+
visitor = TestVisitor.new(node)
|
135
|
+
@visited += visitor.visited
|
136
|
+
end
|
137
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.5.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2022-03-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -176,6 +176,7 @@ files:
|
|
176
176
|
- lib/dentaku/ast/functions/avg.rb
|
177
177
|
- lib/dentaku/ast/functions/count.rb
|
178
178
|
- lib/dentaku/ast/functions/duration.rb
|
179
|
+
- lib/dentaku/ast/functions/enum.rb
|
179
180
|
- lib/dentaku/ast/functions/filter.rb
|
180
181
|
- lib/dentaku/ast/functions/if.rb
|
181
182
|
- lib/dentaku/ast/functions/map.rb
|
@@ -192,6 +193,7 @@ files:
|
|
192
193
|
- lib/dentaku/ast/functions/string_functions.rb
|
193
194
|
- lib/dentaku/ast/functions/sum.rb
|
194
195
|
- lib/dentaku/ast/functions/switch.rb
|
196
|
+
- lib/dentaku/ast/functions/xor.rb
|
195
197
|
- lib/dentaku/ast/grouping.rb
|
196
198
|
- lib/dentaku/ast/identifier.rb
|
197
199
|
- lib/dentaku/ast/literal.rb
|
@@ -209,6 +211,7 @@ files:
|
|
209
211
|
- lib/dentaku/exceptions.rb
|
210
212
|
- lib/dentaku/flat_hash.rb
|
211
213
|
- lib/dentaku/parser.rb
|
214
|
+
- lib/dentaku/print_visitor.rb
|
212
215
|
- lib/dentaku/string_casing.rb
|
213
216
|
- lib/dentaku/token.rb
|
214
217
|
- lib/dentaku/token_matcher.rb
|
@@ -216,9 +219,12 @@ files:
|
|
216
219
|
- lib/dentaku/token_scanner.rb
|
217
220
|
- lib/dentaku/tokenizer.rb
|
218
221
|
- lib/dentaku/version.rb
|
222
|
+
- lib/dentaku/visitor/infix.rb
|
219
223
|
- spec/ast/addition_spec.rb
|
224
|
+
- spec/ast/all_spec.rb
|
220
225
|
- spec/ast/and_function_spec.rb
|
221
226
|
- spec/ast/and_spec.rb
|
227
|
+
- spec/ast/any_spec.rb
|
222
228
|
- spec/ast/arithmetic_spec.rb
|
223
229
|
- spec/ast/avg_spec.rb
|
224
230
|
- spec/ast/case_spec.rb
|
@@ -235,12 +241,14 @@ files:
|
|
235
241
|
- spec/ast/node_spec.rb
|
236
242
|
- spec/ast/numeric_spec.rb
|
237
243
|
- spec/ast/or_spec.rb
|
244
|
+
- spec/ast/pluck_spec.rb
|
238
245
|
- spec/ast/round_spec.rb
|
239
246
|
- spec/ast/rounddown_spec.rb
|
240
247
|
- spec/ast/roundup_spec.rb
|
241
248
|
- spec/ast/string_functions_spec.rb
|
242
249
|
- spec/ast/sum_spec.rb
|
243
250
|
- spec/ast/switch_spec.rb
|
251
|
+
- spec/ast/xor_spec.rb
|
244
252
|
- spec/benchmark.rb
|
245
253
|
- spec/bulk_expression_solver_spec.rb
|
246
254
|
- spec/calculator_spec.rb
|
@@ -248,16 +256,19 @@ files:
|
|
248
256
|
- spec/exceptions_spec.rb
|
249
257
|
- spec/external_function_spec.rb
|
250
258
|
- spec/parser_spec.rb
|
259
|
+
- spec/print_visitor_spec.rb
|
251
260
|
- spec/spec_helper.rb
|
252
261
|
- spec/token_matcher_spec.rb
|
253
262
|
- spec/token_scanner_spec.rb
|
254
263
|
- spec/token_spec.rb
|
255
264
|
- spec/tokenizer_spec.rb
|
265
|
+
- spec/visitor/infix_spec.rb
|
266
|
+
- spec/visitor_spec.rb
|
256
267
|
homepage: http://github.com/rubysolo/dentaku
|
257
268
|
licenses:
|
258
269
|
- MIT
|
259
270
|
metadata: {}
|
260
|
-
post_install_message:
|
271
|
+
post_install_message:
|
261
272
|
rdoc_options: []
|
262
273
|
require_paths:
|
263
274
|
- lib
|
@@ -272,14 +283,16 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
272
283
|
- !ruby/object:Gem::Version
|
273
284
|
version: '0'
|
274
285
|
requirements: []
|
275
|
-
rubygems_version: 3.
|
276
|
-
signing_key:
|
286
|
+
rubygems_version: 3.3.9
|
287
|
+
signing_key:
|
277
288
|
specification_version: 4
|
278
289
|
summary: A formula language parser and evaluator
|
279
290
|
test_files:
|
280
291
|
- spec/ast/addition_spec.rb
|
292
|
+
- spec/ast/all_spec.rb
|
281
293
|
- spec/ast/and_function_spec.rb
|
282
294
|
- spec/ast/and_spec.rb
|
295
|
+
- spec/ast/any_spec.rb
|
283
296
|
- spec/ast/arithmetic_spec.rb
|
284
297
|
- spec/ast/avg_spec.rb
|
285
298
|
- spec/ast/case_spec.rb
|
@@ -296,12 +309,14 @@ test_files:
|
|
296
309
|
- spec/ast/node_spec.rb
|
297
310
|
- spec/ast/numeric_spec.rb
|
298
311
|
- spec/ast/or_spec.rb
|
312
|
+
- spec/ast/pluck_spec.rb
|
299
313
|
- spec/ast/round_spec.rb
|
300
314
|
- spec/ast/rounddown_spec.rb
|
301
315
|
- spec/ast/roundup_spec.rb
|
302
316
|
- spec/ast/string_functions_spec.rb
|
303
317
|
- spec/ast/sum_spec.rb
|
304
318
|
- spec/ast/switch_spec.rb
|
319
|
+
- spec/ast/xor_spec.rb
|
305
320
|
- spec/benchmark.rb
|
306
321
|
- spec/bulk_expression_solver_spec.rb
|
307
322
|
- spec/calculator_spec.rb
|
@@ -309,8 +324,11 @@ test_files:
|
|
309
324
|
- spec/exceptions_spec.rb
|
310
325
|
- spec/external_function_spec.rb
|
311
326
|
- spec/parser_spec.rb
|
327
|
+
- spec/print_visitor_spec.rb
|
312
328
|
- spec/spec_helper.rb
|
313
329
|
- spec/token_matcher_spec.rb
|
314
330
|
- spec/token_scanner_spec.rb
|
315
331
|
- spec/token_spec.rb
|
316
332
|
- spec/tokenizer_spec.rb
|
333
|
+
- spec/visitor/infix_spec.rb
|
334
|
+
- spec/visitor_spec.rb
|