dentaku 3.5.2 → 3.5.4

Sign up to get free protection for your applications and to get access to all the features.
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)