dentaku_zevo 3.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.pryrc +2 -0
- data/.rubocop.yml +114 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +281 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +342 -0
- data/Rakefile +31 -0
- data/dentaku.gemspec +32 -0
- data/lib/dentaku/ast/access.rb +47 -0
- data/lib/dentaku/ast/arithmetic.rb +241 -0
- data/lib/dentaku/ast/array.rb +41 -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 +81 -0
- data/lib/dentaku/ast/combinators.rb +50 -0
- data/lib/dentaku/ast/comparators.rb +89 -0
- data/lib/dentaku/ast/datetime.rb +8 -0
- data/lib/dentaku/ast/function.rb +56 -0
- data/lib/dentaku/ast/function_registry.rb +98 -0
- data/lib/dentaku/ast/functions/all.rb +23 -0
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/any.rb +23 -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 +37 -0
- data/lib/dentaku/ast/functions/filter.rb +23 -0
- data/lib/dentaku/ast/functions/if.rb +51 -0
- data/lib/dentaku/ast/functions/map.rb +23 -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 +30 -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 +55 -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 +28 -0
- data/lib/dentaku/ast/numeric.rb +8 -0
- data/lib/dentaku/ast/operation.rb +39 -0
- data/lib/dentaku/ast/string.rb +15 -0
- data/lib/dentaku/ast.rb +39 -0
- data/lib/dentaku/bulk_expression_solver.rb +128 -0
- data/lib/dentaku/calculator.rb +169 -0
- data/lib/dentaku/date_arithmetic.rb +45 -0
- data/lib/dentaku/dependency_resolver.rb +24 -0
- data/lib/dentaku/exceptions.rb +102 -0
- data/lib/dentaku/flat_hash.rb +38 -0
- data/lib/dentaku/parser.rb +349 -0
- data/lib/dentaku/print_visitor.rb +101 -0
- data/lib/dentaku/string_casing.rb +7 -0
- data/lib/dentaku/token.rb +36 -0
- data/lib/dentaku/token_matcher.rb +138 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +183 -0
- data/lib/dentaku/tokenizer.rb +110 -0
- data/lib/dentaku/version.rb +3 -0
- data/lib/dentaku/visitor/infix.rb +82 -0
- data/lib/dentaku.rb +69 -0
- data/spec/ast/addition_spec.rb +62 -0
- data/spec/ast/all_spec.rb +25 -0
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +32 -0
- data/spec/ast/any_spec.rb +23 -0
- data/spec/ast/arithmetic_spec.rb +91 -0
- data/spec/ast/avg_spec.rb +37 -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 +35 -0
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +69 -0
- data/spec/ast/map_spec.rb +27 -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 +32 -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 +201 -0
- data/spec/calculator_spec.rb +898 -0
- data/spec/dentaku_spec.rb +52 -0
- data/spec/exceptions_spec.rb +9 -0
- data/spec/external_function_spec.rb +106 -0
- data/spec/parser_spec.rb +166 -0
- data/spec/print_visitor_spec.rb +66 -0
- data/spec/spec_helper.rb +71 -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 +359 -0
- data/spec/visitor/infix_spec.rb +31 -0
- data/spec/visitor_spec.rb +138 -0
- metadata +335 -0
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku'
|
3
|
+
require 'dentaku/ast/functions/and'
|
4
|
+
|
5
|
+
describe 'Dentaku::AST::And' do
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
8
|
+
it 'returns false if any of the arguments is false' do
|
9
|
+
result = Dentaku('AND(1 = 1, 0 = 1)')
|
10
|
+
expect(result).to eq(false)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'supports nested expressions' do
|
14
|
+
result = Dentaku('AND(y = 1, x = 1)', x: 1, y: 2)
|
15
|
+
expect(result).to eq(false)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'returns true if all of the arguments are true' do
|
19
|
+
result = Dentaku('AND(1 = 1, "2" = "2", true = true, true)')
|
20
|
+
expect(result).to eq(true)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns true if all nested AND functions return true' do
|
24
|
+
result = Dentaku('AND(AND(1 = 1), AND(true != false, AND(true)))')
|
25
|
+
expect(result).to eq(true)
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'raises an error if no arguments are passed' do
|
29
|
+
expect { calculator.evaluate!('AND()') }.to raise_error(Dentaku::ArgumentError)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'raises an error if a non logical argument is passed' do
|
33
|
+
expect { calculator.evaluate!('AND("r")') }.to raise_error(Dentaku::ArgumentError)
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/combinators'
|
3
|
+
|
4
|
+
require 'dentaku/token'
|
5
|
+
|
6
|
+
describe Dentaku::AST::And do
|
7
|
+
let(:t) { Dentaku::AST::Logical.new Dentaku::Token.new(:logical, true) }
|
8
|
+
let(:f) { Dentaku::AST::Logical.new Dentaku::Token.new(:logical, false) }
|
9
|
+
|
10
|
+
let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
|
11
|
+
|
12
|
+
it 'performs logical AND' do
|
13
|
+
node = described_class.new(t, f)
|
14
|
+
expect(node.value).to eq(false)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'requires logical operands' do
|
18
|
+
expect {
|
19
|
+
described_class.new(t, five)
|
20
|
+
}.to raise_error(Dentaku::NodeError, /requires logical operands/)
|
21
|
+
|
22
|
+
expression = Dentaku::AST::LessThanOrEqual.new(five, five)
|
23
|
+
expect {
|
24
|
+
described_class.new(t, expression)
|
25
|
+
}.not_to raise_error
|
26
|
+
|
27
|
+
expression = Dentaku::AST::Or.new(t, f)
|
28
|
+
expect {
|
29
|
+
described_class.new(t, expression)
|
30
|
+
}.not_to raise_error
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/any'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe Dentaku::AST::Any do
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
it 'performs ANY operation' do
|
8
|
+
result = Dentaku('ANY(vals, val, val > 1)', vals: [1, 2, 3])
|
9
|
+
expect(result).to eq(true)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'works with a single value if needed for some reason' do
|
13
|
+
result = Dentaku('ANY(vals, val, val > 1)', vals: 1)
|
14
|
+
expect(result).to eq(false)
|
15
|
+
|
16
|
+
result = Dentaku('ANY(vals, val, val > 1)', vals: 2)
|
17
|
+
expect(result).to eq(true)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'raises argument error if a string is passed as identifier' do
|
21
|
+
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ArgumentError)
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/arithmetic'
|
3
|
+
|
4
|
+
require 'dentaku/token'
|
5
|
+
|
6
|
+
describe Dentaku::AST::Arithmetic do
|
7
|
+
let(:one) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 1) }
|
8
|
+
let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
|
9
|
+
let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
|
10
|
+
let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
|
11
|
+
let(:ctx) { {'x' => 1, 'y' => 2} }
|
12
|
+
let(:date) { Dentaku::AST::DateTime.new Dentaku::Token.new(:datetime, DateTime.new(2020, 4, 16)) }
|
13
|
+
|
14
|
+
it 'performs an arithmetic operation with numeric operands' do
|
15
|
+
expect(add(one, two)).to eq(3)
|
16
|
+
expect(sub(one, two)).to eq(-1)
|
17
|
+
expect(mul(one, two)).to eq(2)
|
18
|
+
expect(div(one, two)).to eq(0.5)
|
19
|
+
expect(neg(one)).to eq(-1)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'performs an arithmetic operation with one numeric operand and one string operand' do
|
23
|
+
expect(add(one, x)).to eq(2)
|
24
|
+
expect(sub(one, x)).to eq(0)
|
25
|
+
expect(mul(one, x)).to eq(1)
|
26
|
+
expect(div(one, x)).to eq(1)
|
27
|
+
|
28
|
+
expect(add(y, two)).to eq(4)
|
29
|
+
expect(sub(y, two)).to eq(0)
|
30
|
+
expect(mul(y, two)).to eq(4)
|
31
|
+
expect(div(y, two)).to eq(1)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'performs an arithmetic operation with string operands' do
|
35
|
+
expect(add(x, y)).to eq(3)
|
36
|
+
expect(sub(x, y)).to eq(-1)
|
37
|
+
expect(mul(x, y)).to eq(2)
|
38
|
+
expect(div(x, y)).to eq(0.5)
|
39
|
+
expect(neg(x)).to eq(-1)
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'correctly parses string operands to numeric values' do
|
43
|
+
expect(add(x, one, 'x' => '1')).to eq(2)
|
44
|
+
expect(add(x, one, 'x' => '1.1')).to eq(2.1)
|
45
|
+
expect(add(x, one, 'x' => '.1')).to eq(1.1)
|
46
|
+
expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
|
47
|
+
expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
|
48
|
+
|
49
|
+
int_one = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, "1")
|
50
|
+
decimal_one = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, "1.0")
|
51
|
+
|
52
|
+
expect(add(int_one, int_one).class).to eq(Integer)
|
53
|
+
expect(add(int_one, decimal_one).class).to eq(BigDecimal)
|
54
|
+
expect(add(decimal_one, decimal_one).class).to eq(BigDecimal)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'performs arithmetic on arrays' do
|
58
|
+
expect(add(x, y, 'x' => [1], 'y' => [2])).to eq([1, 2])
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'performs date arithmetic' do
|
62
|
+
expect(add(date, one)).to eq(DateTime.new(2020, 4, 17))
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'raises ArgumentError if given individually valid but incompatible arguments' do
|
66
|
+
expect { add(one, date) }.to raise_error(Dentaku::ArgumentError)
|
67
|
+
expect { add(x, one, 'x' => [1]) }.to raise_error(Dentaku::ArgumentError)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def add(left, right, context = ctx)
|
73
|
+
Dentaku::AST::Addition.new(left, right).value(context)
|
74
|
+
end
|
75
|
+
|
76
|
+
def sub(left, right, context = ctx)
|
77
|
+
Dentaku::AST::Subtraction.new(left, right).value(context)
|
78
|
+
end
|
79
|
+
|
80
|
+
def mul(left, right, context = ctx)
|
81
|
+
Dentaku::AST::Multiplication.new(left, right).value(context)
|
82
|
+
end
|
83
|
+
|
84
|
+
def div(left, right, context = ctx)
|
85
|
+
Dentaku::AST::Division.new(left, right).value(context)
|
86
|
+
end
|
87
|
+
|
88
|
+
def neg(node, context = ctx)
|
89
|
+
Dentaku::AST::Negation.new(node).value(context)
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/avg'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe 'Dentaku::AST::Function::Avg' do
|
6
|
+
it 'returns the average of an array of Numeric values' do
|
7
|
+
result = Dentaku('AVG(1, x, 1.8)', x: 2.3)
|
8
|
+
expect(result).to eq(1.7)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns the average of a single entry array of a Numeric value' do
|
12
|
+
result = Dentaku('AVG(x)', x: 2.3)
|
13
|
+
expect(result).to eq(2.3)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the average even if a String is passed' do
|
17
|
+
result = Dentaku('AVG(1, x, 1.8)', x: '2.3')
|
18
|
+
expect(result).to eq(1.7)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns the average even if an array is passed' do
|
22
|
+
result = Dentaku('AVG(1, x, 2.3)', x: [4, 5])
|
23
|
+
expect(result).to eq(3.075)
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'checking errors' do
|
27
|
+
let(:calculator) { Dentaku::Calculator.new }
|
28
|
+
|
29
|
+
it 'raises an error if no arguments are passed' do
|
30
|
+
expect { calculator.evaluate!('AVG()') }.to raise_error(Dentaku::ArgumentError)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises an error if an empty array is passed' do
|
34
|
+
expect { calculator.evaluate!('AVG(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/operation'
|
3
|
+
require 'dentaku/ast/logical'
|
4
|
+
require 'dentaku/ast/identifier'
|
5
|
+
require 'dentaku/ast/arithmetic'
|
6
|
+
require 'dentaku/ast/case'
|
7
|
+
|
8
|
+
require 'dentaku/token'
|
9
|
+
|
10
|
+
describe Dentaku::AST::Case do
|
11
|
+
let!(:one) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 1) }
|
12
|
+
let!(:two) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 2) }
|
13
|
+
let!(:apple) do
|
14
|
+
Dentaku::AST::Logical.new Dentaku::Token.new(:string, 'apple')
|
15
|
+
end
|
16
|
+
let!(:banana) do
|
17
|
+
Dentaku::AST::Logical.new Dentaku::Token.new(:string, 'banana')
|
18
|
+
end
|
19
|
+
let!(:identifier) do
|
20
|
+
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :fruit))
|
21
|
+
end
|
22
|
+
let!(:switch) { Dentaku::AST::CaseSwitchVariable.new(identifier) }
|
23
|
+
|
24
|
+
let!(:when1) { Dentaku::AST::CaseWhen.new(apple) }
|
25
|
+
let!(:then1) { Dentaku::AST::CaseThen.new(one) }
|
26
|
+
let!(:conditional1) { Dentaku::AST::CaseConditional.new(when1, then1) }
|
27
|
+
|
28
|
+
let!(:when2) { Dentaku::AST::CaseWhen.new(banana) }
|
29
|
+
let!(:then2) { Dentaku::AST::CaseThen.new(two) }
|
30
|
+
let!(:conditional2) { Dentaku::AST::CaseConditional.new(when2, then2) }
|
31
|
+
|
32
|
+
describe '#value' do
|
33
|
+
it 'raises an exception if there is no switch variable' do
|
34
|
+
expect { described_class.new(conditional1, conditional2) }
|
35
|
+
.to raise_error('Case missing switch variable')
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'raises an exception if a non-conditional is passed' do
|
39
|
+
expect { described_class.new(switch, conditional1, when2) }
|
40
|
+
.to raise_error(/is not a CaseConditional/)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'tests each conditional against the switch variable' do
|
44
|
+
node = described_class.new(switch, conditional1, conditional2)
|
45
|
+
expect(node.value(fruit: 'banana')).to eq(2)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'raises an exception if the conditional is not matched' do
|
49
|
+
node = described_class.new(switch, conditional1, conditional2)
|
50
|
+
expect { node.value(fruit: 'orange') }
|
51
|
+
.to raise_error("No block matched the switch value 'orange'")
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'uses the else value if provided and conditional is not matched' do
|
55
|
+
three = Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 3)
|
56
|
+
else_statement = Dentaku::AST::CaseElse.new(three)
|
57
|
+
node = described_class.new(
|
58
|
+
switch,
|
59
|
+
conditional1,
|
60
|
+
conditional2,
|
61
|
+
else_statement)
|
62
|
+
expect(node.value(fruit: 'orange')).to eq(3)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#dependencies' do
|
67
|
+
let!(:tax) do
|
68
|
+
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :tax))
|
69
|
+
end
|
70
|
+
let!(:fallback) do
|
71
|
+
Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, :fallback))
|
72
|
+
end
|
73
|
+
let!(:addition) { Dentaku::AST::Addition.new(two, tax) }
|
74
|
+
let!(:when2) { Dentaku::AST::CaseWhen.new(banana) }
|
75
|
+
let!(:then2) { Dentaku::AST::CaseThen.new(addition) }
|
76
|
+
let!(:else2) { Dentaku::AST::CaseElse.new(fallback) }
|
77
|
+
let!(:conditional2) { Dentaku::AST::CaseConditional.new(when2, then2) }
|
78
|
+
|
79
|
+
it 'gathers dependencies from switch and conditionals' do
|
80
|
+
node = described_class.new(switch, conditional1, conditional2, else2)
|
81
|
+
expect(node.dependencies).to eq([:fruit, :tax, :fallback])
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/comparators'
|
3
|
+
|
4
|
+
require 'dentaku/token'
|
5
|
+
|
6
|
+
describe Dentaku::AST::Comparator do
|
7
|
+
let(:one) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 1) }
|
8
|
+
let(:one_str) { Dentaku::AST::String.new Dentaku::Token.new(:string, '1') }
|
9
|
+
let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
|
10
|
+
let(:two_str) { Dentaku::AST::String.new Dentaku::Token.new(:string, '2') }
|
11
|
+
let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
|
12
|
+
let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
|
13
|
+
let(:nilly) do
|
14
|
+
Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'nilly')
|
15
|
+
end
|
16
|
+
let(:ctx) { { 'x' => 'hello', 'y' => 'world', 'nilly' => nil } }
|
17
|
+
|
18
|
+
it 'performs comparison with numeric operands' do
|
19
|
+
expect(less_than(one, two).value(ctx)).to be_truthy
|
20
|
+
expect(less_than(two, one).value(ctx)).to be_falsey
|
21
|
+
expect(greater_than(two, one).value(ctx)).to be_truthy
|
22
|
+
expect(not_equal(x, y).value(ctx)).to be_truthy
|
23
|
+
expect(equal(x, y).value(ctx)).to be_falsey
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'performs conversion from string to numeric operands' do
|
27
|
+
expect(less_than(one, two_str).value(ctx)).to be_truthy
|
28
|
+
expect(less_than(one_str, two_str).value(ctx)).to be_truthy
|
29
|
+
expect(less_than(one_str, two).value(ctx)).to be_truthy
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'raises a dentaku argument error when incorrect arguments are passed in' do
|
33
|
+
expect { less_than(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
|
34
|
+
expect { less_than_or_equal(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
|
35
|
+
expect { greater_than(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
|
36
|
+
expect { greater_than_or_equal(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
|
37
|
+
expect { greater_than_or_equal(one, x).value(ctx) }.to raise_error Dentaku::ArgumentError
|
38
|
+
expect { not_equal(one, nilly).value(ctx) }.to_not raise_error
|
39
|
+
expect { equal(one, nilly).value(ctx) }.to_not raise_error
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'raises a dentaku argument error when nil is passed in as first argument' do
|
43
|
+
expect { less_than(nilly, one).value(ctx) }.to raise_error Dentaku::ArgumentError
|
44
|
+
expect { less_than_or_equal(nilly, one).value(ctx) }.to raise_error Dentaku::ArgumentError
|
45
|
+
expect { greater_than(nilly, one).value(ctx) }.to raise_error Dentaku::ArgumentError
|
46
|
+
expect { greater_than_or_equal(nilly, one).value(ctx) }.to raise_error Dentaku::ArgumentError
|
47
|
+
expect { not_equal(nilly, one).value(ctx) }.to_not raise_error
|
48
|
+
expect { equal(nilly, one).value(ctx) }.to_not raise_error
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'returns correct operator symbols' do
|
52
|
+
expect(less_than(one, two).operator).to eq(:<)
|
53
|
+
expect(less_than_or_equal(one, two).operator).to eq(:<=)
|
54
|
+
expect(greater_than(one, two).operator).to eq(:>)
|
55
|
+
expect(greater_than_or_equal(one, two).operator).to eq(:>=)
|
56
|
+
expect(not_equal(x, y).operator).to eq(:!=)
|
57
|
+
expect(equal(x, y).operator).to eq(:==)
|
58
|
+
expect { Dentaku::AST::Comparator.new(one, two).operator }
|
59
|
+
.to raise_error(NotImplementedError)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def less_than(left, right)
|
65
|
+
Dentaku::AST::LessThan.new(left, right)
|
66
|
+
end
|
67
|
+
|
68
|
+
def less_than_or_equal(left, right)
|
69
|
+
Dentaku::AST::LessThanOrEqual.new(left, right)
|
70
|
+
end
|
71
|
+
|
72
|
+
def greater_than(left, right)
|
73
|
+
Dentaku::AST::GreaterThan.new(left, right)
|
74
|
+
end
|
75
|
+
|
76
|
+
def greater_than_or_equal(left, right)
|
77
|
+
Dentaku::AST::GreaterThanOrEqual.new(left, right)
|
78
|
+
end
|
79
|
+
|
80
|
+
def not_equal(left, right)
|
81
|
+
Dentaku::AST::NotEqual.new(left, right)
|
82
|
+
end
|
83
|
+
|
84
|
+
def equal(left, right)
|
85
|
+
Dentaku::AST::Equal.new(left, right)
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/count'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe 'Dentaku::AST::Count' do
|
6
|
+
it 'returns the length of an array' do
|
7
|
+
result = Dentaku('COUNT(1, x, 1.8)', x: 2.3)
|
8
|
+
expect(result).to eq(3)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns the length of a single number object' do
|
12
|
+
result = Dentaku('COUNT(x)', x: 2.3)
|
13
|
+
expect(result).to eq(1)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the length if a single String is passed' do
|
17
|
+
result = Dentaku('COUNT(x)', x: 'dentaku')
|
18
|
+
expect(result).to eq(7)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns the length if an array is passed' do
|
22
|
+
result = Dentaku('COUNT(x)', x: [4, 5])
|
23
|
+
expect(result).to eq(2)
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'returns the length if an array with one element is passed' do
|
27
|
+
result = Dentaku('COUNT(x)', x: [4])
|
28
|
+
expect(result).to eq(1)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'returns the length if an array even if it has nested array' do
|
32
|
+
result = Dentaku('COUNT(1, x, 3)', x: [4, 5])
|
33
|
+
expect(result).to eq(3)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'returns the length if an array is passed' do
|
37
|
+
result = Dentaku('COUNT()')
|
38
|
+
expect(result).to eq(0)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/arithmetic'
|
3
|
+
|
4
|
+
require 'dentaku/token'
|
5
|
+
|
6
|
+
describe Dentaku::AST::Division do
|
7
|
+
let(:five) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 5) }
|
8
|
+
let(:six) { Dentaku::AST::Logical.new Dentaku::Token.new(:numeric, 6) }
|
9
|
+
|
10
|
+
let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
|
11
|
+
|
12
|
+
it 'allows access to its sub-trees' do
|
13
|
+
node = described_class.new(five, six)
|
14
|
+
expect(node.left).to eq(five)
|
15
|
+
expect(node.right).to eq(six)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'performs division' do
|
19
|
+
node = described_class.new(five, six)
|
20
|
+
expect(node.value.round(4)).to eq(0.8333)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'requires numeric operands' do
|
24
|
+
expect {
|
25
|
+
described_class.new(five, t)
|
26
|
+
}.to raise_error(Dentaku::NodeError, /requires numeric operands/)
|
27
|
+
|
28
|
+
expression = Dentaku::AST::Multiplication.new(five, five)
|
29
|
+
group = Dentaku::AST::Grouping.new(expression)
|
30
|
+
|
31
|
+
expect {
|
32
|
+
described_class.new(group, five)
|
33
|
+
}.not_to raise_error
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/filter'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe Dentaku::AST::Filter do
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
it 'excludes unmatched values' do
|
8
|
+
result = Dentaku('SUM(FILTER(vals, val, val > 1))', vals: [1, 2, 3])
|
9
|
+
expect(result).to eq(5)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'works with a single value if needed for some reason' do
|
13
|
+
result = Dentaku('FILTER(vals, val, val > 1)', vals: 1)
|
14
|
+
expect(result).to eq([])
|
15
|
+
|
16
|
+
result = Dentaku('FILTER(vals, val, val > 1)', vals: 2)
|
17
|
+
expect(result).to eq([2])
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'raises argument error if a string is passed as identifier' do
|
21
|
+
expect { calculator.evaluate!('FILTER({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
|
22
|
+
Dentaku::ArgumentError, 'FILTER() requires second argument to be an identifier'
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'bigdecimal'
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'dentaku/ast/function'
|
4
|
+
require 'dentaku/exceptions'
|
5
|
+
|
6
|
+
class Clazz; end
|
7
|
+
|
8
|
+
describe Dentaku::AST::Function do
|
9
|
+
it 'maintains a function registry' do
|
10
|
+
expect(described_class).to respond_to(:get)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'registers a custom function' do
|
14
|
+
described_class.register("flarble", :string, -> { "flarble" })
|
15
|
+
expect { described_class.get("flarble") }.not_to raise_error
|
16
|
+
function = described_class.get("flarble").new
|
17
|
+
expect(function.value).to eq("flarble")
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'does not throw an error when registering a function with a name that matches a currently defined constant' do
|
21
|
+
expect { described_class.register("clazz", :string, -> { "clazzified" }) }.not_to raise_error
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "#arity" do
|
25
|
+
it "returns the correct arity for custom functions" do
|
26
|
+
zero = described_class.register("zero", :numeric, ->() { 0 })
|
27
|
+
expect(zero.arity).to eq(0)
|
28
|
+
|
29
|
+
one = described_class.register("one", :numeric, ->(x) { x * 2 })
|
30
|
+
expect(one.arity).to eq(1)
|
31
|
+
|
32
|
+
two = described_class.register("two", :numeric, ->(x, y) { x + y })
|
33
|
+
expect(two.arity).to eq(2)
|
34
|
+
|
35
|
+
many = described_class.register("many", :numeric, ->(*args) { args.max })
|
36
|
+
expect(many.arity).to be_nil
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'casts a String to an Integer if possible' do
|
41
|
+
expect(described_class.numeric('3')).to eq(3)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'casts a String to a BigDecimal if possible and if Integer would loose information' do
|
45
|
+
expect(described_class.numeric('3.2')).to eq(3.2)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'casts a String to a BigDecimal with a negative number' do
|
49
|
+
expect(described_class.numeric('-3.2')).to eq(-3.2)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'casts a String to a BigDecimal without a leading zero' do
|
53
|
+
expect(described_class.numeric('-.2')).to eq(-0.2)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'raises an error if the value could not be cast to a Numeric' do
|
57
|
+
expect { described_class.numeric('flarble') }.to raise_error Dentaku::ArgumentError
|
58
|
+
expect { described_class.numeric('-') }.to raise_error Dentaku::ArgumentError
|
59
|
+
expect { described_class.numeric('') }.to raise_error Dentaku::ArgumentError
|
60
|
+
expect { described_class.numeric(nil) }.to raise_error Dentaku::ArgumentError
|
61
|
+
expect { described_class.numeric('7.') }.to raise_error Dentaku::ArgumentError
|
62
|
+
expect { described_class.numeric(true) }.to raise_error Dentaku::ArgumentError
|
63
|
+
end
|
64
|
+
|
65
|
+
it "allows read access to arguments" do
|
66
|
+
fn = described_class.new(1, 2, 3)
|
67
|
+
expect(fn.args).to eq([1, 2, 3])
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/map'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe Dentaku::AST::Map do
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
it 'operates on each value in an array' do
|
8
|
+
result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
|
9
|
+
expect(result).to eq(9)
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'works with an empty array' do
|
13
|
+
result = Dentaku('MAP(vals, val, val + 1)', vals: [])
|
14
|
+
expect(result).to eq([])
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'works with a single value if needed for some reason' do
|
18
|
+
result = Dentaku('MAP(vals, val, val + 1)', vals: 1)
|
19
|
+
expect(result).to eq([2])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'raises argument error if a string is passed as identifier' do
|
23
|
+
expect { calculator.evaluate!('MAP({1, 2, 3}, "val", val + 1)') }.to raise_error(
|
24
|
+
Dentaku::ArgumentError, 'MAP() requires second argument to be an identifier'
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/max'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe 'Dentaku::AST::Function::Max' do
|
6
|
+
it 'returns the largest numeric value in an array of Numeric values' do
|
7
|
+
result = Dentaku('MAX(1, x, 1.8)', x: 2.3)
|
8
|
+
expect(result).to eq(2.3)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns the largest value even if a String is passed' do
|
12
|
+
result = Dentaku('MAX(1, x, 1.8)', x: '2.3')
|
13
|
+
expect(result).to eq(2.3)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the largest value even if an Array is passed' do
|
17
|
+
result = Dentaku('MAX(1, x, 1.8)', x: [1.5, 2.3, 1.7])
|
18
|
+
expect(result).to eq(2.3)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns the largest value if only an Array is passed' do
|
22
|
+
result = Dentaku('MAX(x)', x: [1.5, 2.3, 1.7])
|
23
|
+
expect(result).to eq(2.3)
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'checking errors' do
|
27
|
+
let(:calculator) { Dentaku::Calculator.new }
|
28
|
+
|
29
|
+
it 'does not raise an error if an empty array is passed' do
|
30
|
+
expect(calculator.evaluate!('MAX(x)', x: [])).to eq(nil)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/min'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe 'Dentaku::AST::Function::Min' do
|
6
|
+
it 'returns the smallest numeric value in an array of Numeric values' do
|
7
|
+
result = Dentaku('MIN(1, x, 1.8)', x: 2.3)
|
8
|
+
expect(result).to eq(1)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns the smallest value even if a String is passed' do
|
12
|
+
result = Dentaku('MIN(1, x, 1.8)', x: '0.3')
|
13
|
+
expect(result).to eq(0.3)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the smallest value even if an Array is passed' do
|
17
|
+
result = Dentaku('MIN(1, x, 1.8)', x: [1.5, 0.3, 1.7])
|
18
|
+
expect(result).to eq(0.3)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'returns the smallest value if only an Array is passed' do
|
22
|
+
result = Dentaku('MIN(x)', x: [1.5, 2.3, 1.7])
|
23
|
+
expect(result).to eq(1.5)
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'checking errors' do
|
27
|
+
let(:calculator) { Dentaku::Calculator.new }
|
28
|
+
|
29
|
+
it 'does not raise an error if an empty array is passed' do
|
30
|
+
expect(calculator.evaluate!('MIN(x)', x: [])).to eq(nil)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|