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,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,52 @@
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
+ def type
44
+ nil
45
+ end
46
+ end
47
+ end
48
+ end
49
+
4
50
  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
- })
51
+ Dentaku::AST::Function.register_class(method, Dentaku::AST::RubyMath[method])
8
52
  end
@@ -17,6 +17,14 @@ module Dentaku
17
17
  end
18
18
 
19
19
  class Left < Base
20
+ def self.min_param_count
21
+ 2
22
+ end
23
+
24
+ def self.max_param_count
25
+ 2
26
+ end
27
+
20
28
  def initialize(*args)
21
29
  super
22
30
  @string, @length = *@args
@@ -24,13 +32,21 @@ module Dentaku
24
32
 
25
33
  def value(context = {})
26
34
  string = @string.value(context).to_s
27
- length = @length.value(context)
35
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
28
36
  negative_argument_failure('LEFT') if length < 0
29
37
  string[0, length]
30
38
  end
31
39
  end
32
40
 
33
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
+
34
50
  def initialize(*args)
35
51
  super
36
52
  @string, @length = *@args
@@ -38,13 +54,21 @@ module Dentaku
38
54
 
39
55
  def value(context = {})
40
56
  string = @string.value(context).to_s
41
- length = @length.value(context)
57
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
42
58
  negative_argument_failure('RIGHT') if length < 0
43
59
  string[length * -1, length] || string
44
60
  end
45
61
  end
46
62
 
47
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
+
48
72
  def initialize(*args)
49
73
  super
50
74
  @string, @offset, @length = *@args
@@ -52,15 +76,23 @@ module Dentaku
52
76
 
53
77
  def value(context = {})
54
78
  string = @string.value(context).to_s
55
- offset = @offset.value(context)
79
+ offset = Dentaku::AST::Function.numeric(@offset.value(context)).to_i
56
80
  negative_argument_failure('MID', 'offset') if offset < 0
57
- length = @length.value(context)
81
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
58
82
  negative_argument_failure('MID') if length < 0
59
83
  string[offset - 1, length].to_s
60
84
  end
61
85
  end
62
86
 
63
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
+
64
96
  def initialize(*args)
65
97
  super
66
98
  @string = @args[0]
@@ -77,6 +109,14 @@ module Dentaku
77
109
  end
78
110
 
79
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
+
80
120
  def initialize(*args)
81
121
  super
82
122
  @needle, @haystack = *@args
@@ -96,6 +136,14 @@ module Dentaku
96
136
  end
97
137
 
98
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
+
99
147
  def initialize(*args)
100
148
  super
101
149
  @original, @search, @replacement = *@args
@@ -111,6 +159,14 @@ module Dentaku
111
159
  end
112
160
 
113
161
  class Concat < Base
162
+ def self.min_param_count
163
+ 1
164
+ end
165
+
166
+ def self.max_param_count
167
+ Float::INFINITY
168
+ end
169
+
114
170
  def initialize(*args)
115
171
  super
116
172
  end
@@ -121,6 +177,14 @@ module Dentaku
121
177
  end
122
178
 
123
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
+
124
188
  def initialize(*args)
125
189
  super
126
190
  @needle, @haystack = *args
@@ -1,12 +1,13 @@
1
1
  require_relative '../function'
2
2
 
3
3
  Dentaku::AST::Function.register(:sum, :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: 'SUM()', at_least: 1, given: 0
8
9
  ), 'SUM() requires at least one argument'
9
10
  end
10
11
 
11
- args.flatten.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+)
12
+ flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+)
12
13
  })
@@ -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,11 @@ 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
26
+ when Proc
27
+ v.call
24
28
  else
25
29
  v
26
30
  end
@@ -1,11 +1,13 @@
1
1
  module Dentaku
2
2
  module AST
3
3
  class Negation < Arithmetic
4
+ attr_reader :node
5
+
4
6
  def initialize(node)
5
7
  @node = node
6
8
 
7
9
  unless valid_node?(node)
8
- raise NodeError.new(:numeric, left.type, :left),
10
+ raise NodeError.new(:numeric, node.type, :node),
9
11
  "#{self.class} requires numeric operands"
10
12
  end
11
13
  end
@@ -15,6 +15,10 @@ module Dentaku
15
15
  def dependencies(context = {})
16
16
  []
17
17
  end
18
+
19
+ def type
20
+ nil
21
+ end
18
22
  end
