dentaku 3.4.2 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +3 -4
  4. data/lib/dentaku/ast/access.rb +6 -0
  5. data/lib/dentaku/ast/arithmetic.rb +5 -1
  6. data/lib/dentaku/ast/array.rb +4 -0
  7. data/lib/dentaku/ast/bitwise.rb +8 -0
  8. data/lib/dentaku/ast/case/case_conditional.rb +4 -0
  9. data/lib/dentaku/ast/case/case_else.rb +6 -0
  10. data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
  11. data/lib/dentaku/ast/case/case_then.rb +6 -0
  12. data/lib/dentaku/ast/case/case_when.rb +10 -0
  13. data/lib/dentaku/ast/case.rb +6 -0
  14. data/lib/dentaku/ast/comparators.rb +25 -35
  15. data/lib/dentaku/ast/function.rb +6 -8
  16. data/lib/dentaku/ast/functions/all.rb +4 -17
  17. data/lib/dentaku/ast/functions/any.rb +4 -17
  18. data/lib/dentaku/ast/functions/duration.rb +2 -2
  19. data/lib/dentaku/ast/functions/enum.rb +37 -0
  20. data/lib/dentaku/ast/functions/filter.rb +4 -17
  21. data/lib/dentaku/ast/functions/if.rb +4 -0
  22. data/lib/dentaku/ast/functions/map.rb +3 -16
  23. data/lib/dentaku/ast/functions/pluck.rb +8 -7
  24. data/lib/dentaku/ast/functions/ruby_math.rb +3 -2
  25. data/lib/dentaku/ast/functions/xor.rb +44 -0
  26. data/lib/dentaku/ast/identifier.rb +8 -0
  27. data/lib/dentaku/ast/literal.rb +10 -0
  28. data/lib/dentaku/ast/negation.rb +4 -0
  29. data/lib/dentaku/ast/nil.rb +4 -0
  30. data/lib/dentaku/ast/node.rb +4 -0
  31. data/lib/dentaku/ast/operation.rb +9 -0
  32. data/lib/dentaku/ast/string.rb +7 -0
  33. data/lib/dentaku/ast.rb +2 -0
  34. data/lib/dentaku/parser.rb +5 -3
  35. data/lib/dentaku/print_visitor.rb +101 -0
  36. data/lib/dentaku/token_scanner.rb +1 -1
  37. data/lib/dentaku/version.rb +1 -1
  38. data/lib/dentaku/visitor/infix.rb +82 -0
  39. data/spec/ast/all_spec.rb +25 -0
  40. data/spec/ast/any_spec.rb +23 -0
  41. data/spec/ast/comparator_spec.rb +6 -9
  42. data/spec/ast/filter_spec.rb +7 -0
  43. data/spec/ast/function_spec.rb +5 -0
  44. data/spec/ast/map_spec.rb +12 -0
  45. data/spec/ast/pluck_spec.rb +32 -0
  46. data/spec/ast/xor_spec.rb +35 -0
  47. data/spec/calculator_spec.rb +58 -2
  48. data/spec/parser_spec.rb +18 -3
  49. data/spec/print_visitor_spec.rb +66 -0
  50. data/spec/tokenizer_spec.rb +6 -0
  51. data/spec/visitor/infix_spec.rb +31 -0
  52. data/spec/visitor_spec.rb +137 -0
  53. metadata +24 -6
@@ -34,6 +34,14 @@ module Dentaku
34
34
  context.key?(identifier) ? dependencies_of(context[identifier], context) : [identifier]
35
35
  end
36
36
 
37
+ def accept(visitor)
38
+ visitor.visit_identifier(self)
39
+ end
40
+
41
+ def to_s
42
+ identifier.to_s
43
+ end
44
+
37
45
  private
38
46
 
39
47
  def dependencies_of(node, context)
@@ -4,6 +4,7 @@ module Dentaku
4
4
  attr_reader :type
5
5
 
6
6
  def initialize(token)
7
+ @token = token
7
8
  @value = token.value
8
9
  @type = token.category
