dentaku 3.3.3 → 3.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +2 -7
  3. data/.travis.yml +4 -4
  4. data/CHANGELOG.md +34 -2
  5. data/README.md +4 -2
  6. data/dentaku.gemspec +1 -1
  7. data/lib/dentaku.rb +16 -5
  8. data/lib/dentaku/ast.rb +4 -0
  9. data/lib/dentaku/ast/access.rb +3 -1
  10. data/lib/dentaku/ast/arithmetic.rb +7 -2
  11. data/lib/dentaku/ast/array.rb +3 -1
  12. data/lib/dentaku/ast/case/case_else.rb +12 -4
  13. data/lib/dentaku/ast/case/case_switch_variable.rb +8 -0
  14. data/lib/dentaku/ast/case/case_then.rb +12 -4
  15. data/lib/dentaku/ast/case/case_when.rb +12 -4
  16. data/lib/dentaku/ast/function.rb +10 -1
  17. data/lib/dentaku/ast/functions/all.rb +36 -0
  18. data/lib/dentaku/ast/functions/any.rb +36 -0
  19. data/lib/dentaku/ast/functions/avg.rb +2 -2
  20. data/lib/dentaku/ast/functions/filter.rb +36 -0
  21. data/lib/dentaku/ast/functions/map.rb +36 -0
  22. data/lib/dentaku/ast/functions/pluck.rb +29 -0
  23. data/lib/dentaku/ast/functions/round.rb +1 -1
  24. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  25. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  26. data/lib/dentaku/ast/functions/ruby_math.rb +49 -3
  27. data/lib/dentaku/ast/functions/string_functions.rb +52 -4
  28. data/lib/dentaku/ast/grouping.rb +3 -1
  29. data/lib/dentaku/ast/identifier.rb +6 -4
  30. data/lib/dentaku/bulk_expression_solver.rb +36 -25
  31. data/lib/dentaku/calculator.rb +14 -6
  32. data/lib/dentaku/date_arithmetic.rb +1 -1
  33. data/lib/dentaku/exceptions.rb +3 -3
  34. data/lib/dentaku/flat_hash.rb +7 -0
  35. data/lib/dentaku/parser.rb +2 -1
  36. data/lib/dentaku/tokenizer.rb +1 -1
  37. data/lib/dentaku/version.rb +1 -1
  38. data/spec/ast/arithmetic_spec.rb +19 -5
  39. data/spec/ast/avg_spec.rb +4 -0
  40. data/spec/ast/filter_spec.rb +18 -0
  41. data/spec/ast/map_spec.rb +15 -0
  42. data/spec/ast/max_spec.rb +13 -0
  43. data/spec/ast/min_spec.rb +13 -0
  44. data/spec/ast/mul_spec.rb +5 -0
  45. data/spec/ast/negation_spec.rb +18 -2
  46. data/spec/ast/round_spec.rb +10 -0
  47. data/spec/ast/rounddown_spec.rb +10 -0
  48. data/spec/ast/roundup_spec.rb +10 -0
  49. data/spec/ast/string_functions_spec.rb +35 -0
  50. data/spec/ast/sum_spec.rb +5 -0
  51. data/spec/bulk_expression_solver_spec.rb +27 -0
  52. data/spec/calculator_spec.rb +130 -0
  53. data/spec/dentaku_spec.rb +14 -8
  54. data/spec/parser_spec.rb +13 -0
  55. data/spec/tokenizer_spec.rb +24 -5
  56. metadata +26 -3
@@ -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
  })
@@ -0,0 +1,36 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Filter < 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).select do |item_value|
25
+ expression.value(
26
+ context.merge(
27
+ FlatHash.from_hash_with_intermediates(item_identifier => item_value)
28
+ )
29
+ )
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ Dentaku::AST::Function.register_class(:filter, Dentaku::AST::Filter)
@@ -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.merge(
27
+ FlatHash.from_hash_with_intermediates(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)
@@ -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)
@@ -1,5 +1,5 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:round, :numeric, lambda { |numeric, places = 0|
4
- Dentaku::AST::Function.numeric(numeric).round(places.to_i)
4
+ Dentaku::AST::Function.numeric(numeric).round(Dentaku::AST::Function.numeric(places || 0).to_i)
5
5
  })
