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.
- checksums.yaml +7 -0
- data/.github/workflows/rspec.yml +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/.gitignore +14 -0
- data/.pryrc +2 -0
- data/.rubocop.yml +114 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +328 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +352 -0
- data/Rakefile +31 -0
- data/hayadentaku.gemspec +35 -0
- data/lib/dentaku/ast/access.rb +44 -0
- data/lib/dentaku/ast/arithmetic.rb +292 -0
- data/lib/dentaku/ast/array.rb +38 -0
- data/lib/dentaku/ast/bitwise.rb +42 -0
- data/lib/dentaku/ast/case/case_conditional.rb +38 -0
- data/lib/dentaku/ast/case/case_else.rb +35 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
- data/lib/dentaku/ast/case/case_then.rb +35 -0
- data/lib/dentaku/ast/case/case_when.rb +39 -0
- data/lib/dentaku/ast/case.rb +93 -0
- data/lib/dentaku/ast/combinators.rb +50 -0
- data/lib/dentaku/ast/comparators.rb +88 -0
- data/lib/dentaku/ast/datetime.rb +8 -0
- data/lib/dentaku/ast/function.rb +56 -0
- data/lib/dentaku/ast/function_registry.rb +107 -0
- data/lib/dentaku/ast/functions/abs.rb +5 -0
- data/lib/dentaku/ast/functions/all.rb +19 -0
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/any.rb +19 -0
- data/lib/dentaku/ast/functions/avg.rb +13 -0
- data/lib/dentaku/ast/functions/count.rb +26 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/enum.rb +54 -0
- data/lib/dentaku/ast/functions/filter.rb +21 -0
- data/lib/dentaku/ast/functions/if.rb +47 -0
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/map.rb +19 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/mul.rb +12 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/or.rb +25 -0
- data/lib/dentaku/ast/functions/pluck.rb +34 -0
- data/lib/dentaku/ast/functions/reduce.rb +60 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +8 -0
- data/lib/dentaku/ast/functions/roundup.rb +8 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
- data/lib/dentaku/ast/functions/string_functions.rb +212 -0
- data/lib/dentaku/ast/functions/sum.rb +12 -0
- data/lib/dentaku/ast/functions/switch.rb +8 -0
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/grouping.rb +23 -0
- data/lib/dentaku/ast/identifier.rb +52 -0
- data/lib/dentaku/ast/literal.rb +30 -0
- data/lib/dentaku/ast/logical.rb +8 -0
- data/lib/dentaku/ast/negation.rb +54 -0
- data/lib/dentaku/ast/nil.rb +13 -0
- data/lib/dentaku/ast/node.rb +29 -0
- data/lib/dentaku/ast/numeric.rb +8 -0
- data/lib/dentaku/ast/operation.rb +44 -0
- data/lib/dentaku/ast/string.rb +15 -0
- data/lib/dentaku/ast.rb +42 -0
- data/lib/dentaku/bulk_expression_solver.rb +158 -0
- data/lib/dentaku/calculator.rb +192 -0
- data/lib/dentaku/date_arithmetic.rb +60 -0
- data/lib/dentaku/dependency_resolver.rb +29 -0
- data/lib/dentaku/exceptions.rb +116 -0
- data/lib/dentaku/flat_hash.rb +161 -0
- data/lib/dentaku/parser.rb +318 -0
- data/lib/dentaku/print_visitor.rb +112 -0
- data/lib/dentaku/string_casing.rb +7 -0
- data/lib/dentaku/token.rb +48 -0
- data/lib/dentaku/token_matcher.rb +138 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +240 -0
- data/lib/dentaku/tokenizer.rb +127 -0
- data/lib/dentaku/version.rb +3 -0
- data/lib/dentaku/visitor/infix.rb +86 -0
- data/lib/dentaku.rb +69 -0
- data/spec/ast/abs_spec.rb +26 -0
- data/spec/ast/addition_spec.rb +67 -0
- data/spec/ast/all_spec.rb +38 -0
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +32 -0
- data/spec/ast/any_spec.rb +36 -0
- data/spec/ast/arithmetic_spec.rb +147 -0
- data/spec/ast/avg_spec.rb +42 -0
- data/spec/ast/case_spec.rb +84 -0
- data/spec/ast/comparator_spec.rb +87 -0
- data/spec/ast/count_spec.rb +40 -0
- data/spec/ast/division_spec.rb +64 -0
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +69 -0
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +40 -0
- data/spec/ast/max_spec.rb +33 -0
- data/spec/ast/min_spec.rb +33 -0
- data/spec/ast/mul_spec.rb +43 -0
- data/spec/ast/negation_spec.rb +48 -0
- data/spec/ast/node_spec.rb +43 -0
- data/spec/ast/numeric_spec.rb +16 -0
- data/spec/ast/or_spec.rb +35 -0
- data/spec/ast/pluck_spec.rb +49 -0
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/ast/round_spec.rb +35 -0
- data/spec/ast/rounddown_spec.rb +35 -0
- data/spec/ast/roundup_spec.rb +35 -0
- data/spec/ast/string_functions_spec.rb +217 -0
- data/spec/ast/sum_spec.rb +43 -0
- data/spec/ast/switch_spec.rb +30 -0
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/benchmark.rb +70 -0
- data/spec/bulk_expression_solver_spec.rb +241 -0
- data/spec/calculator_spec.rb +1003 -0
- data/spec/dentaku_spec.rb +52 -0
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/exceptions_spec.rb +9 -0
- data/spec/external_function_spec.rb +177 -0
- data/spec/parser_spec.rb +183 -0
- data/spec/print_visitor_spec.rb +77 -0
- data/spec/spec_helper.rb +69 -0
- data/spec/token_matcher_spec.rb +134 -0
- data/spec/token_scanner_spec.rb +49 -0
- data/spec/token_spec.rb +16 -0
- data/spec/tokenizer_spec.rb +375 -0
- data/spec/visitor/infix_spec.rb +52 -0
- data/spec/visitor_spec.rb +139 -0
- 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,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
|
data/spec/parser_spec.rb
ADDED
|
@@ -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
|
data/spec/spec_helper.rb
ADDED
|
@@ -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
|