9
10
  end
@@ -15,6 +16,15 @@ module Dentaku
15
16
  def dependencies(*)
16
17
  []
17
18
  end
19
+
20
+ def accept(visitor)
21
+ visitor.visit_literal(self)
22
+ end
23
+
24
+ def quoted
25
+ @token.raw_value || value.to_s
26
+ end
27
+ alias_method :to_s, :quoted
18
28
  end
19
29
  end
20
30
  end
@@ -40,6 +40,10 @@ module Dentaku
40
40
  @node.dependencies(context)
41
41
  end
42
42
 
43
+ def accept(visitor)
44
+ visitor.visit_negation(self)
45
+ end
46
+
43
47
  private
44
48
 
45
49
  def valid_node?(node)
@@ -4,6 +4,10 @@ module Dentaku
4
4
  def value(*)
5
5
  nil
6
6
  end
7
+
8
+ def accept(visitor)
9
+ visitor.visit_nil(self)
10
+ end
7
11
  end
8
12
  end
9
13
  end
@@ -19,6 +19,10 @@ module Dentaku
19
19
  def type
20
20
  nil
21
21
  end
22
+
23
+ def name
24
+ self.class.name.to_s.split("::").last.upcase
25
+ end
22
26
  end
23
27
  end
24
28
  end
@@ -25,6 +25,15 @@ module Dentaku
25
25
  def self.right_associative?
26
26
  false
27
27
  end
28
+
29
+ def accept(visitor)
30
+ visitor.visit_operation(self)
31
+ end
32
+
33
+ def display_operator
34
+ operator.to_s
35
+ end
36
+ alias_method :to_s, :display_operator
28
37
  end
29
38
  end
30
39
  end
@@ -3,6 +3,13 @@ require_relative "./literal"
3
3
  module Dentaku
4
4
  module AST
5
5
  class String < Literal
6
+ def quoted
7
+ %Q{"#{ escaped }"}
8
+ end
9
+
10
+ def escaped
11
+ @value.gsub('"', '\"')
12
+ end
6
13
  end
7
14
  end
8
15
  end
data/lib/dentaku/ast.rb CHANGED
@@ -21,6 +21,7 @@ require_relative './ast/functions/any'
21
21
  require_relative './ast/functions/avg'
22
22
  require_relative './ast/functions/count'
23
23
  require_relative './ast/functions/duration'
24
+ require_relative './ast/functions/filter'
24
25
  require_relative './ast/functions/if'
25
26
  require_relative './ast/functions/map'
26
27
  require_relative './ast/functions/max'
@@ -35,3 +36,4 @@ require_relative './ast/functions/ruby_math'
35
36
  require_relative './ast/functions/string_functions'
36
37
  require_relative './ast/functions/sum'
37
38
  require_relative './ast/functions/switch'
39
+ require_relative './ast/functions/xor'
@@ -22,6 +22,7 @@ module Dentaku
22
22
 
23
23
  and: AST::And,
24
24
  or: AST::Or,
25
+ xor: AST::Xor,
25
26
  }.freeze
26
27
 
27
28
  attr_reader :input, :output, :operations, :arities, :case_sensitive
@@ -43,11 +44,11 @@ module Dentaku
43
44
  min_size = operator.arity || operator.min_param_count || count
44
45
  max_size = operator.arity || operator.max_param_count || count
45
46
 
46
- if output.length < min_size
47
+ if output.length < min_size || args_size < min_size
47
48
  fail! :too_few_operands, operator: operator, expect: min_size, actual: output.length
48
49
  end
49
50
 
50
- if output.length > max_size && operations.empty?
51
+ if output.length > max_size && operations.empty? || args_size > max_size
51
52
  fail! :too_many_operands, operator: operator, expect: max_size, actual: output.length
52
53
  end
53
54
 
@@ -143,7 +144,8 @@ module Dentaku
143
144
  inner_case_inputs,
144
145
  operations: [AST::Case],
145
146
  arities: [0],
