dentaku 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -10
  3. data/.travis.yml +3 -6
  4. data/CHANGELOG.md +38 -1
  5. data/README.md +2 -2
  6. data/dentaku.gemspec +0 -2
  7. data/lib/dentaku.rb +14 -6
  8. data/lib/dentaku/ast.rb +5 -0
  9. data/lib/dentaku/ast/access.rb +15 -1
  10. data/lib/dentaku/ast/arithmetic.rb +29 -6
  11. data/lib/dentaku/ast/array.rb +15 -1
  12. data/lib/dentaku/ast/case.rb +13 -3
  13. data/lib/dentaku/ast/case/case_conditional.rb +13 -2
  14. data/lib/dentaku/ast/case/case_else.rb +12 -4
  15. data/lib/dentaku/ast/case/case_switch_variable.rb +8 -0
  16. data/lib/dentaku/ast/case/case_then.rb +12 -4
  17. data/lib/dentaku/ast/case/case_when.rb +12 -4
  18. data/lib/dentaku/ast/function.rb +11 -2
  19. data/lib/dentaku/ast/function_registry.rb +21 -0
  20. data/lib/dentaku/ast/functions/all.rb +36 -0
  21. data/lib/dentaku/ast/functions/any.rb +36 -0
  22. data/lib/dentaku/ast/functions/avg.rb +2 -2
  23. data/lib/dentaku/ast/functions/count.rb +8 -0
  24. data/lib/dentaku/ast/functions/duration.rb +51 -0
  25. data/lib/dentaku/ast/functions/if.rb +15 -2
  26. data/lib/dentaku/ast/functions/map.rb +36 -0
  27. data/lib/dentaku/ast/functions/mul.rb +3 -2
  28. data/lib/dentaku/ast/functions/pluck.rb +29 -0
  29. data/lib/dentaku/ast/functions/round.rb +1 -1
  30. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  31. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  32. data/lib/dentaku/ast/functions/ruby_math.rb +47 -3
  33. data/lib/dentaku/ast/functions/string_functions.rb +68 -4
  34. data/lib/dentaku/ast/functions/sum.rb +3 -2
  35. data/lib/dentaku/ast/grouping.rb +3 -1
  36. data/lib/dentaku/ast/identifier.rb +5 -1
  37. data/lib/dentaku/ast/negation.rb +3 -1
  38. data/lib/dentaku/ast/node.rb +4 -0
  39. data/lib/dentaku/ast/operation.rb +8 -0
  40. data/lib/dentaku/bulk_expression_solver.rb +34 -25
  41. data/lib/dentaku/calculator.rb +19 -6
  42. data/lib/dentaku/date_arithmetic.rb +45 -0
  43. data/lib/dentaku/exceptions.rb +4 -4
  44. data/lib/dentaku/flat_hash.rb +9 -2
  45. data/lib/dentaku/parser.rb +31 -14
  46. data/lib/dentaku/token_matcher.rb +1 -1
  47. data/lib/dentaku/token_scanner.rb +1 -1
  48. data/lib/dentaku/tokenizer.rb +7 -2
  49. data/lib/dentaku/version.rb +1 -1
  50. data/spec/ast/addition_spec.rb +7 -1
  51. data/spec/ast/and_function_spec.rb +6 -6
  52. data/spec/ast/and_spec.rb +1 -1
  53. data/spec/ast/arithmetic_spec.rb +57 -29
  54. data/spec/ast/avg_spec.rb +9 -5
  55. data/spec/ast/count_spec.rb +7 -7
  56. data/spec/ast/division_spec.rb +7 -1
  57. data/spec/ast/function_spec.rb +9 -9
  58. data/spec/ast/max_spec.rb +3 -3
  59. data/spec/ast/min_spec.rb +3 -3
  60. data/spec/ast/mul_spec.rb +10 -6
  61. data/spec/ast/negation_spec.rb +48 -0
  62. data/spec/ast/node_spec.rb +11 -8
  63. data/spec/ast/numeric_spec.rb +1 -1
  64. data/spec/ast/or_spec.rb +6 -6
  65. data/spec/ast/round_spec.rb +14 -4
  66. data/spec/ast/rounddown_spec.rb +14 -4
  67. data/spec/ast/roundup_spec.rb +14 -4
  68. data/spec/ast/string_functions_spec.rb +35 -0
  69. data/spec/ast/sum_spec.rb +10 -6
  70. data/spec/ast/switch_spec.rb +5 -5
  71. data/spec/bulk_expression_solver_spec.rb +18 -1
  72. data/spec/calculator_spec.rb +173 -28
  73. data/spec/dentaku_spec.rb +18 -5
  74. data/spec/external_function_spec.rb +29 -5
  75. data/spec/parser_spec.rb +85 -123
  76. data/spec/spec_helper.rb +6 -4
  77. data/spec/token_matcher_spec.rb +8 -8
  78. data/spec/token_scanner_spec.rb +4 -4
  79. data/spec/tokenizer_spec.rb +32 -13
  80. metadata +11 -4
