dentaku 3.5.3 → 3.5.5

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +26 -0
  3. data/.github/workflows/rubocop.yml +14 -0
  4. data/CHANGELOG.md +22 -6
  5. data/README.md +1 -4
  6. data/dentaku.gemspec +1 -1
  7. data/lib/dentaku/ast/access.rb +0 -3
  8. data/lib/dentaku/ast/arithmetic.rb +36 -50
  9. data/lib/dentaku/ast/array.rb +1 -4
  10. data/lib/dentaku/ast/case.rb +12 -0
  11. data/lib/dentaku/ast/functions/all.rb +1 -5
  12. data/lib/dentaku/ast/functions/any.rb +1 -5
  13. data/lib/dentaku/ast/functions/enum.rb +13 -0
  14. data/lib/dentaku/ast/functions/map.rb +1 -5
  15. data/lib/dentaku/ast/functions/pluck.rb +6 -2
  16. data/lib/dentaku/ast/node.rb +2 -1
  17. data/lib/dentaku/ast/operation.rb +5 -0
  18. data/lib/dentaku/ast.rb +1 -1
  19. data/lib/dentaku/bulk_expression_solver.rb +37 -7
  20. data/lib/dentaku/calculator.rb +21 -5
  21. data/lib/dentaku/date_arithmetic.rb +24 -15
  22. data/lib/dentaku/dependency_resolver.rb +9 -4
  23. data/lib/dentaku/parser.rb +206 -213
  24. data/lib/dentaku/print_visitor.rb +2 -2
  25. data/lib/dentaku/token.rb +12 -0
  26. data/lib/dentaku/version.rb +1 -1
  27. data/lib/dentaku/visitor/infix.rb +1 -1
  28. data/spec/ast/addition_spec.rb +12 -7
  29. data/spec/ast/all_spec.rb +13 -0
  30. data/spec/ast/any_spec.rb +13 -0
  31. data/spec/ast/arithmetic_spec.rb +7 -0
  32. data/spec/ast/division_spec.rb +10 -6
  33. data/spec/ast/map_spec.rb +13 -0
  34. data/spec/ast/pluck_spec.rb +17 -0
  35. data/spec/bulk_expression_solver_spec.rb +24 -1
  36. data/spec/calculator_spec.rb +21 -3
  37. data/spec/dependency_resolver_spec.rb +18 -0
  38. data/spec/external_function_spec.rb +1 -1
  39. data/spec/parser_spec.rb +11 -2
  40. data/spec/print_visitor_spec.rb +5 -0
  41. data/spec/spec_helper.rb +1 -3
  42. data/spec/visitor_spec.rb +1 -1
  43. metadata +10 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a51418767c413ccded1f235a56d34866005edf21cbc0d4cf5848d4cff4bc801f
4
- data.tar.gz: 2009c09a76a5cc0b85cf04769c93f0d372cc8613f410a18401de53ca268b134d
3
+ metadata.gz: 42f22227c2d7aef33e71f5c177f65ca3e7855115466d4d04851e6286bcbe1955
4
+ data.tar.gz: e779f142e5f458aed8437539bfe98ee73b4bf86a0ddb280cceec586ebcd62190
5
5
  SHA512:
