dentaku 3.4.0 → 3.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +4 -3
  3. data/CHANGELOG.md +28 -0
  4. data/README.md +5 -4
  5. data/dentaku.gemspec +2 -0
  6. data/lib/dentaku/ast/access.rb +6 -0
  7. data/lib/dentaku/ast/arithmetic.rb +5 -1
  8. data/lib/dentaku/ast/array.rb +4 -0
  9. data/lib/dentaku/ast/bitwise.rb +8 -0
  10. data/lib/dentaku/ast/case/case_conditional.rb +4 -0
  11. data/lib/dentaku/ast/case/case_else.rb +6 -0
  12. data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
  13. data/lib/dentaku/ast/case/case_then.rb +6 -0
  14. data/lib/dentaku/ast/case/case_when.rb +10 -0
  15. data/lib/dentaku/ast/case.rb +6 -0
  16. data/lib/dentaku/ast/comparators.rb +25 -35
  17. data/lib/dentaku/ast/function.rb +6 -8
  18. data/lib/dentaku/ast/functions/all.rb +6 -19
  19. data/lib/dentaku/ast/functions/any.rb +6 -19
  20. data/lib/dentaku/ast/functions/duration.rb +2 -2
  21. data/lib/dentaku/ast/functions/enum.rb +37 -0
  22. data/lib/dentaku/ast/functions/filter.rb +23 -0
  23. data/lib/dentaku/ast/functions/if.rb +4 -0
  24. data/lib/dentaku/ast/functions/map.rb +5 -18
  25. data/lib/dentaku/ast/functions/mul.rb +2 -3
  26. data/lib/dentaku/ast/functions/pluck.rb +8 -7
  27. data/lib/dentaku/ast/functions/ruby_math.rb +6 -3
  28. data/lib/dentaku/ast/functions/sum.rb +2 -3
  29. data/lib/dentaku/ast/functions/xor.rb +44 -0
  30. data/lib/dentaku/ast/identifier.rb +11 -3
  31. data/lib/dentaku/ast/literal.rb +10 -0
  32. data/lib/dentaku/ast/negation.rb +4 -0
  33. data/lib/dentaku/ast/nil.rb +4 -0
  34. data/lib/dentaku/ast/node.rb +4 -0
  35. data/lib/dentaku/ast/operation.rb +9 -0
  36. data/lib/dentaku/ast/string.rb +7 -0
  37. data/lib/dentaku/ast.rb +2 -0
  38. data/lib/dentaku/bulk_expression_solver.rb +5 -3
  39. data/lib/dentaku/calculator.rb +1 -1
  40. data/lib/dentaku/parser.rb +5 -3
  41. data/lib/dentaku/print_visitor.rb +101 -0
  42. data/lib/dentaku/token_scanner.rb +1 -1
  43. data/lib/dentaku/version.rb +1 -1
  44. data/lib/dentaku/visitor/infix.rb +82 -0
  45. data/lib/dentaku.rb +4 -3
  46. data/spec/ast/all_spec.rb +25 -0
  47. data/spec/ast/any_spec.rb +23 -0
  48. data/spec/ast/comparator_spec.rb +6 -9
  49. data/spec/ast/filter_spec.rb +25 -0
  50. data/spec/ast/function_spec.rb +5 -0
  51. data/spec/ast/map_spec.rb +27 -0
  52. data/spec/ast/max_spec.rb +13 -0
  53. data/spec/ast/min_spec.rb +13 -0
  54. data/spec/ast/mul_spec.rb +3 -2
  55. data/spec/ast/pluck_spec.rb +32 -0
  56. data/spec/ast/sum_spec.rb +3 -2
  57. data/spec/ast/xor_spec.rb +35 -0
  58. data/spec/bulk_expression_solver_spec.rb +10 -0
  59. data/spec/calculator_spec.rb +66 -2
  60. data/spec/parser_spec.rb +18 -3
  61. data/spec/print_visitor_spec.rb +66 -0
  62. data/spec/tokenizer_spec.rb +6 -0
  63. data/spec/visitor/infix_spec.rb +31 -0
  64. data/spec/visitor_spec.rb +137 -0
  65. metadata +43 -6