@@ -1,10 +1,6 @@
1
1
  module Dentaku
2
2
  module AST
3
3
  class CaseElse < Node
4
- def self.arity
5
- 1
6
- end
7
-
8
4
  def initialize(node)
9
5
  @node = node
10
6
  end
@@ -16,6 +12,18 @@ module Dentaku
16
12
  def dependencies(context = {})
17
13
  @node.dependencies(context)
18
14
  end
15
+
16
+ def self.arity
17
+ 1
18
+ end
19
+
20
+ def self.min_param_count
21
+ 1
22
+ end
23
+
24
+ def self.max_param_count
25
+ 1
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -16,6 +16,14 @@ module Dentaku
16
16
  def self.arity
17
17
  1
18
18
  end
19
+
20
+ def self.min_param_count
21
+ 1
22
+ end
23
+
24
+ def self.max_param_count
25
+ 1
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -1,10 +1,6 @@
1
1
  module Dentaku
2
2
  module AST
3
3
  class CaseThen < Node
4
- def self.arity
5
- 1
6
- end
7
-
8
4
  def initialize(node)
9
5
  @node = node
10
6
  end
@@ -16,6 +12,18 @@ module Dentaku
16
12
  def dependencies(context = {})
17
13
  @node.dependencies(context)
18
14
  end
15
+
16
+ def self.arity
17
+ 1
18
+ end
19
+
20
+ def self.min_param_count
21
+ 1
22
+ end
23
+
24
+ def self.max_param_count
25
+ 1
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -1,10 +1,6 @@
1
1
  module Dentaku
2
2
  module AST
3
3
  class CaseWhen < Operation
4
- def self.arity
5
- 1
6
- end
7
-
8
4
  def initialize(node)
9
5
  @node = node
10
6
  end
@@ -16,6 +12,18 @@ module Dentaku
16
12
  def dependencies(context = {})
17
13
  @node.dependencies(context)
18
14
  end
15
+
16
+ def self.arity
17
+ 1
18
+ end
19
+
20
+ def self.min_param_count
21
+ 1
22
+ end
23
+
24
+ def self.max_param_count
25
+ 1
26
+ end
19
27
  end
20
28
  end
21
29
  end
@@ -12,7 +12,16 @@ module Dentaku
12
12
  end
13
13
 
14
14
  def dependencies(context = {})
15
- @args.flat_map { |a| a.dependencies(context) }
15
+ deferred = deferred_args
16
+ @args.each_with_index
17
+ .reject { |_, i| deferred.include? i }
18
+ .flat_map { |a, _| a.dependencies(context) }
19
+ end
20
+
21
+ # override if your function implementation needs to defer evaluation of
22
+ # any arguments
23
+ def deferred_args
24
+ []
16
25
  end
17
26
 
18
27
  def self.get(name)
@@ -38,7 +47,7 @@ module Dentaku
38
47
 
39
48
  if value.is_a?(::String)
40
49
  number = value[/\A-?\d*\.?\d+\z/]
