dentaku 3.3.1 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -8
  3. data/.travis.yml +3 -4
  4. data/CHANGELOG.md +37 -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 +28 -5
  11. data/lib/dentaku/ast/array.rb +15 -1
  12. data/lib/dentaku/ast/case.rb +8 -0
  13. data/lib/dentaku/ast/case/case_conditional.rb +8 -0
  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 +10 -1
  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 +36 -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 +7 -0
  45. data/lib/dentaku/parser.rb +14 -3
  46. data/lib/dentaku/tokenizer.rb +1 -1
  47. data/lib/dentaku/version.rb +1 -1
  48. data/spec/ast/addition_spec.rb +6 -0
  49. data/spec/ast/arithmetic_spec.rb +41 -13
  50. data/spec/ast/avg_spec.rb +4 -0
  51. data/spec/ast/division_spec.rb +6 -0
  52. data/spec/ast/function_spec.rb +1 -1
  53. data/spec/ast/mul_spec.rb +4 -0
  54. data/spec/ast/negation_spec.rb +48 -0
  55. data/spec/ast/node_spec.rb +4 -1
  56. data/spec/ast/round_spec.rb +10 -0
  57. data/spec/ast/rounddown_spec.rb +10 -0
  58. data/spec/ast/roundup_spec.rb +10 -0
  59. data/spec/ast/string_functions_spec.rb +35 -0
  60. data/spec/ast/sum_spec.rb +4 -0
  61. data/spec/bulk_expression_solver_spec.rb +27 -0
  62. data/spec/calculator_spec.rb +144 -3
  63. data/spec/dentaku_spec.rb +18 -5
  64. data/spec/external_function_spec.rb +29 -5
  65. data/spec/parser_spec.rb +13 -0
  66. data/spec/tokenizer_spec.rb +24 -5
  67. metadata +11 -4
@@ -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,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
@@ -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
@@ -0,0 +1,45 @@
1
+ module Dentaku
2
+ class DateArithmetic
3
+ def initialize(date)
4
+ @base = date
5
+ end
6
+
7
+ def add(duration)
8
+ case duration
9
+ when Numeric
10
+ @base + duration
11
+ when Dentaku::AST::Duration::Value
12
+ case duration.unit
13
+ when :year
14
+ Time.local(@base.year + duration.value, @base.month, @base.day).to_datetime
15
+ when :month
16
+ @base >> duration.value
17
+ when :day
18
+ @base + duration.value
19
+ end
20
+ else
21
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
22
+ "'#{duration || duration.class}' is not coercible for date arithmetic"
23
+ end
24
+ end
25
+
26
+ def sub(duration)
27
+ case duration
28
+ when DateTime, Numeric
29
+ @base - duration
30
+ when Dentaku::AST::Duration::Value
31
+ case duration.unit
32
+ when :year
33
+ Time.local(@base.year - duration.value, @base.month, @base.day).to_datetime
34
+ when :month
35
+ @base << duration.value
36
+ when :day
37
+ @base - duration.value
38
+ end
39
+ else
40
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
41
+ "'#{duration || duration.class}' is not coercible for date arithmetic"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -33,7 +33,7 @@ module Dentaku
33
33
  private_class_method :new
34
34
 
35
35
  VALID_REASONS = %i[
36
- node_invalid too_few_operands undefined_function
36
+ node_invalid too_few_operands too_many_operands undefined_function
37
37
  unprocessed_token unknown_case_token unbalanced_bracket
38
38
  unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
39
39
  invalid_statement
@@ -44,7 +44,7 @@ module Dentaku
44
44
  raise ::ArgumentError, "Unhandled #{reason}"
45
45
  end
46
46
 
47
- new reason, meta
47
+ new(reason, **meta)
48
48
  end
49
49
  end
50
50
 
@@ -68,7 +68,7 @@ module Dentaku
68
68
  raise ::ArgumentError, "Unhandled #{reason}"
69
69
  end
70
70
 
71
- new reason, meta
71
+ new(reason, **meta)
72
72
  end
73
73
  end
74
74
 
@@ -92,7 +92,7 @@ module Dentaku
92
92
  raise ::ArgumentError, "Unhandled #{reason}"
93
93
  end
94
94
 
95
- new reason, meta
95
+ new(reason, **meta)
96
96
  end
97
97
  end
98
98