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,64 @@
|
|
|
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 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 division' do
|
|
37
|
+
# Sample struct that has a custom definition for division
|
|
38
|
+
Divisible = Struct.new(:value) do
|
|
39
|
+
def /(other)
|
|
40
|
+
case other
|
|
41
|
+
when Divisible
|
|
42
|
+
value + other.value
|
|
43
|
+
when Numeric
|
|
44
|
+
value + other
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def zero?
|
|
49
|
+
value.zero?
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(5))
|
|
54
|
+
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(6))
|
|
55
|
+
|
|
56
|
+
expect {
|
|
57
|
+
described_class.new(operand_five, operand_six).value
|
|
58
|
+
}.not_to raise_error
|
|
59
|
+
|
|
60
|
+
expect {
|
|
61
|
+
described_class.new(operand_five, six).value
|
|
62
|
+
}.not_to raise_error
|
|
63
|
+
end
|
|
64
|
+
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::ParseError, '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,30 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/intercept'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Function::Intercept' do
|
|
6
|
+
it 'returns the correct intercept for given x and y arrays' do
|
|
7
|
+
x_values = [6, 13, 15, 10, 11, 10]
|
|
8
|
+
y_values = [-1, 8, 8, 13, 3, 15]
|
|
9
|
+
result = Dentaku('INTERCEPT(ys, xs)', xs: x_values, ys: y_values)
|
|
10
|
+
expect(result).to be_within(0.001).of(9.437)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
context 'checking errors' do
|
|
14
|
+
it 'raises an error if arguments are not arrays' do
|
|
15
|
+
expect { Dentaku!("INTERCEPT(1, 2)") }.to raise_error(Dentaku::ArgumentError)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'raises an error if the arrays are not of equal length' do
|
|
19
|
+
x_values = [1, 2, 3]
|
|
20
|
+
y_values = [2, 3, 5, 4]
|
|
21
|
+
expect { Dentaku!("INTERCEPT(y, x)", x: x_values, y: y_values) }.to raise_error(Dentaku::ArgumentError)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'raises an error if any of the arrays is empty' do
|
|
25
|
+
x_values = []
|
|
26
|
+
y_values = [2, 3, 5, 4]
|
|
27
|
+
expect { Dentaku!("INTERCEPT(y, x)", x: x_values, y: y_values) }.to raise_error(Dentaku::ArgumentError)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
|
|
8
|
+
it 'operates on each value in an array' do
|
|
9
|
+
result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
|
|
10
|
+
expect(result).to eq(9)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'works with an empty array' do
|
|
14
|
+
result = Dentaku('MAP(vals, val, val + 1)', vals: [])
|
|
15
|
+
expect(result).to eq([])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'works with a single value if needed for some reason' do
|
|
19
|
+
result = Dentaku('MAP(vals, val, val + 1)', vals: 1)
|
|
20
|
+
expect(result).to eq([2])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'raises argument error if a string is passed as identifier' do
|
|
24
|
+
expect { calculator.evaluate!('MAP({1, 2, 3}, "val", val + 1)') }.to raise_error(
|
|
25
|
+
Dentaku::ParseError, 'MAP() requires second argument to be an identifier'
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
|
30
|
+
expect(
|
|
31
|
+
calculator.evaluate('MAP(items, item, item.value)', items: [{value: 1}, {}])
|
|
32
|
+
).to eq([1, nil])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
|
36
|
+
expect {
|
|
37
|
+
calculator.evaluate!('MAP(items, item, item.value)', items: [{value: 1}, {}])
|
|
38
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
|
39
|
+
end
|
|
40
|
+
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
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/mul'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Function::Mul' do
|
|
6
|
+
it 'returns the product of an array of Numeric values' do
|
|
7
|
+
result = Dentaku('MUL(1, x, 1.8)', x: 2.3)
|
|
8
|
+
expect(result).to eq(4.14)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns the product of a single entry array of a Numeric value' do
|
|
12
|
+
result = Dentaku('MUL(x)', x: 2.3)
|
|
13
|
+
expect(result).to eq(2.3)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'coerces string inputs to numeric' do
|
|
17
|
+
result = Dentaku('mul(1, x, 1.8)', x: '2.3')
|
|
18
|
+
expect(result).to eq(4.14)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns the product even if an array is passed' do
|
|
22
|
+
result = Dentaku('mul(1, x, 2.3)', x: [4, 5])
|
|
23
|
+
expect(result).to eq(46)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'handles nested calls' do
|
|
27
|
+
result = Dentaku('mul(1, x, mul(4, 5))', x: '2.3')
|
|
28
|
+
expect(result).to eq(46)
|
|
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!('MUL()') }.to raise_error(Dentaku::ArgumentError)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'does not raise an error if an empty array is passed' do
|
|
39
|
+
result = calculator.evaluate!('MUL(x)', x: [])
|
|
40
|
+
expect(result).to eq(1)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/arithmetic'
|
|
3
|
+
|
|
4
|
+
require 'dentaku/token'
|
|
5
|
+
|
|
6
|
+
describe Dentaku::AST::Negation do
|
|
7
|
+
let(:five) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 5) }
|
|
8
|
+
let(:t) { Dentaku::AST::Logical.new Dentaku::Token.new(:logical, true) }
|
|
9
|
+
let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
|
|
10
|
+
|
|
11
|
+
it 'allows access to its sub-node' do
|
|
12
|
+
node = described_class.new(five)
|
|
13
|
+
expect(node.node).to eq(five)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'performs negation' do
|
|
17
|
+
node = described_class.new(five)
|
|
18
|
+
expect(node.value).to eq(-5)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'requires numeric operands' do
|
|
22
|
+
expect {
|
|
23
|
+
described_class.new(t)
|
|
24
|
+
}.to raise_error(Dentaku::NodeError, /requires numeric operands/)
|
|
25
|
+
|
|
26
|
+
expression = Dentaku::AST::Negation.new(five)
|
|
27
|
+
group = Dentaku::AST::Grouping.new(expression)
|
|
28
|
+
|
|
29
|
+
expect {
|
|
30
|
+
described_class.new(group)
|
|
31
|
+
}.not_to raise_error
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it 'correctly parses string operands to numeric values' do
|
|
35
|
+
node = described_class.new(x)
|
|
36
|
+
expect(node.value('x' => '5')).to eq(-5)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
it 'raises error if input string is not coercible to numeric' do
|
|
40
|
+
node = described_class.new(x)
|
|
41
|
+
expect { node.value('x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'raises error if given a non-numeric argument' do
|
|
45
|
+
node = described_class.new(x)
|
|
46
|
+
expect { node.value('x' => true) }.to raise_error(Dentaku::ArgumentError)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/node'
|
|
3
|
+
require 'dentaku/tokenizer'
|
|
4
|
+
require 'dentaku/parser'
|
|
5
|
+
|
|
6
|
+
describe Dentaku::AST::Node do
|
|
7
|
+
it 'returns list of dependencies' do
|
|
8
|
+
node = make_node('x + 5')
|
|
9
|
+
expect(node.dependencies).to eq(['x'])
|
|
10
|
+
|
|
11
|
+
node = make_node('5 < x')
|
|
12
|
+
expect(node.dependencies).to eq(['x'])
|
|
13
|
+
|
|
14
|
+
node = make_node('5 < 7')
|
|
15
|
+
expect(node.dependencies).to eq([])
|
|
16
|
+
|
|
17
|
+
node = make_node('(y * 7)')
|
|
18
|
+
expect(node.dependencies).to eq(['y'])
|
|
19
|
+
|
|
20
|
+
node = make_node('if(x > 5, y, z)')
|
|
21
|
+
expect(node.dependencies).to eq(['x', 'y', 'z'])
|
|
22
|
+
|
|
23
|
+
node = make_node('if(x > 5, y, z)')
|
|
24
|
+
expect(node.dependencies('x' => 7)).to eq(['y'])
|
|
25
|
+
|
|
26
|
+
node = make_node('if(x > 5, y, z)')
|
|
27
|
+
expect(node.dependencies('x' => 2)).to eq(['z'])
|
|
28
|
+
|
|
29
|
+
node = make_node('')
|
|
30
|
+
expect(node.dependencies).to eq([])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'returns unique list of dependencies' do
|
|
34
|
+
node = make_node('x + x')
|
|
35
|
+
expect(node.dependencies).to eq(['x'])
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def make_node(expression)
|
|
41
|
+
Dentaku::Parser.new(Dentaku::Tokenizer.new.tokenize(expression)).parse
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/numeric'
|
|
3
|
+
|
|
4
|
+
require 'dentaku/token'
|
|
5
|
+
|
|
6
|
+
describe Dentaku::AST::Numeric do
|
|
7
|
+
subject { described_class.new(Dentaku::Token.new(:numeric, 5)) }
|
|
8
|
+
|
|
9
|
+
it 'has numeric type' do
|
|
10
|
+
expect(subject.type).to eq(:numeric)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'has no dependencies' do
|
|
14
|
+
expect(subject.dependencies).to be_empty
|
|
15
|
+
end
|
|
16
|
+
end
|
data/spec/ast/or_spec.rb
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku'
|
|
3
|
+
require 'dentaku/ast/functions/or'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Or' do
|
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
7
|
+
|
|
8
|
+
it 'returns false if all of the arguments are false' do
|
|
9
|
+
result = Dentaku('OR(1 = "2", 0 = 1)')
|
|
10
|
+
expect(result).to eq(false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'supports nested expressions' do
|
|
14
|
+
result = Dentaku('OR(y = 1, x = 1)', x: 1, y: 2)
|
|
15
|
+
expect(result).to eq(true)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'returns true if any of the arguments is true' do
|
|
19
|
+
result = Dentaku('OR(1 = "1", "2" = "2", true = false, false)')
|
|
20
|
+
expect(result).to eq(true)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'returns true if any nested OR function returns true' do
|
|
24
|
+
result = Dentaku('OR(OR(1 = 0), OR(true = false, OR(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!('OR()') }.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!('OR("r")') }.to raise_error(Dentaku::ArgumentError)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/pluck'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe Dentaku::AST::Pluck do
|
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
7
|
+
|
|
8
|
+
it 'operates on each value in an array' do
|
|
9
|
+
result = Dentaku('PLUCK(users, age)', users: [
|
|
10
|
+
{name: "Bob", age: 44},
|
|
11
|
+
{name: "Jane", age: 27}
|
|
12
|
+
])
|
|
13
|
+
expect(result).to eq([44, 27])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'allows specifying a default for missing values' do
|
|
17
|
+
result = Dentaku!('PLUCK(users, age, -1)', users: [
|
|
18
|
+
{name: "Bob"},
|
|
19
|
+
{name: "Jane", age: 27}
|
|
20
|
+
])
|
|
21
|
+
expect(result).to eq([-1, 27])
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it 'returns nil if pluck key is missing from a hash' do
|
|
25
|
+
result = Dentaku!('PLUCK(users, age)', users: [
|
|
26
|
+
{name: "Bob"},
|
|
27
|
+
{name: "Jane", age: 27}
|
|
28
|
+
])
|
|
29
|
+
expect(result).to eq([nil, 27])
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'works with an empty array' do
|
|
33
|
+
result = Dentaku('PLUCK(users, age)', users: [])
|
|
34
|
+
expect(result).to eq([])
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'raises argument error if a string is passed as identifier' do
|
|
38
|
+
expect do Dentaku.evaluate!('PLUCK(users, "age")', users: [
|
|
39
|
+
{name: "Bob", age: 44},
|
|
40
|
+
{name: "Jane", age: 27}
|
|
41
|
+
]) end.to raise_error(Dentaku::ParseError, 'PLUCK() requires second argument to be an identifier')
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
it 'raises argument error if a non array of hashes is passed as collection' do
|
|
45
|
+
expect { calculator.evaluate!('PLUCK({1, 2, 3}, age)') }.to raise_error(
|
|
46
|
+
Dentaku::ArgumentError, 'PLUCK() requires first argument to be an array of hashes'
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/reduce'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe Dentaku::AST::Reduce do
|
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
7
|
+
|
|
8
|
+
it 'performs REDUCE operation with initial value' do
|
|
9
|
+
result = Dentaku('REDUCE(vals, memo, val, CONCAT(memo, val), "hello")', vals: ["wo", "rl", "d"])
|
|
10
|
+
expect(result).to eq("helloworld")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'performs REDUCE operation without initial value' do
|
|
14
|
+
result = Dentaku('REDUCE(vals, memo, val, CONCAT(memo, val))', vals: ["wo", "rl", "d"])
|
|
15
|
+
expect(result).to eq("world")
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'raises argument error if a string is passed as identifier' do
|
|
19
|
+
expect { calculator.evaluate!('REDUCE({1, 2, 3}, memo, "val", memo + val)') }.to raise_error(Dentaku::ParseError)
|
|
20
|
+
expect { calculator.evaluate!('REDUCE({1, 2, 3}, "memo", val, memo + val)') }.to raise_error(Dentaku::ParseError)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/round'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Function::Round' do
|
|
6
|
+
it 'returns the rounded down value' do
|
|
7
|
+
result = Dentaku('ROUND(1.8)')
|
|
8
|
+
expect(result).to eq(2)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns the rounded down value to the given precision' do
|
|
12
|
+
result = Dentaku('ROUND(x, y)', x: 1.8453, y: 3)
|
|
13
|
+
expect(result).to eq(1.845)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the rounded down value to the given precision, also with strings' do
|
|
17
|
+
result = Dentaku('ROUND(x, y)', x: '1.8453', y: '3')
|
|
18
|
+
expect(result).to eq(1.845)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns the rounded down value to the given precision, also with nil' do
|
|
22
|
+
result = Dentaku('ROUND(x, y)', x: '1.8453', y: nil)
|
|
23
|
+
expect(result).to eq(2)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'checking errors' do
|
|
27
|
+
it 'raises an error if first argument is not numeric' do
|
|
28
|
+
expect { Dentaku!("ROUND(2020-1-1, 0)") }.to raise_error(Dentaku::ArgumentError)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'raises an error if places is not numeric' do
|
|
32
|
+
expect { Dentaku!("ROUND(1.8, 2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/rounddown'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Function::Round' do
|
|
6
|
+
it 'returns the rounded value' do
|
|
7
|
+
result = Dentaku('ROUNDDOWN(1.8)')
|
|
8
|
+
expect(result).to eq(1)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns the rounded value to the given precision' do
|
|
12
|
+
result = Dentaku('ROUNDDOWN(x, y)', x: 1.8453, y: 3)
|
|
13
|
+
expect(result).to eq(1.845)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the rounded value to the given precision, also with strings' do
|
|
17
|
+
result = Dentaku('ROUNDDOWN(x, y)', x: '1.8453', y: '3')
|
|
18
|
+
expect(result).to eq(1.845)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns the rounded value to the given precision, also with nil' do
|
|
22
|
+
result = Dentaku('ROUNDDOWN(x, y)', x: '1.8453', y: nil)
|
|
23
|
+
expect(result).to eq(1)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'checking errors' do
|
|
27
|
+
it 'raises an error if first argument is not numeric' do
|
|
28
|
+
expect { Dentaku!("ROUND(2020-1-1, 0)") }.to raise_error(Dentaku::ArgumentError)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'raises an error if places is not numeric' do
|
|
32
|
+
expect { Dentaku!("ROUND(1.8, 2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/roundup'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Function::Round' do
|
|
6
|
+
it 'returns the rounded value' do
|
|
7
|
+
result = Dentaku('ROUNDUP(1.8)')
|
|
8
|
+
expect(result).to eq(2)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns the rounded value to the given precision' do
|
|
12
|
+
result = Dentaku('ROUNDUP(x, y)', x: 1.8453, y: 3)
|
|
13
|
+
expect(result).to eq(1.846)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the rounded value to the given precision, also with strings' do
|
|
17
|
+
result = Dentaku('ROUNDUP(x, y)', x: '1.8453', y: '3')
|
|
18
|
+
expect(result).to eq(1.846)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns the rounded value to the given precision, also with nil' do
|
|
22
|
+
result = Dentaku('ROUNDUP(x, y)', x: '1.8453', y: nil)
|
|
23
|
+
expect(result).to eq(2)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
context 'checking errors' do
|
|
27
|
+
it 'raises an error if first argument is not numeric' do
|
|
28
|
+
expect { Dentaku!("ROUND(2020-1-1, 0)") }.to raise_error(Dentaku::ArgumentError)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it 'raises an error if places is not numeric' do
|
|
32
|
+
expect { Dentaku!("ROUND(1.8, 2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|