6
- metadata.gz: 6cfeabc676fa63016096d2f3c291d60a68ce247fe7c75ba195cdc2968c28af85de9ab9eeaaa9e88749cbf7920a120b1e6fe4d78464b98f9c825180d42aafc4f1
7
- data.tar.gz: 42863ad11afff2dc72dfe906d1fdc1c07821f4391dcb7fc67921d862c736350be15a6671beac9c16de9e1345f17869e0de40bb131bc1fcfea70190538274bd53
6
+ metadata.gz: 8c4305e2d7f5c8289edcc5f530dbb0d98cbc95f6db59a8ba98561506f2ec7e41ef89c6ab7271d8754f0a0dfc5ade6325833269f38595593c39eca44c8dcce7c5
7
+ data.tar.gz: 9a186d4164b031bbe27fb1490184d9c23387849267d4b1067c6bec6a39afdee4e6bf97d8bbbb364f36474fdad0e37e59f4ef63dae1454055244bc06f6791905b
@@ -0,0 +1,26 @@
1
+ name: rspec
2
+ on:
3
+ push:
4
+ pull_request:
5
+ jobs:
6
+ rspec:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ matrix:
10
+ ruby:
11
+ - '2.5'
12
+ - '2.6'
13
+ - '2.7'
14
+ - '3.0'
15
+ - '3.1'
16
+ - '3.2'
17
+ - '3.3'
18
+ - '3.4'
19
+ fail-fast: false
20
+ steps:
21
+ - uses: actions/checkout@v4
22
+ - uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: "${{ matrix.ruby }}"
25
+ bundler-cache: true
26
+ - run: bundle exec rspec --format documentation
@@ -0,0 +1,14 @@
1
+ name: rubocop
2
+ on:
3
+ push:
4
+ pull_request:
5
+ jobs:
6
+ rubocop:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: ruby/setup-ruby@v1
11
+ with:
12
+ ruby-version: '3'
13
+ bundler-cache: true
14
+ - run: bundle exec rubocop
data/CHANGELOG.md CHANGED
@@ -1,6 +1,20 @@
1
1
  # Change Log
2
2
 
3
- ## [Unreleased]
3
+ ## [v3.5.5] 2025-08-20
4
+ - fix percentages in print visitor
5
+ - repo cleanup
6
+ - fix modulo zero
7
+ - fix array arithmetic
8
+ - refactor parser
9
+
10
+ ## [v3.5.4] 2024-08-13
11
+ - add support for default value for PLUCK function
12
+ - improve error handling for MAP/ANY/ALL functions
13
+ - fix modulo / percentage operator determination
14
+ - fix string casing bug with bulk expressions
15
+ - add explicit gem dependency for BigDecimal
16
+
17
+ ## [v3.5.3] 2024-07-04
4
18
  - add support for empty array literals
5
19
  - add support for quoted identifiers
6
20
  - add REDUCE function
@@ -9,7 +23,7 @@
9
23
  - improve custom class arithmetic
10
24
  - fix IF dependency
11
25
 
12
- ## [v3.5.2]
26
+ ## [v3.5.2] 2023-12-06
13
27
  - add ABS function
14
28
  - add array support for AST visitors
15
29
  - add support for function callbacks
@@ -22,14 +36,14 @@
22
36
  - fix handling of Math::DomainError
23
37
  - fix invalid cast
24
38
 
25
- ## [v3.5.1]
39
+ ## [v3.5.1] 2022-10-24
26
40
  - add bitwise shift left and shift right operators
27
41
  - improve numeric conversions
28
42
  - improve parse exceptions
29
43
  - improve bitwise exceptions
30
44
  - include variable name in bulk expression exceptions
31
45
 
32
- ## [v3.5.0]
46
+ ## [v3.5.0] 2022-03-17
33
47
  - fix bug with function argument count
34
48
  - add XOR operator
35
49
  - make function args publicly accessible
@@ -41,7 +55,7 @@
41
55
  - respect case sensitivity in nested case statments
42
56
  - add visitor pattern
43
57
 
44
- ## [v3.4.2]
58
+ ## [v3.4.2] 2021-07-14
45
59
  - add FILTER function
46
60
  - add concurrent-ruby dependency to make global calculator object thread safe
47
61
  - add Ruby 3 support
@@ -253,7 +267,9 @@
253
267
  ## [v0.1.0] 2012-01-20
254
268
  - initial release
255
269
 
