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,26 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/abs'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Function::Abs' do
|
|
6
|
+
it 'returns the absolute value of number' do
|
|
7
|
+
result = Dentaku('ABS(-4.2)')
|
|
8
|
+
expect(result).to eq(4.2)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns the correct value for positive number' do
|
|
12
|
+
result = Dentaku('ABS(1.3)')
|
|
13
|
+
expect(result).to eq(1.3)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the correct value for zero' do
|
|
17
|
+
result = Dentaku('ABS(0)')
|
|
18
|
+
expect(result).to eq(0)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
context 'checking errors' do
|
|
22
|
+
it 'raises an error if argument is not numeric' do
|
|
23
|
+
expect { Dentaku!("ABS(2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/arithmetic'
|
|
3
|
+
|
|
4
|
+
require 'dentaku/token'
|
|
5
|
+
|
|
6
|
+
describe Dentaku::AST::Addition do
|
|
7
|
+
let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
|
|
8
|
+
let(:six) { Dentaku::AST::Numeric.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 addition' do
|
|
19
|
+
node = described_class.new(five, six)
|
|
20
|
+
expect(node.value).to eq(11)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'requires operands that respond to +' do
|
|
24
|
+
expect {
|
|
25
|
+
described_class.new(five, t)
|
|
26
|
+
}.to raise_error(Dentaku::NodeError, /requires 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
|
+
|
|
36
|
+
it 'allows operands that respond to addition' do
|
|
37
|
+
# Sample struct that has a custom definition for addition
|
|
38
|
+
Addable = Struct.new(:value) do
|
|
39
|
+
def +(other)
|
|
40
|
+
case other
|
|
41
|
+
when Addable
|
|
42
|
+
value + other.value
|
|
43
|
+
when Numeric
|
|
44
|
+
value + other
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(5))
|
|
50
|
+
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(6))
|
|
51
|
+
|
|
52
|
+
expect {
|
|
53
|
+
described_class.new(operand_five, operand_six).value
|
|
54
|
+
}.not_to raise_error
|
|
55
|
+
|
|
56
|
+
expect {
|
|
57
|
+
described_class.new(operand_five, six).value
|
|
58
|
+
}.not_to raise_error
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'does not try to parse nested string as date' do
|
|
62
|
+
a = ['2017-01-01', '2017-01-02']
|
|
63
|
+
b = ['2017-01-01']
|
|
64
|
+
|
|
65
|
+
expect(Dentaku('a + b', a: a, b: b)).to eq(['2017-01-01', '2017-01-02', '2017-01-01'])
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/all'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe Dentaku::AST::All do
|
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
7
|
+
|
|
8
|
+
it 'performs ALL operation' do
|
|
9
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
|
|
10
|
+
expect(result).to eq(false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'works with a single value if needed for some reason' do
|
|
14
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: 1)
|
|
15
|
+
expect(result).to eq(false)
|
|
16
|
+
|
|
17
|
+
result = Dentaku('ALL(vals, val, val > 1)', vals: 2)
|
|
18
|
+
expect(result).to eq(true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'raises argument error if a string is passed as identifier' do
|
|
22
|
+
expect { calculator.evaluate!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
|
|
23
|
+
Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
|
28
|
+
expect(
|
|
29
|
+
calculator.evaluate('ALL(items, item, item.value)', items: [{value: 1}, {}])
|
|
30
|
+
).to be_falsy
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
|
34
|
+
expect {
|
|
35
|
+
calculator.evaluate!('ALL(items, item, item.value)', items: [{value: 1}, {}])
|
|
36
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -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,36 @@
|
|
|
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
|
+
|
|
8
|
+
it 'performs ANY operation' do
|
|
9
|
+
result = Dentaku('ANY(vals, val, val > 1)', vals: [1, 2, 3])
|
|
10
|
+
expect(result).to eq(true)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'works with a single value if needed for some reason' do
|
|
14
|
+
result = Dentaku('ANY(vals, val, val > 1)', vals: 1)
|
|
15
|
+
expect(result).to eq(false)
|
|
16
|
+
|
|
17
|
+
result = Dentaku('ANY(vals, val, val > 1)', vals: 2)
|
|
18
|
+
expect(result).to eq(true)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'raises argument error if a string is passed as identifier' do
|
|
22
|
+
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
|
26
|
+
expect(
|
|
27
|
+
calculator.evaluate('ANY(items, item, item.value)', items: [{value: 1}, {}])
|
|
28
|
+
).to be_truthy
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
|
32
|
+
expect {
|
|
33
|
+
calculator.evaluate!('ANY(items, item, item.value)', items: [{}, {value: 1}])
|
|
34
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/arithmetic'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe Dentaku::AST::Arithmetic do
|
|
6
|
+
let(:one) { Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, 1)) }
|
|
7
|
+
let(:two) { Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, 2)) }
|
|
8
|
+
let(:x) { Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, 'x')) }
|
|
9
|
+
let(:y) { Dentaku::AST::Identifier.new(Dentaku::Token.new(:identifier, 'y')) }
|
|
10
|
+
let(:ctx) { {'x' => 1, 'y' => 2} }
|
|
11
|
+
let(:date) { Dentaku::AST::DateTime.new(Dentaku::Token.new(:datetime, DateTime.new(2020, 4, 16))) }
|
|
12
|
+
|
|
13
|
+
it 'performs an arithmetic operation with numeric operands' do
|
|
14
|
+
expect(add(one, two)).to eq(3)
|
|
15
|
+
expect(sub(one, two)).to eq(-1)
|
|
16
|
+
expect(mul(one, two)).to eq(2)
|
|
17
|
+
expect(div(one, two)).to eq(0.5)
|
|
18
|
+
expect(neg(one)).to eq(-1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'performs an arithmetic operation with one numeric operand and one string operand' do
|
|
22
|
+
expect(add(one, x)).to eq(2)
|
|
23
|
+
expect(sub(one, x)).to eq(0)
|
|
24
|
+
expect(mul(one, x)).to eq(1)
|
|
25
|
+
expect(div(one, x)).to eq(1)
|
|
26
|
+
|
|
27
|
+
expect(add(y, two)).to eq(4)
|
|
28
|
+
expect(sub(y, two)).to eq(0)
|
|
29
|
+
expect(mul(y, two)).to eq(4)
|
|
30
|
+
expect(div(y, two)).to eq(1)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'performs an arithmetic operation with string operands' do
|
|
34
|
+
expect(add(x, y)).to eq(3)
|
|
35
|
+
expect(sub(x, y)).to eq(-1)
|
|
36
|
+
expect(mul(x, y)).to eq(2)
|
|
37
|
+
expect(div(x, y)).to eq(0.5)
|
|
38
|
+
expect(neg(x)).to eq(-1)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'correctly parses string operands to numeric values' do
|
|
42
|
+
expect(add(x, one, 'x' => '1')).to eq(2)
|
|
43
|
+
expect(add(x, one, 'x' => '1.1')).to eq(2.1)
|
|
44
|
+
expect(add(x, one, 'x' => '.1')).to eq(1.1)
|
|
45
|
+
expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
|
|
46
|
+
expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
|
|
47
|
+
|
|
48
|
+
int_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "1"))
|
|
49
|
+
int_neg_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "-1"))
|
|
50
|
+
decimal_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "1.0"))
|
|
51
|
+
decimal_neg_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "-1.0"))
|
|
52
|
+
|
|
53
|
+
[int_one, int_neg_one].permutation(2).each do |(left, right)|
|
|
54
|
+
expect(add(left, right).class).to eq(Integer)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
[decimal_one, decimal_neg_one].each do |left|
|
|
58
|
+
[int_one, int_neg_one, decimal_one, decimal_neg_one].each do |right|
|
|
59
|
+
expect(add(left, right).class).to eq(BigDecimal)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'performs arithmetic on arrays' do
|
|
65
|
+
expect(add(x, y, 'x' => [1], 'y' => [2])).to eq([1, 2])
|
|
66
|
+
expect(sub(x, y, 'x' => [1], 'y' => [2])).to eq([1])
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
it 'performs date arithmetic' do
|
|
70
|
+
expect(add(date, one)).to eq(DateTime.new(2020, 4, 17))
|
|
71
|
+
expect(sub(date, one)).to eq(DateTime.new(2020, 4, 15))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
it 'performs arithmetic on object which implements arithmetic' do
|
|
75
|
+
CanHazMath = Struct.new(:value) do
|
|
76
|
+
extend Forwardable
|
|
77
|
+
|
|
78
|
+
def_delegators :value, :zero?
|
|
79
|
+
|
|
80
|
+
def coerce(other)
|
|
81
|
+
case other
|
|
82
|
+
when Numeric
|
|
83
|
+
[other, value]
|
|
84
|
+
else
|
|
85
|
+
super
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
[:+, :-, :/, :*].each do |operand|
|
|
90
|
+
define_method(operand) do |other|
|
|
91
|
+
case other
|
|
92
|
+
when CanHazMath
|
|
93
|
+
value.public_send(operand, other.value)
|
|
94
|
+
when Numeric
|
|
95
|
+
value.public_send(operand, other)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
op_one = CanHazMath.new(1)
|
|
102
|
+
op_two = CanHazMath.new(2)
|
|
103
|
+
|
|
104
|
+
[op_two, two].each do |left|
|
|
105
|
+
[op_one, one].each do |right|
|
|
106
|
+
expect(add(x, y, 'x' => left, 'y' => right)).to eq(3)
|
|
107
|
+
expect(sub(x, y, 'x' => left, 'y' => right)).to eq(1)
|
|
108
|
+
expect(mul(x, y, 'x' => left, 'y' => right)).to eq(2)
|
|
109
|
+
expect(div(x, y, 'x' => left, 'y' => right)).to eq(2)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'does not try to parse nested string as date' do
|
|
115
|
+
a = ['2017-01-01', '2017-01-02']
|
|
116
|
+
b = ['2017-01-01']
|
|
117
|
+
|
|
118
|
+
expect(Dentaku('a - b', a: a, b: b)).to eq(['2017-01-02'])
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'raises ArgumentError if given individually valid but incompatible arguments' do
|
|
122
|
+
expect { add(one, date) }.to raise_error(Dentaku::ArgumentError)
|
|
123
|
+
expect { add(x, one, 'x' => [1]) }.to raise_error(Dentaku::ArgumentError)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private
|
|
127
|
+
|
|
128
|
+
def add(left, right, context = ctx)
|
|
129
|
+
Dentaku::AST::Addition.new(left, right).value(context)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def sub(left, right, context = ctx)
|
|
133
|
+
Dentaku::AST::Subtraction.new(left, right).value(context)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def mul(left, right, context = ctx)
|
|
137
|
+
Dentaku::AST::Multiplication.new(left, right).value(context)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def div(left, right, context = ctx)
|
|
141
|
+
Dentaku::AST::Division.new(left, right).value(context)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def neg(node, context = ctx)
|
|
145
|
+
Dentaku::AST::Negation.new(node).value(context)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
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 as BigDecimal' do
|
|
7
|
+
result = Dentaku('AVG(1, 2)')
|
|
8
|
+
expect(result).to eq(1.5)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns the average of an array of Numeric values' do
|
|
12
|
+
result = Dentaku('AVG(1, x, 1.8)', x: 2.3)
|
|
13
|
+
expect(result).to eq(1.7)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the average of a single entry array of a Numeric value' do
|
|
17
|
+
result = Dentaku('AVG(x)', x: 2.3)
|
|
18
|
+
expect(result).to eq(2.3)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns the average even if a String is passed' do
|
|
22
|
+
result = Dentaku('AVG(1, x, 1.8)', x: '2.3')
|
|
23
|
+
expect(result).to eq(1.7)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'returns the average even if an array is passed' do
|
|
27
|
+
result = Dentaku('AVG(1, x, 2.3)', x: [4, 5])
|
|
28
|
+
expect(result).to eq(3.075)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'checking errors' do
|
|
32
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
33
|
+
|
|
34
|
+
it 'raises an error if no arguments are passed' do
|
|
35
|
+
expect { calculator.evaluate!('AVG()') }.to raise_error(Dentaku::ArgumentError)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'raises an error if an empty array is passed' do
|
|
39
|
+
expect { calculator.evaluate!('AVG(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
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
|