dentaku 3.4.2 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +3 -4
  4. data/lib/dentaku/ast/access.rb +6 -0
  5. data/lib/dentaku/ast/arithmetic.rb +5 -1
  6. data/lib/dentaku/ast/array.rb +4 -0
  7. data/lib/dentaku/ast/bitwise.rb +8 -0
  8. data/lib/dentaku/ast/case/case_conditional.rb +4 -0
  9. data/lib/dentaku/ast/case/case_else.rb +6 -0
  10. data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
  11. data/lib/dentaku/ast/case/case_then.rb +6 -0
  12. data/lib/dentaku/ast/case/case_when.rb +10 -0
  13. data/lib/dentaku/ast/case.rb +6 -0
  14. data/lib/dentaku/ast/comparators.rb +25 -35
  15. data/lib/dentaku/ast/function.rb +6 -8
  16. data/lib/dentaku/ast/functions/all.rb +4 -17
  17. data/lib/dentaku/ast/functions/any.rb +4 -17
  18. data/lib/dentaku/ast/functions/duration.rb +2 -2
  19. data/lib/dentaku/ast/functions/enum.rb +37 -0
  20. data/lib/dentaku/ast/functions/filter.rb +4 -17
  21. data/lib/dentaku/ast/functions/if.rb +4 -0
  22. data/lib/dentaku/ast/functions/map.rb +3 -16
  23. data/lib/dentaku/ast/functions/pluck.rb +8 -7
  24. data/lib/dentaku/ast/functions/ruby_math.rb +3 -2
  25. data/lib/dentaku/ast/functions/xor.rb +44 -0
  26. data/lib/dentaku/ast/identifier.rb +8 -0
  27. data/lib/dentaku/ast/literal.rb +10 -0
  28. data/lib/dentaku/ast/negation.rb +4 -0
  29. data/lib/dentaku/ast/nil.rb +4 -0
  30. data/lib/dentaku/ast/node.rb +4 -0
  31. data/lib/dentaku/ast/operation.rb +9 -0
  32. data/lib/dentaku/ast/string.rb +7 -0
  33. data/lib/dentaku/ast.rb +2 -0
  34. data/lib/dentaku/parser.rb +5 -3
  35. data/lib/dentaku/print_visitor.rb +101 -0
  36. data/lib/dentaku/token_scanner.rb +1 -1
  37. data/lib/dentaku/version.rb +1 -1
  38. data/lib/dentaku/visitor/infix.rb +82 -0
  39. data/spec/ast/all_spec.rb +25 -0
  40. data/spec/ast/any_spec.rb +23 -0
  41. data/spec/ast/comparator_spec.rb +6 -9
  42. data/spec/ast/filter_spec.rb +7 -0
  43. data/spec/ast/function_spec.rb +5 -0
  44. data/spec/ast/map_spec.rb +12 -0
  45. data/spec/ast/pluck_spec.rb +32 -0
  46. data/spec/ast/xor_spec.rb +35 -0
  47. data/spec/calculator_spec.rb +58 -2
  48. data/spec/parser_spec.rb +18 -3
  49. data/spec/print_visitor_spec.rb +66 -0
  50. data/spec/tokenizer_spec.rb +6 -0
  51. data/spec/visitor/infix_spec.rb +31 -0
  52. data/spec/visitor_spec.rb +137 -0
  53. 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
@@ -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.2
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: 2021-07-16 00:00:00.000000000 Z
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.1.4
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