hayadentaku 3.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rspec.yml +26 -0
  3. data/.github/workflows/rubocop.yml +14 -0
  4. data/.gitignore +14 -0
  5. data/.pryrc +2 -0
  6. data/.rubocop.yml +114 -0
  7. data/.travis.yml +10 -0
  8. data/CHANGELOG.md +328 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE +21 -0
  11. data/README.md +352 -0
  12. data/Rakefile +31 -0
  13. data/hayadentaku.gemspec +35 -0
  14. data/lib/dentaku/ast/access.rb +44 -0
  15. data/lib/dentaku/ast/arithmetic.rb +292 -0
  16. data/lib/dentaku/ast/array.rb +38 -0
  17. data/lib/dentaku/ast/bitwise.rb +42 -0
  18. data/lib/dentaku/ast/case/case_conditional.rb +38 -0
  19. data/lib/dentaku/ast/case/case_else.rb +35 -0
  20. data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
  21. data/lib/dentaku/ast/case/case_then.rb +35 -0
  22. data/lib/dentaku/ast/case/case_when.rb +39 -0
  23. data/lib/dentaku/ast/case.rb +93 -0
  24. data/lib/dentaku/ast/combinators.rb +50 -0
  25. data/lib/dentaku/ast/comparators.rb +88 -0
  26. data/lib/dentaku/ast/datetime.rb +8 -0
  27. data/lib/dentaku/ast/function.rb +56 -0
  28. data/lib/dentaku/ast/function_registry.rb +107 -0
  29. data/lib/dentaku/ast/functions/abs.rb +5 -0
  30. data/lib/dentaku/ast/functions/all.rb +19 -0
  31. data/lib/dentaku/ast/functions/and.rb +25 -0
  32. data/lib/dentaku/ast/functions/any.rb +19 -0
  33. data/lib/dentaku/ast/functions/avg.rb +13 -0
  34. data/lib/dentaku/ast/functions/count.rb +26 -0
  35. data/lib/dentaku/ast/functions/duration.rb +51 -0
  36. data/lib/dentaku/ast/functions/enum.rb +54 -0
  37. data/lib/dentaku/ast/functions/filter.rb +21 -0
  38. data/lib/dentaku/ast/functions/if.rb +47 -0
  39. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  40. data/lib/dentaku/ast/functions/map.rb +19 -0
  41. data/lib/dentaku/ast/functions/max.rb +5 -0
  42. data/lib/dentaku/ast/functions/min.rb +5 -0
  43. data/lib/dentaku/ast/functions/mul.rb +12 -0
  44. data/lib/dentaku/ast/functions/not.rb +5 -0
  45. data/lib/dentaku/ast/functions/or.rb +25 -0
  46. data/lib/dentaku/ast/functions/pluck.rb +34 -0
  47. data/lib/dentaku/ast/functions/reduce.rb +60 -0
  48. data/lib/dentaku/ast/functions/round.rb +5 -0
  49. data/lib/dentaku/ast/functions/rounddown.rb +8 -0
  50. data/lib/dentaku/ast/functions/roundup.rb +8 -0
  51. data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
  52. data/lib/dentaku/ast/functions/string_functions.rb +212 -0
  53. data/lib/dentaku/ast/functions/sum.rb +12 -0
  54. data/lib/dentaku/ast/functions/switch.rb +8 -0
  55. data/lib/dentaku/ast/functions/xor.rb +44 -0
  56. data/lib/dentaku/ast/grouping.rb +23 -0
  57. data/lib/dentaku/ast/identifier.rb +52 -0
  58. data/lib/dentaku/ast/literal.rb +30 -0
  59. data/lib/dentaku/ast/logical.rb +8 -0
  60. data/lib/dentaku/ast/negation.rb +54 -0
  61. data/lib/dentaku/ast/nil.rb +13 -0
  62. data/lib/dentaku/ast/node.rb +29 -0
  63. data/lib/dentaku/ast/numeric.rb +8 -0
  64. data/lib/dentaku/ast/operation.rb +44 -0
  65. data/lib/dentaku/ast/string.rb +15 -0
  66. data/lib/dentaku/ast.rb +42 -0
  67. data/lib/dentaku/bulk_expression_solver.rb +158 -0
  68. data/lib/dentaku/calculator.rb +192 -0
  69. data/lib/dentaku/date_arithmetic.rb +60 -0
  70. data/lib/dentaku/dependency_resolver.rb +29 -0
  71. data/lib/dentaku/exceptions.rb +116 -0
  72. data/lib/dentaku/flat_hash.rb +161 -0
  73. data/lib/dentaku/parser.rb +318 -0
  74. data/lib/dentaku/print_visitor.rb +112 -0
  75. data/lib/dentaku/string_casing.rb +7 -0
  76. data/lib/dentaku/token.rb +48 -0
  77. data/lib/dentaku/token_matcher.rb +138 -0
  78. data/lib/dentaku/token_matchers.rb +29 -0
  79. data/lib/dentaku/token_scanner.rb +240 -0
  80. data/lib/dentaku/tokenizer.rb +127 -0
  81. data/lib/dentaku/version.rb +3 -0
  82. data/lib/dentaku/visitor/infix.rb +86 -0
  83. data/lib/dentaku.rb +69 -0
  84. data/spec/ast/abs_spec.rb +26 -0
  85. data/spec/ast/addition_spec.rb +67 -0
  86. data/spec/ast/all_spec.rb +38 -0
  87. data/spec/ast/and_function_spec.rb +35 -0
  88. data/spec/ast/and_spec.rb +32 -0
  89. data/spec/ast/any_spec.rb +36 -0
  90. data/spec/ast/arithmetic_spec.rb +147 -0
  91. data/spec/ast/avg_spec.rb +42 -0
  92. data/spec/ast/case_spec.rb +84 -0
  93. data/spec/ast/comparator_spec.rb +87 -0
  94. data/spec/ast/count_spec.rb +40 -0
  95. data/spec/ast/division_spec.rb +64 -0
  96. data/spec/ast/filter_spec.rb +25 -0
  97. data/spec/ast/function_spec.rb +69 -0
  98. data/spec/ast/intercept_spec.rb +30 -0
  99. data/spec/ast/map_spec.rb +40 -0
  100. data/spec/ast/max_spec.rb +33 -0
  101. data/spec/ast/min_spec.rb +33 -0
  102. data/spec/ast/mul_spec.rb +43 -0
  103. data/spec/ast/negation_spec.rb +48 -0
  104. data/spec/ast/node_spec.rb +43 -0
  105. data/spec/ast/numeric_spec.rb +16 -0
  106. data/spec/ast/or_spec.rb +35 -0
  107. data/spec/ast/pluck_spec.rb +49 -0
  108. data/spec/ast/reduce_spec.rb +22 -0
  109. data/spec/ast/round_spec.rb +35 -0
  110. data/spec/ast/rounddown_spec.rb +35 -0
  111. data/spec/ast/roundup_spec.rb +35 -0
  112. data/spec/ast/string_functions_spec.rb +217 -0
  113. data/spec/ast/sum_spec.rb +43 -0
  114. data/spec/ast/switch_spec.rb +30 -0
  115. data/spec/ast/xor_spec.rb +35 -0
  116. data/spec/benchmark.rb +70 -0
  117. data/spec/bulk_expression_solver_spec.rb +241 -0
  118. data/spec/calculator_spec.rb +1003 -0
  119. data/spec/dentaku_spec.rb +52 -0
  120. data/spec/dependency_resolver_spec.rb +18 -0
  121. data/spec/exceptions_spec.rb +9 -0
  122. data/spec/external_function_spec.rb +177 -0
  123. data/spec/parser_spec.rb +183 -0
  124. data/spec/print_visitor_spec.rb +77 -0
  125. data/spec/spec_helper.rb +69 -0
  126. data/spec/token_matcher_spec.rb +134 -0
  127. data/spec/token_scanner_spec.rb +49 -0
  128. data/spec/token_spec.rb +16 -0
  129. data/spec/tokenizer_spec.rb +375 -0
  130. data/spec/visitor/infix_spec.rb +52 -0
  131. data/spec/visitor_spec.rb +139 -0
  132. metadata +353 -0
@@ -0,0 +1,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
@@ -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