@@ -1,7 +1,7 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:rounddown, :numeric, lambda { |numeric, precision = 0|
4
- precision = precision.to_i
4
+ precision = Dentaku::AST::Function.numeric(precision || 0).to_i
5
5
  tens = 10.0**precision
6
6
  result = (Dentaku::AST::Function.numeric(numeric) * tens).floor / tens
7
7
  precision <= 0 ? result.to_i : result
@@ -1,7 +1,7 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:roundup, :numeric, lambda { |numeric, precision = 0|
4
- precision = precision.to_i
4
+ precision = Dentaku::AST::Function.numeric(precision || 0).to_i
5
5
  tens = 10.0**precision
6
6
  result = (Dentaku::AST::Function.numeric(numeric) * tens).ceil / tens
7
7
  precision <= 0 ? result.to_i : result
@@ -1,8 +1,54 @@
1
1
  # import all functions from Ruby's Math module
2
2
  require_relative '../function'
3
3
 
4
+ module Dentaku
5
+ module AST
6
+ class RubyMath < Function
7
+ def self.[](method)
8
+ klass = Class.new(self)
9
+ klass.implement(method)
10
+ klass
11
+ end
12
+
13
+ def self.implement(method)
14
+ @name = method
15
+ @implementation = Math.method(method)
16
+ end
17
+
18
+ def self.name
19
+ @name
20
+ end
21
+
22
+ def self.arity
23
+ @implementation.arity < 0 ? nil : @implementation.arity
24
+ end
25
+
26
+ def self.min_param_count
27
+ @implementation.parameters.select { |type, _name| type == :req }.count
28
+ end
29
+
30
+ def self.max_param_count
31
+ @implementation.parameters.select { |type, _name| type == :rest }.any? ? Float::INFINITY : @implementation.parameters.count
32
+ end
33
+
34
+ def self.call(*args)
35
+ @implementation.call(*args)
36
+ end
37
+
38
+ def value(context = {})
39
+ args = @args.flatten.map { |a| Dentaku::AST::Function.numeric(a.value(context)) }
40
+ self.class.call(*args)
41
+ end
42
+
43
+ ARRAY_RETURN_TYPES = [:frexp, :lgamma].freeze
44
+
45
+ def type
46
+ ARRAY_RETURN_TYPES.include?(@name) ? :array : :numeric
47
+ end
48
+ end
49
+ end
50
+ end
51
+
4
52
  Math.methods(false).each do |method|
5
- Dentaku::AST::Function.register(method, :numeric, lambda { |*args|
6
- Math.send(method, *args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) })
7
- })
53
+ Dentaku::AST::Function.register_class(method, Dentaku::AST::RubyMath[method])
8
54
  end
@@ -32,13 +32,21 @@ module Dentaku
32
32
 
33
33
  def value(context = {})
34
34
  string = @string.value(context).to_s
35
- length = @length.value(context)
35
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
36
36
  negative_argument_failure('LEFT') if length < 0
37
37
  string[0, length]
38
38
  end
39
39
  end
40
40
 
41
41
  class Right < Base
42
+ def self.min_param_count
43
+ 2
44
+ end
45
+
46
+ def self.max_param_count
47
+ 2
48
+ end
49
+
42
50
  def initialize(*args)
43
51
  super
44
52
  @string, @length = *@args
@@ -46,13 +54,21 @@ module Dentaku
46
54
 
47
55
  def value(context = {})
48
56
  string = @string.value(context).to_s
49
- length = @length.value(context)
57
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
50
58
  negative_argument_failure('RIGHT') if length < 0
51
59
  string[length * -1, length] || string
52
60
  end
53
61
  end
54
62
 
55
63
  class Mid < Base
64
+ def self.min_param_count
65
+ 3
66
+ end
67
+
68
+ def self.max_param_count
69
+ 3
70
+ end
71
+
56
72
  def initialize(*args)
57
73
  super
58
74
  @string, @offset, @length = *@args
@@ -60,15 +76,23 @@ module Dentaku
60
76
 
61
77
  def value(context = {})
62
78
  string = @string.value(context).to_s
63
- offset = @offset.value(context)
79
+ offset = Dentaku::AST::Function.numeric(@offset.value(context)).to_i
64
80
  negative_argument_failure('MID', 'offset') if offset < 0
65
- length = @length.value(context)
81
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
66
82
  negative_argument_failure('MID') if length < 0
67
83
  string[offset - 1, length].to_s
68
84
  end
69
85
  end
70
86
 
71
87
  class Len < Base
88
+ def self.min_param_count
89
+ 1
90
+ end
91
+
92
+ def self.max_param_count
93
+ 1
94
+ end
95
+
72
96
  def initialize(*args)
73
97
  super
74
98
  @string = @args[0]
@@ -85,6 +109,14 @@ module Dentaku
85
109
  end
86
110
 
87
111
  class Find < Base
112
+ def self.min_param_count
113
+ 2
114
+ end
115
+
116
+ def self.max_param_count
117
+ 2
118
+ end
119
+
88
120
  def initialize(*args)
89
121
  super
90
122
  @needle, @haystack = *@args
@@ -104,6 +136,14 @@ module Dentaku
104
136
  end
105
137
 
106
138
  class Substitute < Base