@@ -1,9 +1,9 @@
1
- require_relative '../function'
1
+ require_relative './enum'
2
2
  require_relative '../../exceptions'
3
3
 
4
4
  module Dentaku
5
5
  module AST
6
- class Pluck < Function
6
+ class Pluck < Enum
7
7
  def self.min_param_count
8
8
  2
9
9
  end
@@ -12,12 +12,13 @@ module Dentaku
12
12
  2
13
13
  end
14
14
 
15
- def deferred_args
16
- [1]
17
- end
18
-
19
15
  def value(context = {})
20
- collection = @args[0].value(context)
16
+ collection = Array(@args[0].value(context))
17
+ unless collection.all? { |elem| elem.is_a?(Hash) }
18
+ raise ArgumentError.for(:incompatible_type, value: collection),
19
+ 'PLUCK() requires first argument to be an array of hashes'
20
+ end
21
+
21
22
  pluck_path = @args[1].identifier
22
23
 
23
24
  collection.map { |h| h.transform_keys(&:to_s)[pluck_path] }
@@ -5,9 +5,10 @@ module Dentaku
5
5
  module AST
6
6
  class RubyMath < Function
7
7
  def self.[](method)
8
- klass = Class.new(self)
8
+ klass_name = method.to_s.capitalize
9
+ klass = const_set(klass_name , Class.new(self))
9
10
  klass.implement(method)
10
- klass
11
+ const_get(klass_name)
11
12
  end
12
13
 
13
14
  def self.implement(method)
@@ -40,8 +41,10 @@ module Dentaku
40
41
  self.class.call(*args)
41
42
  end
42
43
 
44
+ ARRAY_RETURN_TYPES = [:frexp, :lgamma].freeze
45
+
43
46
  def type
44
- nil
47
+ ARRAY_RETURN_TYPES.include?(@name) ? :array : :numeric
45
48
  end
46
49
  end
47
50
  end
@@ -1,13 +1,12 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:sum, :numeric, ->(*args) {
4
- flatten_args = args.flatten
5
- if flatten_args.empty?
4
+ if args.empty?
6
5
  raise Dentaku::ArgumentError.for(
7
6
  :too_few_arguments,
8
7
  function_name: 'SUM()', at_least: 1, given: 0
9
8
  ), 'SUM() requires at least one argument'
10
9
  end
11
10
 
12
- flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+)
11
+ args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+)
13
12
  })
@@ -0,0 +1,44 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Xor < Function
7
+ def self.min_param_count
8
+ 1
9
+ end
10
+
11
+ def self.max_param_count
12
+ Float::INFINITY
13
+ end
14
+
15
+ def value(context = {})
16
+ if @args.empty?
17
+ raise Dentaku::ArgumentError.for(
18
+ :too_few_arguments,
19
+ function_name: 'XOR()', at_least: 1, given: 0
20
+ ), 'XOR() requires at least one argument'
21
+ end
22
+
23
+ true_arg_count = 0
24
+ @args.each do |arg|
25
+ case arg.value(context)
26
+ when TrueClass
27
+ true_arg_count += 1
28
+ break if true_arg_count > 1
29
+ when FalseClass, nil
30
+ next
31
+ else
32
+ raise Dentaku::ArgumentError.for(
33
+ :incompatible_type,
34
+ function_name: 'XOR()', expect: :logical, actual: arg.class
35
+ ), 'XOR() requires arguments to be logical expressions'
36
+ end
37
+ end
38
+ true_arg_count == 1
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ Dentaku::AST::Function.register_class(:xor, Dentaku::AST::Xor)
@@ -31,13 +31,21 @@ module Dentaku
31
31
  end
32
32
 
33
33
  def dependencies(context = {})
34
- context.key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
34
+ context.key?(identifier) ? dependencies_of(context[identifier], context) : [identifier]
35
+ end
36
+
37
+ def accept(visitor)
38
+ visitor.visit_identifier(self)
39
+ end
40
+
41
+ def to_s
42
+ identifier.to_s
35
43
  end