146
- function_registry: @function_registry
147
+ function_registry: @function_registry,
148
+ case_sensitive: case_sensitive
147
149
  )
148
150
  subparser.parse
149
151
  output.concat(subparser.output)
@@ -0,0 +1,101 @@
1
+ module Dentaku
2
+ class PrintVisitor
3
+ def initialize(node)
4
+ @output = ''
5
+ node.accept(self)
6
+ end
7
+
8
+ def visit_operation(node)
9
+ if node.left
10
+ visit_operand(node.left, node.class.precedence, suffix: " ")
11
+ end
12
+
13
+ @output << node.display_operator
14
+
15
+ if node.right
16
+ visit_operand(node.right, node.class.precedence, prefix: " ")
17
+ end
18
+ end
19
+
20
+ def visit_operand(node, precedence, prefix: "", suffix: "")
21
+ @output << prefix
22
+ @output << "(" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
23
+ node.accept(self)
24
+ @output << ")" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
25
+ @output << suffix
26
+ end
27
+
28
+ def visit_function(node)
29
+ @output << node.name
30
+ @output << "("
31
+ arg_count = node.args.length
32
+ node.args.each_with_index do |a, index|
33
+ a.accept(self)
34
+ @output << ", " unless index >= arg_count - 1
35
+ end
36
+ @output << ")"
37
+ end
38
+
39
+ def visit_case(node)
40
+ @output << "CASE "
41
+ node.switch.accept(self)
42
+ node.conditions.each { |c| c.accept(self) }
43
+ node.else && node.else.accept(self)
44
+ @output << " END"
45
+ end
46
+
47
+ def visit_switch(node)
48
+ node.node.accept(self)
49
+ end
50
+
51
+ def visit_case_conditional(node)
52
+ node.when.accept(self)
53
+ node.then.accept(self)
54
+ end
55
+
56
+ def visit_when(node)
57
+ @output << " WHEN "
58
+ node.node.accept(self)
59
+ end
60
+
61
+ def visit_then(node)
62
+ @output << " THEN "
63
+ node.node.accept(self)
64
+ end
65
+
66
+ def visit_else(node)
67
+ @output << " ELSE "
68
+ node.node.accept(self)
69
+ end
70
+
71
+ def visit_negation(node)
72
+ @output << "-"
73
+ @output << "(" unless node.node.is_a? Dentaku::AST::Literal
74
+ node.node.accept(self)
75
+ @output << ")" unless node.node.is_a? Dentaku::AST::Literal
76
+ end
77
+
78
+ def visit_access(node)
79
+ node.structure.accept(self)
80
+ @output << "["
81
+ node.index.accept(self)
82
+ @output << "]"
83
+ end
84
+
85
+ def visit_literal(node)
86
+ @output << node.quoted
87
+ end
88
+
89
+ def visit_identifier(node)
90
+ @output << node.identifier
91
+ end
92
+
93
+ def visit_nil(node)
94
+ @output << "NULL"
95
+ end
96
+
97
+ def to_s
98
+ @output
99
+ end
100
+ end
101
+ end
@@ -91,7 +91,7 @@ module Dentaku
91
91
 
92
92
  def numeric
93
93
  new(:numeric, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
94
- raw =~ /\./ ? BigDecimal(raw) : raw.to_i
94
+ raw =~ /(\.|e|E)/ ? BigDecimal(raw) : raw.to_i
95
95
  })
96
96
  end
97
97
 
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.4.2"
2
+ VERSION = "3.5.0"
3
3
  end