41
- return number.include?('.') ? ::BigDecimal.new(number, DIG) : number.to_i if number
50
+ return number.include?('.') ? BigDecimal(number, DIG) : number.to_i if number
42
51
  end
43
52
 
44
53
  raise Dentaku::ArgumentError.for(:incompatible_type, value: value, for: Numeric),
@@ -38,6 +38,14 @@ module Dentaku
38
38
  @implementation.arity < 0 ? nil : @implementation.arity
39
39
  end
40
40
 
41
+ def self.min_param_count
42
+ @implementation.parameters.select { |type, _name| type == :req }.count
43
+ end
44
+
45
+ def self.max_param_count
46
+ @implementation.parameters.select { |type, _name| type == :rest }.any? ? Float::INFINITY : @implementation.parameters.count
47
+ end
48
+
41
49
  def value(context = {})
42
50
  args = @args.map { |a| a.value(context) }
43
51
  self.class.implementation.call(*args)
@@ -48,6 +56,8 @@ module Dentaku
48
56
  end
49
57
  end
50
58
 
59
+ define_class(name, function)
60
+
51
61
  function.name = name
52
62
  function.type = type
53
63
  function.implementation = implementation
@@ -72,6 +82,17 @@ module Dentaku
72
82
  def function_name(name)
73
83
  name.to_s.downcase
74
84
  end
85
+
86
+ def normalize_name(function_name)
87
+ function_name.to_s.capitalize.gsub(/\W/, '_')
88
+ end
89
+
90
+ def define_class(function_name, function)
91
+ class_name = normalize_name(function_name)
92
+ return if Dentaku::AST::Function.const_defined?(class_name)
93
+
94
+ Dentaku::AST::Function.const_set(class_name, function)
95
+ end
75
96
  end
76
97
  end
77
98
  end
@@ -0,0 +1,36 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class All < Function
7
+ def self.min_param_count
8
+ 3
9
+ end
10
+
11
+ def self.max_param_count
12
+ 3
13
+ end
14
+
15
+ def deferred_args
16
+ [1, 2]
17
+ end
18
+
19
+ def value(context = {})
20
+ collection = @args[0].value(context)
21
+ item_identifier = @args[1].identifier
22
+ expression = @args[2]
23
+
24
+ Array(collection).all? do |item_value|
25
+ expression.value(
26
+ context.update(
27
+ FlatHash.from_hash(item_identifier => item_value)
28
+ )
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ Dentaku::AST::Function.register_class(:all, Dentaku::AST::All)
@@ -0,0 +1,36 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Any < Function
7
+ def self.min_param_count
8
+ 3
9
+ end
10
+
11
+ def self.max_param_count
12
+ 3
13
+ end
14
+
15
+ def deferred_args
16
+ [1, 2]
17
+ end
18
+
19
+ def value(context = {})
20
+ collection = @args[0].value(context)
21
+ item_identifier = @args[1].identifier
22
+ expression = @args[2]
23
+
24
+ Array(collection).any? do |item_value|
25
+ expression.value(
26
+ context.update(
27
+ FlatHash.from_hash(item_identifier => item_value)
28
+ )
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ Dentaku::AST::Function.register_class(:any, Dentaku::AST::Any)
@@ -1,13 +1,13 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
4
- if args.empty?
4
+ flatten_args = args.flatten
5
+ if flatten_args.empty?
5
6
  raise Dentaku::ArgumentError.for(
6
7
  :too_few_arguments,
7
8
  function_name: 'AVG()', at_least: 1, given: 0
8
9
  ), 'AVG() requires at least one argument'
9
10
  end
10
11
 
11
- flatten_args = args.flatten
12
12
  flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / flatten_args.length
13
13
  })
@@ -3,6 +3,14 @@ require_relative '../function'
3
3
  module Dentaku
4
4
  module AST
5
5
  class Count < Function
6
+ def self.min_param_count
7
+ 0
8
+ end
9
+
10
+ def self.max_param_count
11
+ Float::INFINITY
12
+ end
13
+
6
14
  def value(context = {})
7
15
  if @args.length == 1