139
+ def self.min_param_count
140
+ 3
141
+ end
142
+
143
+ def self.max_param_count
144
+ 3
145
+ end
146
+
107
147
  def initialize(*args)
108
148
  super
109
149
  @original, @search, @replacement = *@args
@@ -137,6 +177,14 @@ module Dentaku
137
177
  end
138
178
 
139
179
  class Contains < Base
180
+ def self.min_param_count
181
+ 2
182
+ end
183
+
184
+ def self.max_param_count
185
+ 2
186
+ end
187
+
140
188
  def initialize(*args)
141
189
  super
142
190
  @needle, @haystack = *args
@@ -1,6 +1,8 @@
1
+ require_relative "./node"
2
+
1
3
  module Dentaku
2
4
  module AST
3
- class Grouping
5
+ class Grouping < Node
4
6
  def initialize(node)
5
7
  @node = node
6
8
  end
@@ -20,7 +20,9 @@ module Dentaku
20
20
 
21
21
  case v
22
22
  when Node
23
- v.value(context)
23
+ value = v.value(context)
24
+ context[identifier] = value if Dentaku.cache_identifier?
25
+ value
24
26
  when Proc
25
27
  v.call
26
28
  else
@@ -29,13 +31,13 @@ module Dentaku
29
31
  end
30
32
 
31
33
  def dependencies(context = {})
32
- context.key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
34
+ context.key?(identifier) ? dependencies_of(context[identifier], context) : [identifier]
33
35
  end
34
36
 
35
37
  private
36
38
 
37
- def dependencies_of(node)
38
- node.respond_to?(:dependencies) ? node.dependencies : []
39
+ def dependencies_of(node, context)
40
+ node.respond_to?(:dependencies) ? node.dependencies(context) : []
39
41
  end
40
42
  end
41
43
  end
@@ -53,40 +53,55 @@ module Dentaku
53
53
  end
54
54
 
55
55
  def expression_with_exception_handler(&block)
56
- ->(expr, ex) { block.call(ex) }
56
+ ->(_expr, ex) { block.call(ex) }
57
57
  end
58
58
 
59
59
  def load_results(&block)
60
- variables_in_resolve_order.each_with_object({}) do |var_name, r|
61
- begin
62
- solved = calculator.memory
63
- value_from_memory = solved[var_name.downcase]
64
-
65
- if value_from_memory.nil? &&
66
- expressions[var_name].nil? &&
67
- !solved.has_key?(var_name)
68
- next
69
- end
70
-
71
- value = value_from_memory || evaluate!(
60
+ facts, _formulas = expressions.transform_keys(&:downcase)
61
+ .transform_values { |v| calculator.ast(v) }
62
+ .partition { |_, v| calculator.dependencies(v, nil).empty? }
63
+
64
+ evaluated_facts = facts.to_h.each_with_object({}) do |(var_name, ast), h|
65
+ with_rescues(var_name, h, block) do
66
+ h[var_name] = ast.is_a?(Array) ? ast.map(&:value) : ast.value
67
+ end
68
+ end
69
+
70
+ context = calculator.memory.merge(evaluated_facts)
71
+
72
+ variables_in_resolve_order.each_with_object({}) do |var_name, results|
73
+ next if expressions[var_name].nil?
74
+
75
+ with_rescues(var_name, results, block) do
76
+ results[var_name] = evaluated_facts[var_name] || calculator.evaluate!(
72
77
  expressions[var_name],
73
- expressions.merge(r).merge(solved),
78
+ context.merge(results),
74
79
  &expression_with_exception_handler(&block)
75
80
  )
76
-
77
- r[var_name] = value
78
- rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
79
- ex.recipient_variable = var_name
80
- r[var_name] = block.call(ex)
81
- rescue Dentaku::ArgumentError => ex
82
- r[var_name] = block.call(ex)
83
81
  end
84
82
  end
83
+
85
84
  rescue TSort::Cyclic => ex
86
85
  block.call(ex)
87
86
  {}
88
87
  end
89
88
 
89
+ def with_rescues(var_name, results, block)
90
+ yield
91
+
92
+ rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
93
+ ex.recipient_variable = var_name
94
+ results[var_name] = block.call(ex)
95
+
96
+ rescue Dentaku::ArgumentError => ex
97
+ results[var_name] = block.call(ex)
98
+
99
+ ensure
100
+ if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
101
+ results[var_name] = calculator.memory[var_name.downcase]
102
+ end
103
+ end
104
+
90
105
  def expressions
91
106
  @expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
92
107
  end
@@ -113,9 +128,5 @@ module Dentaku
113
128
  end
114
129
  }
115
130
  end
116
-
117
- def evaluate!(expression, results, &block)
118
- calculator.evaluate!(expression, results, &block)
119
- end
120
131
  end
121
132
  end