@@ -0,0 +1,82 @@
1
+ # infix visitor
2
+ #
3
+ # use this visitor in a processor to get infix visiting order
4
+ #
5
+ # visitor node deps
6
+ # accept -> visit left ->
7
+ # process
8
+ # visit right ->
9
+ module Dentaku
10
+ module Visitor
11
+ module Infix
12
+ def visit(ast)
13
+ ast.accept(self)
14
+ end
15
+
16
+ def process(_ast)
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def visit_function(node)
21
+ node.args.each do |arg|
22
+ visit(arg)
23
+ end
24
+ process(node)
25
+ end
26
+
27
+ def visit_identifier(node)
28
+ process(node)
29
+ end
30
+
31
+ def visit_operation(node)
32
+ visit(node.left) if node.left
33
+ process(node)
34
+ visit(node.right) if node.right
35
+ end
36
+
37
+ def visit_operand(node)
38
+ process(node)
39
+ end
40
+
41
+ def visit_case(node)
42
+ process(node)
43
+ end
44
+
45
+ def visit_switch(node)
46
+ process(node)
47
+ end
48
+
49
+ def visit_case_conditional(node)
50
+ process(node)
51
+ end
52
+
53
+ def visit_when(node)
54
+ process(node)
55
+ end
56
+
57
+ def visit_then(node)
58
+ process(node)
59
+ end
60
+
61
+ def visit_else(node)
62
+ process(node)
63
+ end
64
+
65
+ def visit_negation(node)
66
+ process(node)
67
+ end
68
+
69
+ def visit_access(node)
70
+ process(node)
71
+ end
72
+
73
+ def visit_literal(node)
74
+ process(node)
75
+ end
76
+
77
+ def visit_nil(node)
78
+ process(node)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,25 @@
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
+ it 'performs ALL operation' do
8
+ result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
9
+ expect(result).to eq(false)
10
+ end
11
+
12
+ it 'works with a single value if needed for some reason' do
13
+ result = Dentaku('ALL(vals, val, val > 1)', vals: 1)
14
+ expect(result).to eq(false)
15
+
16
+ result = Dentaku('ALL(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!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
22
+ Dentaku::ArgumentError, 'ALL() requires second argument to be an identifier'
23
+ )
24
+ end
25
+ 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
@@ -26,15 +26,16 @@ describe Dentaku::AST::Comparator do
26
26
  expect { less_than_or_equal(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
27
27
  expect { greater_than(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
28
28
  expect { greater_than_or_equal(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
29
+ expect { greater_than_or_equal(one, x).value(ctx) }.to raise_error Dentaku::ArgumentError
29
30
  expect { not_equal(one, nilly).value(ctx) }.to_not raise_error
30
31
  expect { equal(one, nilly).value(ctx) }.to_not raise_error
31
32
  end
32
33
 
33
- it 'raises a dentaku error when nil is passed in as first argument' do
34
- expect { less_than(nilly, one).value(ctx) }.to raise_error Dentaku::Error
35
- expect { less_than_or_equal(nilly, one).value(ctx) }.to raise_error Dentaku::Error
36
- expect { greater_than(nilly, one).value(ctx) }.to raise_error Dentaku::Error
37
- expect { greater_than_or_equal(nilly, one).value(ctx) }.to raise_error Dentaku::Error
34
+ it 'raises a dentaku argument error when nil is passed in as first argument' do
35
+ expect { less_than(nilly, one).value(ctx) }.to raise_error Dentaku::ArgumentError
36
+ expect { less_than_or_equal(nilly, one).value(ctx) }.to raise_error Dentaku::ArgumentError
37
+ expect { greater_than(nilly, one).value(ctx) }.to raise_error Dentaku::ArgumentError
38
+ expect { greater_than_or_equal(nilly, one).value(ctx) }.to raise_error Dentaku::ArgumentError
38
39
  expect { not_equal(nilly, one).value(ctx) }.to_not raise_error
39
40
  expect { equal(nilly, one).value(ctx) }.to_not raise_error
40
41
  end
@@ -50,10 +51,6 @@ describe Dentaku::AST::Comparator do
50
51
  .to raise_error(NotImplementedError)
51
52
  end
52
53
 
53
- it 'relies on inheriting classes to expose value method' do
54
- expect { described_class.new(one, two).value(ctx) }.to raise_error NoMethodError
55
- end
56
-
57
54
  private
58
55
 
59
56
  def less_than(left, right)
@@ -3,6 +3,7 @@ require 'dentaku/ast/functions/filter'
3
3
  require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Filter do
6
+ let(:calculator) { Dentaku::Calculator.new }
6
7
  it 'excludes unmatched values' do
7
8
  result = Dentaku('SUM(FILTER(vals, val, val > 1))', vals: [1, 2, 3])
8
9
  expect(result).to eq(5)
@@ -15,4 +16,10 @@ describe Dentaku::AST::Filter do
15
16
  result = Dentaku('FILTER(vals, val, val > 1)', vals: 2)
16
17
  expect(result).to eq([2])
17
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
18
25
  end
@@ -61,4 +61,9 @@ describe Dentaku::AST::Function do
61
61
  expect { described_class.numeric('7.') }.to raise_error Dentaku::ArgumentError
62
62
  expect { described_class.numeric(true) }.to raise_error Dentaku::ArgumentError
63
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
64
69
  end
data/spec/ast/map_spec.rb CHANGED
@@ -3,6 +3,7 @@ require 'dentaku/ast/functions/map'
3
3
  require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Map do
6
+ let(:calculator) { Dentaku::Calculator.new }
6
7
  it 'operates on each value in an array' do
7
8
  result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
8
9
  expect(result).to eq(9)
@@ -12,4 +13,15 @@ describe Dentaku::AST::Map do
12
13
  result = Dentaku('MAP(vals, val, val + 1)', vals: [])
13
14
  expect(result).to eq([])
14
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
15
27
  end
@@ -0,0 +1,32 @@
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
+ it 'operates on each value in an array' do
8
+ result = Dentaku('PLUCK(users, age)', users: [
9
+ {name: "Bob", age: 44},
10
+ {name: "Jane", age: 27}
11
+ ])
12
+ expect(result).to eq([44, 27])
13
+ end
14
+
15
+ it 'works with an empty array' do
16
+ result = Dentaku('PLUCK(users, age)', users: [])
17
+ expect(result).to eq([])
18
+ end
19
+
20
+ it 'raises argument error if a string is passed as identifier' do
21
+ expect do Dentaku.evaluate!('PLUCK(users, "age")', users: [
22
+ {name: "Bob", age: 44},
23
+ {name: "Jane", age: 27}
24
+ ]) end.to raise_error(Dentaku::ArgumentError, 'PLUCK() requires second argument to be an identifier')
25
+ end
26
+
27
+ it 'raises argument error if a non array of hashes is passed as collection' do
28
+ expect { calculator.evaluate!('PLUCK({1, 2, 3}, age)') }.to raise_error(
29
+ Dentaku::ArgumentError, 'PLUCK() requires first argument to be an array of hashes'
30
+ )
31
+ end
32
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+ require 'dentaku'
3
+ require 'dentaku/ast/functions/or'
4
+
5
+ describe 'Dentaku::AST::Xor' do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+
8
+ it 'returns false if all of the arguments are false' do
9
+ result = Dentaku('XOR(false, false)')
10
+ expect(result).to eq(false)
11
+ end
12
+
13
+ it 'returns true if only one of the arguments is true' do
14
+ result = Dentaku('XOR(false, true)')
15
+ expect(result).to eq(true)
16
+ end
17
+
18
+ it 'returns false if more than one of the arguments is true' do
19
+ result = Dentaku('XOR(false, true, true)')
20
+ expect(result).to eq(false)
21
+ end
22
+
23
+ it 'supports nested expressions' do
24
+ result = Dentaku('XOR(y = 1, x = 1)', x: 1, y: 2)
25
+ expect(result).to eq(true)
26
+ end
27
+
28
+ it 'raises an error if no arguments are passed' do
29
+ expect { calculator.evaluate!('XOR()') }.to raise_error(Dentaku::ParseError)
30
+ end
31
+
32
+ it 'raises an error if a non logical argument is passed' do
33
+ expect { calculator.evaluate!('XOR("r")') }.to raise_error(Dentaku::ArgumentError)
34
+ end
35
+ end
@@ -2,6 +2,7 @@ require 'spec_helper'
2
2
  require 'dentaku'
3
3
  describe Dentaku::Calculator do
4
4
  let(:calculator) { described_class.new }
5
+ let(:with_case_sensitivity) { described_class.new(case_sensitive: true) }
5
6
  let(:with_memory) { described_class.new.store(apples: 3) }
6
7
  let(:with_aliases) { described_class.new(aliases: { round: ['rrround'] }) }
7
8
  let(:without_nested_data) { described_class.new(nested_data_support: false) }
@@ -186,6 +187,11 @@ describe Dentaku::Calculator do
186
187
  it "finds no dependencies in array literals" do
187
188
  expect(calculator.dependencies([1, 2, 3])).to eq([])
188
189
  end
190
+
191
+ it "finds dependencies in item expressions" do
192
+ expect(calculator.dependencies('MAP(vals, val, val + step)')).to eq(['vals', 'step'])
193
+ expect(calculator.dependencies('ALL(people, person, person.age < adult)')).to eq(['people', 'adult'])
194
+ end
189
195
  end
190
196
 
191
197
  describe 'solve!' do
@@ -257,8 +263,13 @@ describe Dentaku::Calculator do
257
263
 
258
264
  describe 'solve' do
259
265
  it "returns :undefined when variables are unbound" do
260
- expressions = {more_apples: "apples + 1"}
261
- expect(calculator.solve(expressions)).to eq(more_apples: :undefined)
266
+ expressions = {more_apples: "apples + 1", compare_apples: "apples > 1"}
267
+ expect(calculator.solve(expressions)).to eq(more_apples: :undefined, compare_apples: :undefined)
268
+ end
269
+
270
+ it "returns :undefined when variables are nil" do
271
+ expressions = {more_apples: "apples + 1", compare_apples: "apples > 1"}
272
+ expect(calculator.store(apples: nil).solve(expressions)).to eq(more_apples: :undefined, compare_apples: :undefined)
262
273
  end
263
274
 
264
275
  it "allows passing in a custom value to an error handler" do
@@ -304,6 +315,20 @@ describe Dentaku::Calculator do
304
315
  )
305
316
  }.not_to raise_error
306
317
  end
318
+
319
+ it "integrates with custom functions" do
320
+ calculator.add_function(:custom, :integer, -> { 1 })
321
+
322
+ result = calculator.solve(
323
+ a: "1",
324
+ b: "CUSTOM() - a"
325
+ )
326
+
327
+ expect(result).to eq(
328
+ a: 1,
329
+ b: 0
330
+ )
331
+ end
307
332
  end
308
333
 
309
334
  it 'evaluates a statement with no variables' do
@@ -666,6 +691,31 @@ describe Dentaku::Calculator do
666
691
  expect(value).to eq(5)
667
692
  end
668
693
 
694
+ it 'handles nested case statements with case-sensitivity' do
695
+ formula = <<-FORMULA
696
+ CASE fruit
697
+ WHEN 'apple'
698
+ THEN 1 * quantity
699
+ WHEN 'banana'
700
+ THEN
701
+ CASE QUANTITY
702
+ WHEN 1 THEN 2
703
+ WHEN 10 THEN
704
+ CASE type
705
+ WHEN 'organic' THEN 5
706
+ END
707
+ END
708
+ END
709
+ FORMULA
710
+ value = with_case_sensitivity.evaluate(
711
+ formula,
712
+ type: 'organic',
713
+ quantity: 1,
714
+ QUANTITY: 10,
715
+ fruit: 'banana')
716
+ expect(value).to eq(5)
717
+ end
718
+
669
719
  it 'handles multiple nested case statements' do
670
720
  formula = <<-FORMULA
671
721
  CASE fruit
@@ -710,6 +760,12 @@ describe Dentaku::Calculator do
710
760
  end
711
761
  end
712
762
  end
763
+
764
+ it 'are defined with a properly named class that represents it to support AST marshaling' do
765
+ expect {
766
+ Marshal.dump(calculator.ast('SQRT(20)'))
767
+ }.not_to raise_error
768
+ end
713
769
  end
714
770
 
715
771
  describe 'disable_cache' do