hayadentaku 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.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rspec.yml +26 -0
  3. data/.github/workflows/rubocop.yml +14 -0
  4. data/.gitignore +14 -0
  5. data/.pryrc +2 -0
  6. data/.rubocop.yml +114 -0
  7. data/.travis.yml +10 -0
  8. data/CHANGELOG.md +328 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE +21 -0
  11. data/README.md +352 -0
  12. data/Rakefile +31 -0
  13. data/hayadentaku.gemspec +35 -0
  14. data/lib/dentaku/ast/access.rb +44 -0
  15. data/lib/dentaku/ast/arithmetic.rb +292 -0
  16. data/lib/dentaku/ast/array.rb +38 -0
  17. data/lib/dentaku/ast/bitwise.rb +42 -0
  18. data/lib/dentaku/ast/case/case_conditional.rb +38 -0
  19. data/lib/dentaku/ast/case/case_else.rb +35 -0
  20. data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
  21. data/lib/dentaku/ast/case/case_then.rb +35 -0
  22. data/lib/dentaku/ast/case/case_when.rb +39 -0
  23. data/lib/dentaku/ast/case.rb +93 -0
  24. data/lib/dentaku/ast/combinators.rb +50 -0
  25. data/lib/dentaku/ast/comparators.rb +88 -0
  26. data/lib/dentaku/ast/datetime.rb +8 -0
  27. data/lib/dentaku/ast/function.rb +56 -0
  28. data/lib/dentaku/ast/function_registry.rb +107 -0
  29. data/lib/dentaku/ast/functions/abs.rb +5 -0
  30. data/lib/dentaku/ast/functions/all.rb +19 -0
  31. data/lib/dentaku/ast/functions/and.rb +25 -0
  32. data/lib/dentaku/ast/functions/any.rb +19 -0
  33. data/lib/dentaku/ast/functions/avg.rb +13 -0
  34. data/lib/dentaku/ast/functions/count.rb +26 -0
  35. data/lib/dentaku/ast/functions/duration.rb +51 -0
  36. data/lib/dentaku/ast/functions/enum.rb +54 -0
  37. data/lib/dentaku/ast/functions/filter.rb +21 -0
  38. data/lib/dentaku/ast/functions/if.rb +47 -0
  39. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  40. data/lib/dentaku/ast/functions/map.rb +19 -0
  41. data/lib/dentaku/ast/functions/max.rb +5 -0
  42. data/lib/dentaku/ast/functions/min.rb +5 -0
  43. data/lib/dentaku/ast/functions/mul.rb +12 -0
  44. data/lib/dentaku/ast/functions/not.rb +5 -0
  45. data/lib/dentaku/ast/functions/or.rb +25 -0
  46. data/lib/dentaku/ast/functions/pluck.rb +34 -0
  47. data/lib/dentaku/ast/functions/reduce.rb +60 -0
  48. data/lib/dentaku/ast/functions/round.rb +5 -0
  49. data/lib/dentaku/ast/functions/rounddown.rb +8 -0
  50. data/lib/dentaku/ast/functions/roundup.rb +8 -0
  51. data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
  52. data/lib/dentaku/ast/functions/string_functions.rb +212 -0
  53. data/lib/dentaku/ast/functions/sum.rb +12 -0
  54. data/lib/dentaku/ast/functions/switch.rb +8 -0
  55. data/lib/dentaku/ast/functions/xor.rb +44 -0
  56. data/lib/dentaku/ast/grouping.rb +23 -0
  57. data/lib/dentaku/ast/identifier.rb +52 -0
  58. data/lib/dentaku/ast/literal.rb +30 -0
  59. data/lib/dentaku/ast/logical.rb +8 -0
  60. data/lib/dentaku/ast/negation.rb +54 -0
  61. data/lib/dentaku/ast/nil.rb +13 -0
  62. data/lib/dentaku/ast/node.rb +29 -0
  63. data/lib/dentaku/ast/numeric.rb +8 -0
  64. data/lib/dentaku/ast/operation.rb +44 -0
  65. data/lib/dentaku/ast/string.rb +15 -0
  66. data/lib/dentaku/ast.rb +42 -0
  67. data/lib/dentaku/bulk_expression_solver.rb +158 -0
  68. data/lib/dentaku/calculator.rb +192 -0
  69. data/lib/dentaku/date_arithmetic.rb +60 -0
  70. data/lib/dentaku/dependency_resolver.rb +29 -0
  71. data/lib/dentaku/exceptions.rb +116 -0
  72. data/lib/dentaku/flat_hash.rb +161 -0
  73. data/lib/dentaku/parser.rb +318 -0
  74. data/lib/dentaku/print_visitor.rb +112 -0
  75. data/lib/dentaku/string_casing.rb +7 -0
  76. data/lib/dentaku/token.rb +48 -0
  77. data/lib/dentaku/token_matcher.rb +138 -0
  78. data/lib/dentaku/token_matchers.rb +29 -0
  79. data/lib/dentaku/token_scanner.rb +240 -0
  80. data/lib/dentaku/tokenizer.rb +127 -0
  81. data/lib/dentaku/version.rb +3 -0
  82. data/lib/dentaku/visitor/infix.rb +86 -0
  83. data/lib/dentaku.rb +69 -0
  84. data/spec/ast/abs_spec.rb +26 -0
  85. data/spec/ast/addition_spec.rb +67 -0
  86. data/spec/ast/all_spec.rb +38 -0
  87. data/spec/ast/and_function_spec.rb +35 -0
  88. data/spec/ast/and_spec.rb +32 -0
  89. data/spec/ast/any_spec.rb +36 -0
  90. data/spec/ast/arithmetic_spec.rb +147 -0
  91. data/spec/ast/avg_spec.rb +42 -0
  92. data/spec/ast/case_spec.rb +84 -0
  93. data/spec/ast/comparator_spec.rb +87 -0
  94. data/spec/ast/count_spec.rb +40 -0
  95. data/spec/ast/division_spec.rb +64 -0
  96. data/spec/ast/filter_spec.rb +25 -0
  97. data/spec/ast/function_spec.rb +69 -0
  98. data/spec/ast/intercept_spec.rb +30 -0
  99. data/spec/ast/map_spec.rb +40 -0
  100. data/spec/ast/max_spec.rb +33 -0
  101. data/spec/ast/min_spec.rb +33 -0
  102. data/spec/ast/mul_spec.rb +43 -0
  103. data/spec/ast/negation_spec.rb +48 -0
  104. data/spec/ast/node_spec.rb +43 -0
  105. data/spec/ast/numeric_spec.rb +16 -0
  106. data/spec/ast/or_spec.rb +35 -0
  107. data/spec/ast/pluck_spec.rb +49 -0
  108. data/spec/ast/reduce_spec.rb +22 -0
  109. data/spec/ast/round_spec.rb +35 -0
  110. data/spec/ast/rounddown_spec.rb +35 -0
  111. data/spec/ast/roundup_spec.rb +35 -0
  112. data/spec/ast/string_functions_spec.rb +217 -0
  113. data/spec/ast/sum_spec.rb +43 -0
  114. data/spec/ast/switch_spec.rb +30 -0
  115. data/spec/ast/xor_spec.rb +35 -0
  116. data/spec/benchmark.rb +70 -0
  117. data/spec/bulk_expression_solver_spec.rb +241 -0
  118. data/spec/calculator_spec.rb +1003 -0
  119. data/spec/dentaku_spec.rb +52 -0
  120. data/spec/dependency_resolver_spec.rb +18 -0
  121. data/spec/exceptions_spec.rb +9 -0
  122. data/spec/external_function_spec.rb +177 -0
  123. data/spec/parser_spec.rb +183 -0
  124. data/spec/print_visitor_spec.rb +77 -0
  125. data/spec/spec_helper.rb +69 -0
  126. data/spec/token_matcher_spec.rb +134 -0
  127. data/spec/token_scanner_spec.rb +49 -0
  128. data/spec/token_spec.rb +16 -0
  129. data/spec/tokenizer_spec.rb +375 -0
  130. data/spec/visitor/infix_spec.rb +52 -0
  131. data/spec/visitor_spec.rb +139 -0
  132. metadata +353 -0
