dentaku 3.5.2 → 3.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/README.md +1 -1
  4. data/dentaku.gemspec +1 -0
  5. data/lib/dentaku/ast/access.rb +0 -3
  6. data/lib/dentaku/ast/arithmetic.rb +25 -35
  7. data/lib/dentaku/ast/array.rb +1 -4
  8. data/lib/dentaku/ast/functions/all.rb +1 -5
  9. data/lib/dentaku/ast/functions/any.rb +1 -5
  10. data/lib/dentaku/ast/functions/enum.rb +13 -0
  11. data/lib/dentaku/ast/functions/if.rb +3 -7
  12. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  13. data/lib/dentaku/ast/functions/map.rb +1 -5
  14. data/lib/dentaku/ast/functions/pluck.rb +6 -2
  15. data/lib/dentaku/ast/functions/reduce.rb +61 -0
  16. data/lib/dentaku/ast/node.rb +2 -1
  17. data/lib/dentaku/ast.rb +3 -1
  18. data/lib/dentaku/bulk_expression_solver.rb +37 -7
  19. data/lib/dentaku/calculator.rb +23 -7
  20. data/lib/dentaku/date_arithmetic.rb +26 -15
  21. data/lib/dentaku/dependency_resolver.rb +9 -4
  22. data/lib/dentaku/exceptions.rb +8 -3
  23. data/lib/dentaku/parser.rb +8 -5
  24. data/lib/dentaku/token.rb +12 -0
  25. data/lib/dentaku/token_scanner.rb +7 -2
  26. data/lib/dentaku/version.rb +1 -1
  27. data/spec/ast/addition_spec.rb +4 -4
  28. data/spec/ast/all_spec.rb +13 -0
  29. data/spec/ast/any_spec.rb +13 -0
  30. data/spec/ast/arithmetic_spec.rb +43 -2
  31. data/spec/ast/division_spec.rb +25 -0
  32. data/spec/ast/intercept_spec.rb +30 -0
  33. data/spec/ast/map_spec.rb +13 -0
  34. data/spec/ast/pluck_spec.rb +17 -0
  35. data/spec/ast/reduce_spec.rb +22 -0
  36. data/spec/bulk_expression_solver_spec.rb +33 -0
  37. data/spec/calculator_spec.rb +66 -7
  38. data/spec/dependency_resolver_spec.rb +18 -0
  39. data/spec/parser_spec.rb +3 -0
  40. data/spec/tokenizer_spec.rb +6 -4
  41. data/spec/visitor_spec.rb +2 -1
  42. metadata +25 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0b10f7d9e6a9d200c283dcf077fd56e8f4abe4922c02e3095ba20dbb29f6b81c
4
- data.tar.gz: add2d3bf7c462edefb9a4c52d79595d3a161e1d78046dbb1fe90e8aa9979a13b
3
+ metadata.gz: ef9e8cb8d852db9a8e4c81c28815c6a7e7008c6e9bebcc8a11222768756fd7a8
4
+ data.tar.gz: f7d1c005ef1ba8bcfc77fcac8b885378a0e3e36fdba8f97a2c602b6b0ba6f34c
5
5
  SHA512:
6
- metadata.gz: 48c2571ea61f8bb9f8a7a4483b8d588741f2c44c4fdc2d04ecd2a5c5b75a94f414b5772a3e1dca21898f8d2ed6488e54925c6a689bfd721315c0c8e0992991d7
7
- data.tar.gz: b469e9c4a69c6083b93cda29ea8bf5bf4ad9c91e4a6362e5880768492e21ecd476a09eb858134b8e6c78017207c6cf50479c7ebb3af661c7e20f3866f034b49a
6
+ metadata.gz: 6d616fd597440c13e8432ac6f9d5480592cdcbb7e9aec30c1f2c21e94e0a4bcf3ffced31cf486dc7390bd8668bb5e377745a7eebf467edfab1cd5fa0ae4f70e9
7
+ data.tar.gz: fca27bf921a54469f065f634a44d4a6481c6420ccbcaa39400ffe4a5cdd172ff6ae209a04aee96208a64979bbff814635719732b557df33f7d406f87123b1c3a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Change Log
2
2
 