19
23
  end
20
24
  end
@@ -5,6 +5,14 @@ module Dentaku
5
5
  class Operation < Node
6
6
  attr_reader :left, :right
7
7
 
8
+ def self.min_param_count
9
+ arity
10
+ end
11
+
12
+ def self.max_param_count
13
+ arity
14
+ end
15
+
8
16
  def initialize(left, right)
9
17
  @left = left
10
18
  @right = right
@@ -53,40 +53,53 @@ 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
+ context = calculator.memory.merge(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
+ variables_in_resolve_order.each_with_object({}) do |var_name, results|
71
+ next if expressions[var_name].nil?
72
+
73
+ with_rescues(var_name, results, block) do
74
+ results[var_name] = calculator.evaluate!(
72
75
  expressions[var_name],
73
- expressions.merge(r).merge(solved),
76
+ context.merge(results),
74
77
  &expression_with_exception_handler(&block)
75
78
  )
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
79
  end
84
80
  end
81
+
85
82
  rescue TSort::Cyclic => ex
86
83
  block.call(ex)
87
84
  {}
88
85
  end
89
86
 
87
+ def with_rescues(var_name, results, block)
88
+ yield
89
+
90
+ rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
91
+ ex.recipient_variable = var_name
92
+ results[var_name] = block.call(ex)
93
+
94
+ rescue Dentaku::ArgumentError => ex
95
+ results[var_name] = block.call(ex)
96
+
97
+ ensure
98
+ if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
99
+ results[var_name] = calculator.memory[var_name.downcase]
100
+ end
101
+ end
102
+
90
103
  def expressions
91
104
  @expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
92
105
  end
@@ -113,9 +126,5 @@ module Dentaku
113
126
  end
114
127
  }
115
128
  end
116
-
117
- def evaluate!(expression, results, &block)
118
- calculator.evaluate!(expression, results, &block)
119
- end
120
129
  end
121
130
  end
@@ -9,7 +9,8 @@ require 'dentaku/token'
9
9
  module Dentaku
10
10
  class Calculator
11
11
  include StringCasing
12
- attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases, :nested_data_support
12
+ attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
13
+ :nested_data_support, :ast_cache
13
14
 
14
15
  def initialize(options = {})
15
16
  clear
@@ -61,7 +62,7 @@ module Dentaku
61
62
  unbound = node.dependencies - memory.keys
62
63
  unless unbound.empty?
63
64
  raise UnboundVariableError.new(unbound),
64
- "no value provided for variables: #{unbound.join(', ')}"
65
+ "no value provided for variables: #{unbound.uniq.join(', ')}"
65
66
  end
66
67
  node.value(memory)
67
68
  end
@@ -76,13 +77,21 @@ module Dentaku
76
77
  end
77
78
 
78
79
  def dependencies(expression, context = {})
79
- if expression.is_a? Array
80
- return expression.flat_map { |e| dependencies(e, context) }
80
+ test_context = context.nil? ? {} : store(context) { memory }
81
+
82
+ case expression
83
+ when Dentaku::AST::Node
84
+ expression.dependencies(test_context)
85
+ when Array
86
+ expression.flat_map { |e| dependencies(e, context) }
87
+ else
88
+ ast(expression).dependencies(test_context)
81
89
  end
82
- store(context) { ast(expression).dependencies(memory) }
83
90
  end
84
91
 
85
92
  def ast(expression)
93
+ return expression.map { |e| ast(e) } if expression.is_a? Array
94
+
86
95
  @ast_cache.fetch(expression) {
87
96
  options = {
88
97
  case_sensitive: case_sensitive,
@@ -97,6 +106,10 @@ module Dentaku
97
106
  }
98
107
  end
99
108
 
109
+ def load_cache(ast_cache)
110
+ @ast_cache = ast_cache
111
+ end
112
+
100
113
  def clear_cache(pattern = :all)
101
114
  case pattern
102
115
  when :all
@@ -114,7 +127,7 @@ module Dentaku
114
127
  restore = Hash[memory]
115
128
 
116
129
  if value.nil?
117
- key_or_hash = FlatHash.from_hash(key_or_hash) if nested_data_support
130
+ key_or_hash = FlatHash.from_hash_with_intermediates(key_or_hash) if nested_data_support
118
131
  key_or_hash.each do |key, val|
119
132
  memory[standardize_case(key.to_s)] = val
120
133
  end