@@ -0,0 +1,52 @@
1
+ require 'dentaku'
2
+
3
+ describe Dentaku do
4
+ it 'evaulates an expression' do
5
+ expect(Dentaku('5+3')).to eql(8)
6
+ end
7
+
8
+ it 'binds values to variables' do
9
+ expect(Dentaku('oranges > 7', oranges: 10)).to be_truthy
10
+ end
11
+
12
+ it 'evaulates a nested function' do
13
+ expect(Dentaku('roundup(roundup(3 * cherries) + raspberries)', cherries: 1.5, raspberries: 0.9)).to eql(6)
14
+ end
15
+
16
+ it 'treats variables as case-insensitive' do
17
+ expect(Dentaku('40 + N', 'n' => 2)).to eql(42)
18
+ expect(Dentaku('40 + N', 'N' => 2)).to eql(42)
19
+ expect(Dentaku('40 + n', 'N' => 2)).to eql(42)
20
+ expect(Dentaku('40 + n', 'n' => 2)).to eql(42)
21
+ end
22
+
23
+ it 'raises a parse error for bad logic expressions' do
24
+ expect {
25
+ Dentaku!('true AND')
26
+ }.to raise_error(Dentaku::ParseError)
27
+ end
28
+
29
+ it 'evaluates with class-level shortcut functions' do
30
+ expect(described_class.evaluate('2+2')).to eq(4)
31
+ expect(described_class.evaluate!('2+2')).to eq(4)
32
+ expect { described_class.evaluate!('a+1') }.to raise_error(Dentaku::UnboundVariableError)
33
+ end
34
+
35
+ it 'accepts a block for custom handling of unbound variables' do
36
+ unbound = 'apples * 1.5'
37
+ expect(described_class.evaluate(unbound) { :bar }).to eq(:bar)
38
+ expect(described_class.evaluate(unbound) { |e| e }).to eq(unbound)
39
+ end
40
+
41
+ it 'evaluates with class-level aliases' do
42
+ described_class.aliases = { roundup: ['roundupup'] }
43
+ expect(described_class.evaluate('roundupup(6.1)')).to eq(7)
44
+ end
45
+
46
+ it 'sets caching opt-in flags' do
47
+ expect {
48
+ described_class.enable_caching!
49
+ }.to change { described_class.cache_ast? }.from(false).to(true)
50
+ .and change { described_class.cache_dependency_order? }.from(false).to(true)
51
+ end
52
+ end
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/dependency_resolver'
3
+
4
+ describe Dentaku::DependencyResolver do
5
+ it 'sorts expressions in dependency order' do
6
+ dependencies = {"first" => ["second"], "second" => ["third"], "third" => []}
7
+ expect(described_class.find_resolve_order(dependencies)).to eq(
8
+ ["third", "second", "first"]
9
+ )
10
+ end
11
+
12
+ it 'handles case differences' do
13
+ dependencies = {"FIRST" => ["second"], "SeCoNd" => ["third"], "THIRD" => []}
14
+ expect(described_class.find_resolve_order(dependencies)).to eq(
15
+ ["THIRD", "SeCoNd", "FIRST"]
16
+ )
17
+ end
18
+ end
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/exceptions'
3
+
4
+ describe Dentaku::UnboundVariableError do
5
+ it 'includes variable name(s) in message' do
6
+ exception = described_class.new(['length'])
7
+ expect(exception.unbound_variables).to include('length')
8
+ end
9
+ end
@@ -0,0 +1,177 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/calculator'
4
+
5
+ describe Dentaku::Calculator do
6
+ describe 'functions' do
7
+ describe 'external functions' do
8
+ let(:custom_calculator) do
9
+ c = described_class.new
10
+
11
+ c.add_function(:now, :string, -> { Time.now.to_s })
12
+
13
+ fns = [
14
+ [:pow, :numeric, ->(mantissa, exponent) { mantissa**exponent }],
15
+ [:biggest, :numeric, ->(*args) { args.max }],
16
+ [:smallest, :numeric, ->(*args) { args.min }],
17
+ [:optional, :numeric, ->(x, y, z = 0) { x + y + z }],
18
+ ]
19
+
20
+ c.add_functions(fns)
21
+ end
22
+
23
+ it 'includes NOW' do
24
+ now = custom_calculator.evaluate('NOW()')
25
+ expect(now).not_to be_nil
26
+ expect(now).not_to be_empty
27
+ end
28
+
29
+ it 'includes POW' do
30
+ expect(custom_calculator.evaluate('POW(2,3)')).to eq(8)
31
+ expect(custom_calculator.evaluate('POW(3,2)')).to eq(9)
32
+ expect(custom_calculator.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
33
+ end
34
+
35
+ it 'includes BIGGEST' do
36
+ expect(custom_calculator.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
37
+ end
38
+
39
+ it 'includes SMALLEST' do
40
+ expect(custom_calculator.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
41
+ end
42
+
43
+ it 'includes OPTIONAL' do
44
+ expect(custom_calculator.evaluate('OPTIONAL(1,2)')).to eq(3)
45
+ expect(custom_calculator.evaluate('OPTIONAL(1,2,3)')).to eq(6)
46
+ expect { custom_calculator.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
47
+ expect { custom_calculator.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
48
+ end
49
+
50
+ it 'supports array parameters' do
51
+ calculator = described_class.new
52
+ calculator.add_function(
53
+ :includes,
54
+ :logical,
55
+ ->(haystack, needle) {
56
+ haystack.include?(needle)
57
+ }
58
+ )
59
+
60
+ expect(calculator.evaluate("INCLUDES(list, 2)", list: [1, 2, 3])).to eq(true)
61
+ end
62
+ end
63
+
64
+ describe 'with callbacks' do
65
+ let(:custom_calculator) do
66
+ c = described_class.new
67
+
68
+ @counts = Hash.new(0)
69
+
70
+ @initial_time = "2023-02-03"
71
+ @last_time = @initial_time
72
+
73
+ c.add_function(
74
+ :reverse,
75
+ :stringl,
76
+ ->(a) { a.reverse },
77
+ lambda do |args|
78
+ args.each do |arg|
79
+ @counts[arg.value] += 1 if arg.type == :string
80
+ end
81
+ end
82
+ )
83
+
84
+ fns = [
85
+ [:biggest_callback, :numeric, ->(*args) { args.max }, ->(args) { args.each { |arg| raise Dentaku::ArgumentError unless arg.type == :numeric } }],
86
+ [:pythagoras, :numeric, ->(l1, l2) { Math.sqrt(l1**2 + l2**2) }, ->(e) { @last_time = Time.now.to_s }],
87
+ [:callback_lambda, :string, ->() { " " }, ->() { "lambda executed" }],
88
+ [:no_lambda_function, :numeric, ->(a) { a**a }],
89
+ ]
90
+
91
+ c.add_functions(fns)
92
+ end
93
+
94
+ it 'includes BIGGEST_CALLBACK' do
95
+ expect(custom_calculator.evaluate('BIGGEST_CALLBACK(1, 2, 5, 4)')).to eq(5)
96
+ expect { custom_calculator.dependencies('BIGGEST_CALLBACK(1, 3, 6, "hi", 10)') }.to raise_error(Dentaku::ArgumentError)
97
+ end
98
+
99
+ it 'includes REVERSE' do
100
+ expect(custom_calculator.evaluate('REVERSE(\'Dentaku\')')).to eq('ukatneD')
101
+ expect { custom_calculator.evaluate('REVERSE(22)') }.to raise_error(NoMethodError)
102
+ expect(@counts["Dentaku"]).to eq(1)
103
+ end
104
+
105
+ it 'includes PYTHAGORAS' do
106
+ expect(custom_calculator.evaluate('PYTHAGORAS(8, 7)')).to eq(10.63014581273465)
107
+ expect(custom_calculator.evaluate('PYTHAGORAS(3, 4)')).to eq(5)
108
+ expect(@last_time).not_to eq(@initial_time)
109
+ end
110
+
111
+ it 'exposes the `callback` method of a function' do
112
+ expect(Dentaku::AST::Function::Callback_lambda.callback.call()).to eq("lambda executed")
113
+ end
114
+
115
+ it 'does not add a `callback` method to built-in functions' do
116
+ expect { Dentaku::AST::If.callback.call }.to raise_error(NoMethodError)
117
+ end
118
+
119
+ it 'defaults `callback` method to nil if not specified' do
120
+ expect(Dentaku::AST::Function::No_lambda_function.callback).to eq(nil)
121
+ end
122
+ end
123
+
124
+ it 'allows registering "bang" functions' do
125
+ calculator = described_class.new
126
+ calculator.add_function(:hey!, :string, -> { "hey!" })
127
+ expect(calculator.evaluate("hey!()")).to eq("hey!")
128
+ end
129
+
130
+ it 'defines for a given function a properly named class that represents it to support AST marshaling' do
131
+ calculator = described_class.new
132
+ expect {
133
+ calculator.add_function(:ho, :string, -> {})
134
+ }.to change {
135
+ Dentaku::AST::Function.const_defined?("Ho")
136
+ }.from(false).to(true)
137
+
138
+ expect {
139
+ Marshal.dump(calculator.ast('MAX(1, 2)'))
140
+ }.not_to raise_error
141
+ end
142
+
143
+ it 'does not store functions across all calculators' do
144
+ calculator1 = described_class.new
145
+ calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
146
+
147
+ calculator2 = described_class.new
148
+ calculator2.add_function(:my_function, :numeric, ->(x) { 4 * x + 3 })
149
+
150
+ expect(calculator1.evaluate!("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
151
+ expect(calculator2.evaluate!("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
152
+
153
+ expect {
154
+ described_class.new.evaluate!("1 + my_function(2)")
155
+ }.to raise_error(Dentaku::ParseError)
156
+ end
157
+
158
+ describe 'Dentaku::Calculator.add_function' do
159
+ it 'adds a function to default/global function registry' do
160
+ described_class.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
161
+ expect(described_class.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
162
+ end
163
+ end
164
+
165
+ describe 'Dentaku::Calculator.add_functions' do
166
+ it 'adds multiple functions to default/global function registry' do
167
+ described_class.add_functions([
168
+ [:cube, :numeric, ->(x) { x**3 }],
169
+ [:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c, i| i.even? ? c.upcase : c.downcase }.join() }],
170
+ ])
171
+
172
+ expect(described_class.new.evaluate("1 + cube(3)")).to eq(28)
173
+ expect(described_class.new.evaluate("spongebob('How are you today?')")).to eq("HoW ArE YoU ToDaY?")
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,183 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/token'
3
+ require 'dentaku/tokenizer'
4
+ require 'dentaku/parser'
5
+
6
+ describe Dentaku::Parser do
7
+ it 'parses an integer literal' do
8
+ node = parse('5')
9
+ expect(node.value).to eq(5)
10
+ end
11
+
12
+ it 'performs simple addition' do
13
+ node = parse('5 + 4')
14
+ expect(node.value).to eq(9)
15
+ end
16
+
17
+ it 'compares two numbers' do
18
+ node = parse('5 < 4')
19
+ expect(node.value).to eq(false)
20
+ end
21
+
22
+ it 'calculates unary percentage' do
23
+ node = parse('5%')
24
+ expect(node.value).to eq(0.05)
25
+ end
26
+
27
+ it 'calculates bitwise OR' do
28
+ node = parse('2|3')
29
+ expect(node.value).to eq(3)
30
+
31
+ node = parse('(5 | 2) + 1')
32
+ expect(node.value).to eq(8)
33
+ end
34
+
35
+ it 'performs multiple operations in one stream' do
36
+ node = parse('5 * 4 + 3')
37
+ expect(node.value).to eq(23)
38
+ end
39
+
40
+ it 'respects order of operations' do
41
+ node = parse('5 + 4*3')
42
+ expect(node.value).to eq(17)
43
+ end
44
+
45
+ it 'respects grouping by parenthesis' do
46
+ node = parse('(5 + 4) * 3')
47
+ expect(node.value).to eq(27)
48
+ end
49
+
50
+ it 'evaluates functions' do
51
+ node = parse('IF(5 < 4, 3, 2)')
52
+ expect(node.value).to eq(2)
53
+ end
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
+
60
+ it 'represents formulas with variables' do
61
+ node = parse('5 * x')
62
+ expect { node.value }.to raise_error(Dentaku::UnboundVariableError)
63
+ expect(node.value("x" => 3)).to eq(15)
64
+ end
65
+
66
+ it 'evaluates access into data structures' do
67
+ node = parse('a[1]')
68
+ expect { node.value }.to raise_error(Dentaku::UnboundVariableError)
69
+ expect(node.value("a" => [1, 2, 3])).to eq(2)
70
+ end
71
+
72
+ it 'evaluates boolean expressions' do
73
+ node = parse('true AND false')
74
+ expect(node.value).to eq(false)
75
+ end
76
+
77
+ it 'evaluates a case statement' do
78
+ node = parse('CASE x WHEN 1 THEN 2 WHEN 3 THEN 4 END')
79
+ expect(node.value("x" => 3)).to eq(4)
80
+ end
81
+
82
+ it 'evaluates a nested case statement with case-sensitivity' do
83
+ 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 })
84
+ expect(node.value("x" => 1, "y" => "A", "Y" => "B")).to eq(3)
85
+ end
86
+
87
+ it 'evaluates arrays' do
88
+ node = parse('{}')
89
+ expect(node.value).to eq([])
90
+
91
+ node = parse('{1, 2, 3}')
92
+ expect(node.value).to eq([1, 2, 3])
93
+
94
+ node = parse('{1, 2, 3} + {4,5,6}')
95
+ expect(node.value).to eq([1, 2, 3, 4, 5, 6])
96
+
97
+ node = parse('{1, 2, 3} - {2,3}')
98
+ expect(node.value).to eq([1])
99
+ end
100
+
101
+ context 'invalid expression' do
102
+ it 'raises a parse error for bad math' do
103
+ expect {
104
+ parse("5 * -")
105
+ }.to raise_error(Dentaku::ParseError)
106
+ end
107
+
108
+ it 'raises a parse error for bad logic' do
109
+ expect {
110
+ parse("TRUE AND")
111
+ }.to raise_error(Dentaku::ParseError)
112
+ end
113
+
114
+ it 'raises a parse error for too many operands' do
115
+ expect {
116
+ parse("IF(1, 0, IF(1, 2, 3, 4))")
117
+ }.to raise_error(Dentaku::ParseError)
118
+
119
+ expect {
120
+ parse("CASE a WHEN 1 THEN true ELSE THEN IF(1, 2, 3, 4) END")
121
+ }.to raise_error(Dentaku::ParseError)
122
+ end
123
+
124
+ it 'raises a parse error for bad grouping structure' do
125
+ expect {
126
+ parse(",")
127
+ }.to raise_error(Dentaku::ParseError)
128
+
129
+ expect {
130
+ parse("5, x")
131
+ described_class.new([five, comma, x]).parse
132
+ }.to raise_error(Dentaku::ParseError)
133
+
134
+ expect {
135
+ parse("5 + 5, x")
136
+ }.to raise_error(Dentaku::ParseError)
137
+
138
+ expect {
139
+ parse("{1, 2, }")
140
+ }.to raise_error(Dentaku::ParseError)
141
+
142
+ expect {
143
+ parse("CONCAT('1', '2', )")
144
+ }.to raise_error(Dentaku::ParseError)
145
+ end
146
+
147
+ it 'raises parse errors for malformed case statements' do
148
+ expect {
149
+ parse("CASE a when 'one' then 1")
150
+ }.to raise_error(Dentaku::ParseError)
151
+
152
+ expect {
153
+ parse("case a whend 'one' then 1 end")
154
+ }.to raise_error(Dentaku::ParseError)
155
+
156
+ expect {
157
+ parse("CASE a WHEN 'one' THEND 1 END")
158
+ }.to raise_error(Dentaku::ParseError)
159
+
160
+ expect {
161
+ parse("CASE a when 'one' then end")
162
+ }.to raise_error(Dentaku::ParseError)
163
+ end
164
+
165
+ it 'raises a parse error when trying to access an undefined function' do
166
+ expect {
167
+ parse("undefined()")
168
+ }.to raise_error(Dentaku::ParseError)
169
+ end
170
+ end
171
+
172
+ it "evaluates explicit 'NULL' as nil" do
173
+ node = parse("NULL")
174
+ expect(node.value).to eq(nil)
175
+ end
176
+
177
+ private
178
+
179
+ def parse(expr, parser_options = {}, tokenizer_options = {})
180
+ tokens = Dentaku::Tokenizer.new.tokenize(expr, tokenizer_options)
181
+ described_class.new(tokens, parser_options).parse
182
+ end
183
+ end
@@ -0,0 +1,77 @@
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 'handles grouping correctly' do
12
+ formula = '10 - (0 - 10)'
13
+ repr = roundtrip(formula)
14
+ expect(repr).to eq(formula)
15
+ end
16
+
17
+ it 'quotes string literals' do
18
+ repr = roundtrip('Concat(\'a\', "B")')
19
+ expect(repr).to eq('CONCAT("a", "B")')
20
+ end
21
+
22
+ it 'handles unary operations on literals' do
23
+ repr = roundtrip('- 4')
24
+ expect(repr).to eq('-4')
25
+ end
26
+
27
+ it 'handles unary operations on trees' do
28
+ repr = roundtrip('- (5 + 5)')
29
+ expect(repr).to eq('-(5 + 5)')
30
+ end
31
+
32
+ it 'handles a complex arithmetic expression' do
33
+ repr = roundtrip('(((1 + 7) * (8 ^ 2)) / - (3.0 - apples))')
34
+ expect(repr).to eq('(1 + 7) * 8 ^ 2 / -(3.0 - apples)')
35
+ end
36
+
37
+ it 'handles a complex logical expression' do
38
+ repr = roundtrip('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
39
+ expect(repr).to eq('1 < 2 and 3 <= 4 or 5 > 6 and 7 >= 8 or 9 != 10 and true')
40
+ end
41
+
42
+ it 'handles a function call' do
43
+ repr = roundtrip('IF(a[0] = NULL, "five", \'seven\')')
44
+ expect(repr).to eq('IF(a[0] = NULL, "five", "seven")')
45
+ end
46
+
47
+ it 'handles a case statement' do
48
+ repr = roundtrip('case (a % 5) when 0 then a else b end')
49
+ expect(repr).to eq('CASE a % 5 WHEN 0 THEN a ELSE b END')
50
+ end
51
+
52
+ it 'handles a bitwise operators' do
53
+ repr = roundtrip('0xCAFE & 0xDECAF | 0xBEEF')
54
+ expect(repr).to eq('0xCAFE & 0xDECAF | 0xBEEF')
55
+ end
56
+
57
+ it 'handles a datetime literal' do
58
+ repr = roundtrip('2017-12-24 23:59:59')
59
+ expect(repr).to eq('2017-12-24 23:59:59')
60
+ end
61
+
62
+ it 'handles a percentage in a formula' do
63
+ repr = roundtrip('((3*4%) * 0.001)')
64
+ expect(repr).to eq('3 * 4% * 0.001')
65
+ end
66
+
67
+ private
68
+
69
+ def roundtrip(string)
70
+ described_class.new(parsed(string)).to_s
71
+ end
72
+
73
+ def parsed(string)
74
+ tokens = Dentaku::Tokenizer.new.tokenize(string)
75
+ Dentaku::Parser.new(tokens).parse
76
+ end
77
+ end
@@ -0,0 +1,69 @@
1
+ require 'pry'
2
+ require 'simplecov'
3
+
4
+ SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
5
+ SimpleCov::Formatter::HTMLFormatter,
6
+ ])
7
+
8
+ SimpleCov.minimum_coverage 90
9
+ # SimpleCov.minimum_coverage_by_file 80
10
+
11
+ SimpleCov.start do
12
+ add_filter "spec/"
13
+ end
14
+
15
+ RSpec.configure do |c|
16
+ c.before(:all) {
17
+ if Dentaku.respond_to?(:aliases=)
18
+ # add example for alias because we can set aliases just once
19
+ # before `calculator` method called
20
+ Dentaku.aliases = { roundup: ['roundupup'] }
21
+ end
22
+ }
23
+ end
24
+
25
+ # automatically create a token stream from bare values
26
+ def token_stream(*args)
27
+ args.map do |value|
28
+ type = type_for(value)
29
+ Dentaku::Token.new(type, value)
30
+ end
31
+ end
32
+
33
+ # make a (hopefully intelligent) guess about type
34
+ def type_for(value)
35
+ case value
36
+ when Numeric
37
+ :numeric
38
+ when String
39
+ :string
40
+ when true, false
41
+ :logical
42
+ when :add, :subtract, :multiply, :divide, :mod, :pow
43
+ :operator
44
+ when :open, :close, :comma
45
+ :grouping
46
+ when :lbracket, :rbracket
47
+ :access
48
+ when :le, :ge, :ne, :lt, :gt, :eq
49
+ :comparator
50
+ when :and, :or
51
+ :combinator
52
+ when :if, :round, :roundup, :rounddown, :not
53
+ :function
54
+ else
55
+ :identifier
56
+ end
57
+ end
58
+
59
+ def identifier(name)
60
+ Dentaku::AST::Identifier.new(token(name))
61
+ end
62
+
63
+ def literal(value)
64
+ Dentaku::AST::Literal.new(token(value))
65
+ end
66
+
67
+ def token(value)
68
+ Dentaku::Token.new(type_for(value), value)
69
+ end