3
+ ## [v3.5.4]
4
+ - add support for default value for PLUCK function
5
+ - improve error handling for MAP/ANY/ALL functions
6
+ - fix modulo / percentage operator determination
7
+ - fix string casing bug with bulk expressions
8
+ - add explicit gem dependency for BigDecimal
9
+
10
+ ## [v3.5.3]
11
+ - add support for empty array literals
12
+ - add support for quoted identifiers
13
+ - add REDUCE function
14
+ - add INTERCEPT function
15
+ - improve date/time parsing an arithmetic
16
+ - improve custom class arithmetic
17
+ - fix IF dependency
18
+
3
19
  ## [v3.5.2]
4
20
  - add ABS function
5
21
  - add array support for AST visitors
@@ -244,6 +260,8 @@
244
260
  ## [v0.1.0] 2012-01-20
245
261
  - initial release
246
262
 
263
+ [v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
264
+ [v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
247
265
  [v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
248
266
  [v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
249
267
  [v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
data/README.md CHANGED
@@ -145,7 +145,7 @@ Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
145
145
 
146
146
  Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`
147
147
 
148
- Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`
148
+ Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`
149
149
 
150
150
  Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L593))
151
151
 
data/dentaku.gemspec CHANGED
@@ -14,6 +14,7 @@ Gem::Specification.new do |s|
14
14
  Dentaku is a parser and evaluator for mathematical formulas
15
15
  DESC
16
16
 
17
+ s.add_dependency('bigdecimal')
17
18
  s.add_dependency('concurrent-ruby')
18
19
 
19
20
  s.add_development_dependency('codecov')
@@ -17,9 +17,6 @@ module Dentaku
17
17
  arity
18
18
  end
19
19
 
20
- def self.peek(*)
21
- end
22
-
23
20
  def initialize(data_structure, index)
24
21
  @structure = data_structure
25
22
  @index = index
@@ -62,6 +62,8 @@ module Dentaku
62
62
 
63
63
  def decimal(val)
64
64
  BigDecimal(val.to_s, Float::DIG + 1)
65
+ rescue # return as is, in case value can't be coerced to big decimal
66
+ val
65
67
  end
66
68
 
67
69
  def datetime?(val)
@@ -177,50 +179,42 @@ module Dentaku
177
179
 
178
180
  class Modulo < Arithmetic
179
181
  def self.arity
180
- @arity
182
+ 2
181
183
  end
182
184
 
183
- def self.peek(input)
184
- @arity = 1
185
- @arity = 2 if input.length > 1
185
+ def self.precedence
186
+ 20
186
187
  end
187
188
 
188
- def initialize(left, right = nil)
189
- if right
190
- @left = left
191
- @right = right
192
- else
193
- @right = left
194
- end
189
+ def self.resolve_class(next_token)
190
+ next_token.nil? || next_token.operator? || next_token.close? ? Percentage : self
191
+ end
192
+
193
+ def operator
194
+ :%
195
+ end
196
+ end
197
+
198
+ class Percentage < Arithmetic
199
+ def self.arity
200
+ 1
201
+ end
202
+
203
+ def initialize(child)
204
+ @right = child
195
205
 
196
- unless valid_left?
197
- raise NodeError.new(%i[numeric nil], left.type, :left),
198
- "#{self.class} requires numeric operands or nil"
199
- end
200
206
  unless valid_right?
201
207
  raise NodeError.new(:numeric, right.type, :right),
202
- "#{self.class} requires numeric operands"
208
+ "#{self.class} requires a numeric operand"
203
209
  end
204
210
  end
205
211
 
206
212
  def dependencies(context = {})
207
- if percent?
208
- @right.dependencies(context)
209
- else
210
- super
211
- end
212
- end
213
-
214
- def percent?
215
- left.nil?
213
+ @right.dependencies(context)
216
214
  end
217
215
 
218
216
  def value(context = {})
219
- if percent?
220
- cast(right.value(context)) * 0.01
221
- else
222
- super
223
- end
217
+ cast(right.value(context)) * 0.01
224
218
  end
225
219
 
226
220
  def operator
@@ -228,11 +222,7 @@ module Dentaku
228
222
  end
229
223
 
230
224
  def self.precedence
231
- 20
232
- end
233
-
234
- def valid_left?
235
- valid_node?(left) || left.nil?
225
+ 30
236
226
  end
237
227
  end
238
228
 
@@ -14,9 +14,6 @@ module Dentaku
14
14
  Float::INFINITY
15
15
  end
16
16
 
17
- def self.peek(*)
18
- end
19
-
20
17
  def initialize(*elements)
21
18
  @elements = *elements
22
19
  end
@@ -32,7 +29,7 @@ module Dentaku
32
29
  def type
33
30
  nil
34
31
  end
35
-
32
+
36
33
  def accept(visitor)
37
34
  visitor.visit_array(self)
38
35
  end
@@ -9,11 +9,7 @@ module Dentaku
9
9
  expression = @args[2]
10
10
 
11
11
  collection.all? do |item_value|
12
- expression.value(
13
- context.merge(
14
- FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
- )
16
- )
12
+ mapped_value(expression, context, item_identifier => item_value)
17
13
  end
18
14
  end
19
15
  end
@@ -9,11 +9,7 @@ module Dentaku
9
9
  expression = @args[2]
10
10
 
11
11
  collection.any? do |item_value|
12
- expression.value(
13
- context.merge(
14
- FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
- )
16
- )
12
+ mapped_value(expression, context, item_identifier => item_value)
17
13
  end
18
14
  end
19
15
  end
@@ -33,6 +33,19 @@ module Dentaku
33
33
  def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
34
34
  raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
35
35
  end
36
+
37
+ private
38
+
39
+ def mapped_value(expression, context, item_context)
40
+ expression.value(
41
+ context.merge(
42
+ FlatHash.from_hash_with_intermediates(item_context)
43
+ )
44
+ )
45
+ rescue => e
46
+ raise e if context["__evaluation_mode"] == :strict
47
+ nil
48
+ end
36
49
  end
37
50
  end
38
51
  end
@@ -36,13 +36,9 @@ module Dentaku
36
36
  end
37
37
 
38
38
  def dependencies(context = {})
39
- deps = predicate.dependencies(context)
40
-
41
- if deps.empty?
42
- predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
43
- else
44
- (deps + left.dependencies(context) + right.dependencies(context)).uniq
45
- end
39
+ predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
40
+ rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError
41
+ args.flat_map { |arg| arg.dependencies(context) }.uniq
46
42
  end
47
43
  end
48
44
  end
@@ -0,0 +1,33 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
4
+ if args.length != 2
5
+ raise Dentaku::ArgumentError.for(
6
+ :wrong_number_of_arguments,
7
+ function_name: 'INTERCEPT()', exact: 2, given: args.length
8
+ ), 'INTERCEPT() requires exactly two arrays of numbers'
9
+ end
10
+
11
+ x_values, y_values = args
12
+ if !x_values.is_a?(Array) || !y_values.is_a?(Array) || x_values.length != y_values.length
13
+ raise Dentaku::ArgumentError.for(
14
+ :invalid_value,
15
+ function_name: 'INTERCEPT()'
16
+ ), 'INTERCEPT() requires arrays of equal length'
17
+ end
18
+
19
+ n = x_values.length.to_f
20
+ x_values = x_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
21
+ y_values = y_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
22
+
23
+ x_avg = x_values.sum / n
24
+ y_avg = y_values.sum / n
25
+
26
+ xy_sum = x_values.zip(y_values).map { |x, y| (x_avg - x) * (y_avg - y) }.sum
27
+ x_square_sum = x_values.map { |x| (x_avg - x)**2 }.sum
28
+
29
+ slope = xy_sum / x_square_sum
30
+ intercept = x_values.zip(y_values).map { |x, y| y - slope * x }.sum / n
31
+
32
+ BigDecimal(intercept, Float::DIG + 1)
33
+ })
@@ -9,11 +9,7 @@ module Dentaku
9
9
  expression = @args[2]
10
10
 
11
11
  collection.map do |item_value|
12
- expression.value(
13
- context.merge(
14
- FlatHash.from_hash_with_intermediates(item_identifier => item_value)
15
- )
16
- )
12
+ mapped_value(expression, context, item_identifier => item_value)
17
13
  end
18
14
  end
19
15
  end
@@ -9,19 +9,23 @@ module Dentaku
9
9
  end
10
10
 
11
11
  def self.max_param_count
12
- 2
12
+ 3
13
13
  end
14
14
 
15
15
  def value(context = {})
16
16
  collection = Array(@args[0].value(context))
17
+
17
18
  unless collection.all? { |elem| elem.is_a?(Hash) }
18
19
  raise ArgumentError.for(:incompatible_type, value: collection),
19
20
  'PLUCK() requires first argument to be an array of hashes'
20
21
  end
21
22
 
22
23
  pluck_path = @args[1].identifier
24
+ default = @args[2]
23
25
 
24
- collection.map { |h| h.transform_keys(&:to_s)[pluck_path] }
26
+ collection.map { |h|
27
+ h.transform_keys(&:to_s).fetch(pluck_path, default&.value(context))
28
+ }
25
29
  end
26
30
  end
27
31
  end
@@ -0,0 +1,61 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Reduce < Function
7
+ def self.min_param_count
8
+ 4
9
+ end
10
+
11
+ def self.max_param_count
12
+ 5
13
+ end
14
+
15
+ def initialize(*args)
16
+ super
17
+
18
+ validate_identifier(@args[1], 'second')
19
+ validate_identifier(@args[2], 'third')
20
+ end
21
+
22
+ def dependencies(context = {})
23
+ collection = @args[0]
24
+ memo_identifier = @args[1].identifier
25
+ item_identifier = @args[2].identifier
26
+ expression = @args[3]
27
+
28
+ collection_deps = collection.dependencies(context)
29
+ expression_deps = expression.dependencies(context).reject do |i|
30
+ i == memo_identifier || i.start_with?("#{memo_identifier}.") ||
31
+ i == item_identifier || i.start_with?("#{item_identifier}.")
32
+ end
33
+ inital_value_deps = @args[4] ? @args[4].dependencies(context) : []
34
+
35
+ collection_deps + expression_deps + inital_value_deps
36
+ end
37
+
38
+ def value(context = {})
39
+ collection = Array(@args[0].value(context))
40
+ memo_identifier = @args[1].identifier
41
+ item_identifier = @args[2].identifier
42
+ expression = @args[3]
43
+ initial_value = @args[4] && @args[4].value(context)
44
+
45
+ collection.reduce(initial_value) do |memo, item|
46
+ expression.value(
47
+ context.merge(
48
+ FlatHash.from_hash_with_intermediates(memo_identifier => memo, item_identifier => item)
49
+ )
50
+ )
51
+ end
52
+ end
53
+
54
+ def validate_identifier(arg, position, message = "#{name}() requires #{position} argument to be an identifier")
55
+ raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ Dentaku::AST::Function.register_class(:reduce, Dentaku::AST::Reduce)
@@ -9,7 +9,8 @@ module Dentaku
9
9
  nil
10
10
  end
11
11
 
12
- def self.peek(*)
12
+ def self.resolve_class(*)
13
+ self
13
14
  end
14
15
 
15
16
  def dependencies(context = {})
data/lib/dentaku/ast.rb CHANGED
@@ -24,12 +24,14 @@ require_relative './ast/functions/count'
24
24
  require_relative './ast/functions/duration'
25
25
  require_relative './ast/functions/filter'
26
26
  require_relative './ast/functions/if'
27
+ require_relative './ast/functions/intercept'
27
28
  require_relative './ast/functions/map'
28
29
  require_relative './ast/functions/max'
29
30
  require_relative './ast/functions/min'
30
31
  require_relative './ast/functions/not'
31
32
  require_relative './ast/functions/or'
32
33
  require_relative './ast/functions/pluck'
34
+ require_relative './ast/functions/reduce'
33
35
  require_relative './ast/functions/round'
34
36
  require_relative './ast/functions/rounddown'
35
37
  require_relative './ast/functions/roundup'
@@ -37,4 +39,4 @@ require_relative './ast/functions/ruby_math'
37
39
  require_relative './ast/functions/string_functions'
38
40
  require_relative './ast/functions/sum'
39
41
  require_relative './ast/functions/switch'
40
- require_relative './ast/functions/xor'
42
+ require_relative './ast/functions/xor'
@@ -6,16 +6,41 @@ require 'dentaku/tokenizer'
6
6
 
7
7
  module Dentaku
8
8
  class BulkExpressionSolver
9
+ class StrictEvaluator
10
+ def initialize(calculator)
11
+ @calculator = calculator
12
+ end
13
+
14
+ def evaluate(*args)
15
+ @calculator.evaluate!(*args)
16
+ end
17
+ end
18
+
19
+ class PermissiveEvaluator
20
+ def initialize(calculator, block)
21
+ @calculator = calculator
22
+ @block = block || ->(*) { :undefined }
23
+ end
24
+
25
+ def evaluate(*args)
26
+ @calculator.evaluate(*args) { |expr, ex|
27
+ @block.call(ex)
28
+ }
29
+ end
30
+ end
31
+
9
32
  def initialize(expressions, calculator)
10
33
  @expression_hash = FlatHash.from_hash(expressions)
11
34
  @calculator = calculator
12
35
  end
13
36
 
14
37
  def solve!
38
+ @evaluator = StrictEvaluator.new(calculator)
15
39
  solve(&raise_exception_handler)
16
40
  end
17
41
 
18
42
  def solve(&block)
43
+ @evaluator ||= PermissiveEvaluator.new(calculator, block)
19
44
  error_handler = block || return_undefined_handler
20
45
  results = load_results(&error_handler)
21
46
 
@@ -42,7 +67,7 @@ module Dentaku
42
67
  @dep_cache ||= {}
43
68
  end
44
69
 
45
- attr_reader :expression_hash, :calculator
70
+ attr_reader :expression_hash, :calculator, :evaluator
46
71
 
47
72
  def return_undefined_handler
48
73
  ->(*) { :undefined }
@@ -52,8 +77,11 @@ module Dentaku
52
77
  ->(ex) { raise ex }
53
78
  end
54
79
 
55
- def expression_with_exception_handler(&block)
56
- ->(_expr, ex) { block.call(ex) }
80
+ def expression_with_exception_handler(var_name, &block)
81
+ ->(_expr, ex) {
82
+ ex.recipient_variable = var_name
83
+ block.call(ex)
84
+ }
57
85
  end
58
86
 
59
87
  def load_results(&block)
@@ -73,11 +101,14 @@ module Dentaku
73
101
  next if expressions[var_name].nil?
74
102
 
75
103
  with_rescues(var_name, results, block) do
76
- results[var_name] = evaluated_facts[var_name] || calculator.evaluate!(
104
+ results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
77
105
  expressions[var_name],
78
106
  context.merge(results),
79
- &expression_with_exception_handler(&block)
80
- )
107
+ &expression_with_exception_handler(var_name, &block)
108
+ ).tap { |res|
109
+ res.recipient_variable = var_name if res.respond_to?(:recipient_variable=)
110
+ res
111
+ }
81
112
  end
82
113
  end
83
114
 
@@ -88,7 +119,6 @@ module Dentaku
88
119
 
89
120
  def with_rescues(var_name, results, block)
90
121
  yield
91
-
92
122
  rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
93
123
  ex.recipient_variable = var_name
94
124
  results[var_name] = block.call(ex)
@@ -52,28 +52,39 @@ module Dentaku
52
52
  end
53
53
 
54
54
  def evaluate(expression, data = {}, &block)
55
- evaluate!(expression, data)
55
+ context = evaluation_context(data, :permissive)
56
+ return evaluate_array(expression, context, &block) if expression.is_a?(Array)
57
+
58
+ evaluate!(expression, context)
56
59
  rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
57
60
  block.call(expression, ex) if block_given?
58
61
  end
59
62
 
63
+ private def evaluate_array(expression, data = {}, &block)
64
+ expression.map { |e| evaluate(e, data, &block) }
65
+ end
66
+
60
67
  def evaluate!(expression, data = {}, &block)
61
- return expression.map { |e|
62
- evaluate(e, data, &block)
63
- } if expression.is_a? Array
68
+ context = evaluation_context(data, :strict)
69
+ return evaluate_array!(expression, context, &block) if expression.is_a? Array
64
70
 
65
- store(data) do
66
- node = expression
67
- node = ast(node) unless node.is_a?(AST::Node)
71
+ store(context) do
72
+ node = ast(expression)
68
73
  unbound = node.dependencies(memory)
74
+
69
75
  unless unbound.empty?
70
76
  raise UnboundVariableError.new(unbound),
71
77
  "no value provided for variables: #{unbound.uniq.join(', ')}"
72
78
  end
79
+
73
80
  node.value(memory)
74
81
  end
75
82
  end
76
83
 
84
+ private def evaluate_array!(expression, data = {}, &block)
85
+ expression.map { |e| evaluate!(e, data, &block) }
86
+ end
87
+
77
88
  def solve!(expression_hash)
78
89
  BulkExpressionSolver.new(expression_hash, self).solve!
79
90
  end
@@ -96,6 +107,7 @@ module Dentaku
96
107
  end
97
108
 
98
109
  def ast(expression)
110
+ return expression if expression.is_a?(AST::Node)
99
111
  return expression.map { |e| ast(e) } if expression.is_a? Array
100
112
 
101
113
  @ast_cache.fetch(expression) {
@@ -130,6 +142,10 @@ module Dentaku
130
142
  end
131
143
  end
132
144
 
145
+ def evaluation_context(data, evaluation_mode)
146
+ data.key?(:__evaluation_mode) ? data : data.merge(__evaluation_mode: evaluation_mode)
147
+ end
148
+
133
149
  def store(key_or_hash, value = nil)
134
150
  restore = Hash[memory]
135
151
 
@@ -13,13 +13,11 @@ module Dentaku
13
13
  when Numeric
14
14
  @base + duration
15
15
  when Dentaku::AST::Duration::Value
16
- case duration.unit
17
- when :year
18
- Time.local(@base.year + duration.value, @base.month, @base.day).to_datetime
19
- when :month
20
- @base >> duration.value
21
- when :day
22
- @base + duration.value
16
+ case @base
17
+ when Time
18
+ change_datetime(@base.to_datetime, duration.unit, duration.value).to_time
19
+ else
20
+ change_datetime(@base, duration.unit, duration.value)
23
21
  end
24
22
  else
25
23
  raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
@@ -29,21 +27,34 @@ module Dentaku
29
27
 
30
28
  def sub(duration)
31
29
  case duration
32
- when DateTime, Numeric
30
+ when Date, DateTime, Numeric, Time
33
31
  @base - duration
34
32
  when Dentaku::AST::Duration::Value
35
- case duration.unit
36
- when :year
37
- Time.local(@base.year - duration.value, @base.month, @base.day).to_datetime
38
- when :month
39
- @base << duration.value
40
- when :day
41
- @base - duration.value
33
+ case @base
34
+ when Time
35
+ change_datetime(@base.to_datetime, duration.unit, -duration.value).to_time
36
+ else
37
+ change_datetime(@base, duration.unit, -duration.value)
42
38
  end
39
+ when Dentaku::TokenScanner::DATE_TIME_REGEXP
40
+ @base - Time.parse(duration).to_datetime
43
41
  else
44
42
  raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
45
43
  "'#{duration || duration.class}' is not coercible for date arithmetic"
46
44
  end
47
45
  end
46
+
47
+ private
48
+
49
+ def change_datetime(base, unit, value)
50
+ case unit
51
+ when :year
52
+ base >> (value * 12)
53
+ when :month
54
+ base >> value
55
+ when :day
56
+ base + value
57
+ end
58
+ end
48
59
  end
49
60
  end
@@ -4,13 +4,18 @@ module Dentaku
4
4
  class DependencyResolver
5
5
  include TSort
6
6
 
7
- def self.find_resolve_order(vars_to_dependencies_hash)
8
- self.new(vars_to_dependencies_hash).tsort
7
+ def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
8
+ self.new(vars_to_dependencies_hash).sort
9
9
  end
10
10
 
11
11
  def initialize(vars_to_dependencies_hash)
12
- # ensure variables are strings
13
- @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_s, v] }]
12
+ @key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [k.downcase, k] }]
13
+ # ensure variables are normalized strings
14
+ @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.downcase.to_s, v] }]
15
+ end
16
+
17
+ def sort
18
+ tsort.map { |k| @key_mapping.fetch(k, k) }
14
19
  end
15
20
 
16
21
  def tsort_each_node(&block)