36
44
 
37
45
  private
38
46
 
39
- def dependencies_of(node)
40
- node.respond_to?(:dependencies) ? node.dependencies : []
47
+ def dependencies_of(node, context)
48
+ node.respond_to?(:dependencies) ? node.dependencies(context) : []
41
49
  end
42
50
  end
43
51
  end
@@ -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'
@@ -61,17 +61,19 @@ module Dentaku
61
61
  .transform_values { |v| calculator.ast(v) }
62
62
  .partition { |_, v| calculator.dependencies(v, nil).empty? }
63
63
 
64
- context = calculator.memory.merge(facts.to_h.each_with_object({}) do |(var_name, ast), h|
64
+ evaluated_facts = facts.to_h.each_with_object({}) do |(var_name, ast), h|
65
65
  with_rescues(var_name, h, block) do
66
66
  h[var_name] = ast.is_a?(Array) ? ast.map(&:value) : ast.value
67
67
  end
68
- end)
68
+ end
69
+
70
+ context = calculator.memory.merge(evaluated_facts)
69
71
 
70
72
  variables_in_resolve_order.each_with_object({}) do |var_name, results|
71
73
  next if expressions[var_name].nil?
72
74
 
73
75
  with_rescues(var_name, results, block) do
74
- results[var_name] = calculator.evaluate!(
76
+ results[var_name] = evaluated_facts[var_name] || calculator.evaluate!(
75
77
  expressions[var_name],
76
78
  context.merge(results),
77
79
  &expression_with_exception_handler(&block)
@@ -59,7 +59,7 @@ module Dentaku
59
59
  store(data) do
60
60
  node = expression
61
61
  node = ast(node) unless node.is_a?(AST::Node)
62
- unbound = node.dependencies - memory.keys
62
+ unbound = node.dependencies(memory)
63
63
  unless unbound.empty?
64
64
  raise UnboundVariableError.new(unbound),
65
65
  "no value provided for variables: #{unbound.uniq.join(', ')}"
@@ -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.0"
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
data/lib/dentaku.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require "bigdecimal"
2
+ require "concurrent"
2
3
  require "dentaku/calculator"
3
4
  require "dentaku/version"
4
5
 
@@ -9,11 +10,11 @@ module Dentaku
9
10
  @aliases = {}
10
11
 
11
12
  def self.evaluate(expression, data = {}, &block)
12
- calculator.evaluate(expression, data, &block)
13
+ calculator.value.evaluate(expression, data, &block)
13
14
  end
14
15
 
15
16
  def self.evaluate!(expression, data = {}, &block)
16
- calculator.evaluate!(expression, data, &block)
17
+ calculator.value.evaluate!(expression, data, &block)
17
18
  end
18
19
 
19
20
  def self.enable_caching!
@@ -55,7 +56,7 @@ module Dentaku
55
56
  end
56
57
 
57
58
  def self.calculator
58
- @calculator ||= Dentaku::Calculator.new
59
+ @calculator ||= Concurrent::ThreadLocalVar.new { Dentaku::Calculator.new }
59
60
  end
60
61
  end
61
62
 
@@ -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)
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/filter'
3
+ require 'dentaku'
4
+
5
+ describe Dentaku::AST::Filter do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+ it 'excludes unmatched values' do
8
+ result = Dentaku('SUM(FILTER(vals, val, val > 1))', vals: [1, 2, 3])
9
+ expect(result).to eq(5)
10
+ end
11
+
12
+ it 'works with a single value if needed for some reason' do
13
+ result = Dentaku('FILTER(vals, val, val > 1)', vals: 1)
14
+ expect(result).to eq([])
15
+
16
+ result = Dentaku('FILTER(vals, val, val > 1)', vals: 2)
17
+ expect(result).to eq([2])
18
+ end
19
+
20
+ it 'raises argument error if a string is passed as identifier' do
21
+ expect { calculator.evaluate!('FILTER({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
22
+ Dentaku::ArgumentError, 'FILTER() requires second argument to be an identifier'
23
+ )
24
+ end
25
+ end
@@ -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