256
- [Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.2...HEAD
270
+ [v3.5.5]: https://github.com/rubysolo/dentaku/compare/v3.5.4...v3.5.5
271
+ [v3.5.4]: https://github.com/rubysolo/dentaku/compare/v3.5.3...v3.5.4
272
+ [v3.5.3]: https://github.com/rubysolo/dentaku/compare/v3.5.2...v3.5.3
257
273
  [v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
258
274
  [v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
259
275
  [v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
data/README.md CHANGED
@@ -3,9 +3,6 @@ Dentaku
3
3
 
4
4
  [![Join the chat at https://gitter.im/rubysolo/dentaku](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/rubysolo/dentaku?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
5
5
  [![Gem Version](https://badge.fury.io/rb/dentaku.png)](http://badge.fury.io/rb/dentaku)
6
- [![Build Status](https://travis-ci.org/rubysolo/dentaku.png?branch=master)](https://travis-ci.org/rubysolo/dentaku)
7
- [![Code Climate](https://codeclimate.com/github/rubysolo/dentaku.png)](https://codeclimate.com/github/rubysolo/dentaku)
8
- [![Coverage](https://codecov.io/gh/rubysolo/dentaku/branch/master/graph/badge.svg)](https://codecov.io/gh/rubysolo/dentaku)
9
6
 
10
7
 
11
8
  DESCRIPTION
@@ -147,7 +144,7 @@ Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`
147
144
 
148
145
  Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`
149
146
 
150
- Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L593))
147
+ Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/main/lib/dentaku/ast/case.rb))
151
148
 
152
149
  String: `LEFT`, `RIGHT`, `MID`, `LEN`, `FIND`, `SUBSTITUTE`, `CONCAT`, `CONTAINS`
153
150
 
data/dentaku.gemspec CHANGED
@@ -14,9 +14,9 @@ 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
- s.add_development_dependency('codecov')
20
20
  s.add_development_dependency('pry')
21
21
  s.add_development_dependency('pry-byebug')
22
22
  s.add_development_dependency('pry-stack_explorer')
@@ -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
@@ -9,20 +9,6 @@ module Dentaku
9
9
  DECIMAL = /\A-?\d*\.\d+\z/.freeze
10
10
  INTEGER = /\A-?\d+\z/.freeze
11
11
 
12
- def initialize(*)
13
- super
14
-
15
- unless valid_left?
16
- raise NodeError.new(:numeric, left.type, :left),
17
- "#{self.class} requires numeric operands"
18
- end
19
-
20
- unless valid_right?
21
- raise NodeError.new(:numeric, right.type, :right),
22
- "#{self.class} requires numeric operands"
23
- end
24
- end
25
-
26
12
  def type
27
13
  :numeric
28
14
  end
@@ -69,8 +55,9 @@ module Dentaku
69
55
  def datetime?(val)
70
56
  # val is a Date, Time, or DateTime
71
57
  return true if val.respond_to?(:strftime)
58
+ return false unless val.is_a?(::String)
72
59
 
73
- val.to_s =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
60
+ val =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
74
61
  end
75
62
 
76
63
  def valid_node?(node)
@@ -179,62 +166,61 @@ module Dentaku
179
166
 
180
167
  class Modulo < Arithmetic
181
168
  def self.arity
182
- @arity
169
+ 2
183
170
  end
184
171
 
185
- def self.peek(input)
186
- @arity = 1
187
- @arity = 2 if input.length > 1
172
+ def self.precedence
173
+ 20
188
174
  end
189
175
 
190
- def initialize(left, right = nil)
191
- if right
192
- @left = left
193
- @right = right
194
- else
195
- @right = left
196
- end
176
+ def self.resolve_class(next_token)
177
+ next_token.nil? || next_token.operator? || next_token.close? ? Percentage : self
178
+ end
197
179
 
198
- unless valid_left?
199
- raise NodeError.new(%i[numeric nil], left.type, :left),
200
- "#{self.class} requires numeric operands or nil"
201
- end
202
- unless valid_right?
203
- raise NodeError.new(:numeric, right.type, :right),
204
- "#{self.class} requires numeric operands"
205
- end
180
+ def operator
181
+ :%
206
182
  end
207
183
 
208
- def dependencies(context = {})
209
- if percent?
210
- @right.dependencies(context)
211
- else
212
- super
184
+ def value(context = {})
185
+ r = decimal(cast(right.value(context)))
186
+ raise Dentaku::ZeroDivisionError if r.zero?
187
+
188
+ cast(cast(left.value(context)) % r)
189
+ end
190
+ end
191
+
192
+ class Percentage < Arithmetic
193
+ def self.arity
194
+ 1
195
+ end
196
+
197
+ def initialize(child)
198
+ @left = child
199
+
200
+ unless valid_left?
201
+ raise NodeError.new(:numeric, left.type, :left),
202
+ "#{self.class} requires a numeric operand"
213
203
  end
214
204
  end
215
205
 
216
- def percent?
217
- left.nil?
206
+ def dependencies(context = {})
207
+ @left.dependencies(context)
218
208
  end
219
209
 
220
210
  def value(context = {})
221
- if percent?
222
- cast(right.value(context)) * 0.01
223
- else
224
- super
225
- end
211
+ cast(left.value(context)) * 0.01
226
212
  end
227
213
 
228
214
  def operator
229
215
  :%
230
216
  end
231
217
 
232
- def self.precedence
233
- 20
218
+ def operator_spacing
219
+ ""
234
220
  end
235
221
 
236
- def valid_left?
237
- valid_node?(left) || left.nil?
222
+ def self.precedence
223
+ 30
238
224
  end
239
225
  end
240
226
 
@@ -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
@@ -7,6 +7,18 @@ require 'dentaku/exceptions'
7
7
 
8
8
  module Dentaku
9
9
  module AST
10
+ # Examples of using in a formula:
11
+ #
12
+ # CASE x WHEN 1 THEN 2 WHEN 3 THEN 4 ELSE END
13
+ #
14
+ # CASE fruit
15
+ # WHEN 'apple'
16
+ # THEN 1 * quantity
17
+ # WHEN 'banana'
18
+ # THEN 2 * quantity
19
+ # ELSE
20
+ # 3 * quantity
21
+ # END
10
22
  class Case < Node
11
23
  attr_reader :switch, :conditions, :else
12
24
 
@@ -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
@@ -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
@@ -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 = {})
@@ -33,7 +33,12 @@ module Dentaku
33
33
  def display_operator
34
34
  operator.to_s
35
35
  end
36
+
36
37
  alias_method :to_s, :display_operator
38
+
39
+ def operator_spacing
40
+ " "
41
+ end
37
42
  end
38
43
  end
39
44
  end
data/lib/dentaku/ast.rb CHANGED
@@ -39,4 +39,4 @@ require_relative './ast/functions/ruby_math'
39
39
  require_relative './ast/functions/string_functions'
40
40
  require_relative './ast/functions/sum'
41
41
  require_relative './ast/functions/switch'
42
- 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,27 +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
71
+ store(context) do
66
72
  node = ast(expression)
67
73
  unbound = node.dependencies(memory)
74
+
68
75
  unless unbound.empty?
69
76
  raise UnboundVariableError.new(unbound),
70
77
  "no value provided for variables: #{unbound.uniq.join(', ')}"
71
78
  end
79
+
72
80
  node.value(memory)
73
81
  end
74
82
  end
75
83
 
84
+ private def evaluate_array!(expression, data = {}, &block)
85
+ expression.map { |e| evaluate!(e, data, &block) }
86
+ end
87
+
76
88
  def solve!(expression_hash)
77
89
  BulkExpressionSolver.new(expression_hash, self).solve!
78
90
  end
@@ -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,16 +27,14 @@ module Dentaku
29
27
 
30
28
  def sub(duration)
31
29
  case duration
32
- when Date, 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
43
39
  when Dentaku::TokenScanner::DATE_TIME_REGEXP
44
40
  @base - Time.parse(duration).to_datetime
@@ -47,5 +43,18 @@ module Dentaku
47
43
  "'#{duration || duration.class}' is not coercible for date arithmetic"
48
44
  end
49
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
50
59
  end
51
60
  end