8
16
  first_arg = @args[0].value(context)
@@ -0,0 +1,51 @@
1
+ require_relative '../function'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Duration < Function
6
+ def self.min_param_count
7
+ 1
8
+ end
9
+
10
+ def self.max_param_count
11
+ 1
12
+ end
13
+
14
+ class Value
15
+ attr_reader :value, :unit
16
+
17
+ def initialize(value, unit)
18
+ @value = value
19
+ @unit = validate_unit(unit)
20
+ end
21
+
22
+ def validate_unit(unit)
23
+ case unit.downcase
24
+ when /years?/ then :year
25
+ when /months?/ then :month
26
+ when /days?/ then :day
27
+ else
28
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: unit, for: Duration),
29
+ "'#{unit || unit.class}' is not a valid duration unit"
30
+ end
31
+ end
32
+ end
33
+
34
+ def type
35
+ :duration
36
+ end
37
+
38
+ def value(context = {})
39
+ value_node, unit_node = *@args
40
+ Value.new(value_node.value(context), unit_node.identifier)
41
+ end
42
+
43
+ def dependencies(context = {})
44
+ value_node = @args.first
45
+ value_node.dependencies(context)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ Dentaku::AST::Function.register_class(:duration, Dentaku::AST::Duration)
@@ -5,6 +5,14 @@ module Dentaku
5
5
  class If < Function
6
6
  attr_reader :predicate, :left, :right
7
7
 
8
+ def self.min_param_count
9
+ 3
10
+ end
11
+
12
+ def self.max_param_count
13
+ 3
14
+ end
15
+
8
16
  def initialize(predicate, left, right)
9
17
  @predicate = predicate
10
18
  @left = left
@@ -24,8 +32,13 @@ module Dentaku
24
32
  end
25
33
 
26
34
  def dependencies(context = {})
27
- # TODO : short-circuit?
28
- (predicate.dependencies(context) + left.dependencies(context) + right.dependencies(context)).uniq
35
+ deps = predicate.dependencies(context)
36
+
37
+ if deps.empty?
38
+ predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
39
+ else
40
+ (deps + left.dependencies(context) + right.dependencies(context)).uniq
41
+ end
29
42
  end
30
43
  end
31
44
  end
@@ -0,0 +1,36 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Map < Function
7
+ def self.min_param_count
8
+ 3
9
+ end
10
+
11
+ def self.max_param_count
12
+ 3
13
+ end
14
+
15
+ def deferred_args
16
+ [1, 2]
17
+ end
18
+
19
+ def value(context = {})
20
+ collection = @args[0].value(context)
21
+ item_identifier = @args[1].identifier
22
+ expression = @args[2]
23
+
24
+ collection.map do |item_value|
25
+ expression.value(
26
+ context.update(
27
+ FlatHash.from_hash(item_identifier => item_value)
28
+ )
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ Dentaku::AST::Function.register_class(:map, Dentaku::AST::Map)
@@ -1,12 +1,13 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:mul, :numeric, ->(*args) {
4
- if args.empty?
4
+ flatten_args = args.flatten
5
+ if flatten_args.empty?
5
6
  raise Dentaku::ArgumentError.for(
6
7
  :too_few_arguments,
7
8
  function_name: 'MUL()', at_least: 1, given: 0
8
9
  ), 'MUL() requires at least one argument'
9
10
  end
10
11
 
11
- args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
12
+ flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(1, :*)
12
13
  })
@@ -0,0 +1,29 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Pluck < Function
7
+ def self.min_param_count
8
+ 2
9
+ end
10
+
11
+ def self.max_param_count
12
+ 2
13
+ end
14
+
15
+ def deferred_args
16
+ [1]
17
+ end
18
+
19
+ def value(context = {})
20
+ collection = @args[0].value(context)
21
+ pluck_path = @args[1].identifier
22
+
23
+ collection.map { |h| h.transform_keys(&:to_s)[pluck_path] }
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ Dentaku::AST::Function.register_class(:pluck, Dentaku::AST::Pluck)