dentaku 3.2.0 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +5 -10
  3. data/.travis.yml +4 -6
  4. data/CHANGELOG.md +86 -2
  5. data/README.md +7 -6
  6. data/dentaku.gemspec +1 -1
  7. data/lib/dentaku/ast/access.rb +21 -1
  8. data/lib/dentaku/ast/arithmetic.rb +51 -15
  9. data/lib/dentaku/ast/array.rb +41 -0
  10. data/lib/dentaku/ast/bitwise.rb +30 -5
  11. data/lib/dentaku/ast/case/case_conditional.rb +17 -2
  12. data/lib/dentaku/ast/case/case_else.rb +17 -3
  13. data/lib/dentaku/ast/case/case_switch_variable.rb +14 -0
  14. data/lib/dentaku/ast/case/case_then.rb +17 -3
  15. data/lib/dentaku/ast/case/case_when.rb +21 -3
  16. data/lib/dentaku/ast/case.rb +19 -3
  17. data/lib/dentaku/ast/comparators.rb +38 -28
  18. data/lib/dentaku/ast/function.rb +11 -3
  19. data/lib/dentaku/ast/function_registry.rb +21 -0
  20. data/lib/dentaku/ast/functions/all.rb +23 -0
  21. data/lib/dentaku/ast/functions/and.rb +2 -2
  22. data/lib/dentaku/ast/functions/any.rb +23 -0
  23. data/lib/dentaku/ast/functions/avg.rb +2 -2
  24. data/lib/dentaku/ast/functions/count.rb +8 -0
  25. data/lib/dentaku/ast/functions/duration.rb +51 -0
  26. data/lib/dentaku/ast/functions/enum.rb +37 -0
  27. data/lib/dentaku/ast/functions/filter.rb +23 -0
  28. data/lib/dentaku/ast/functions/if.rb +19 -2
  29. data/lib/dentaku/ast/functions/map.rb +23 -0
  30. data/lib/dentaku/ast/functions/or.rb +4 -4
  31. data/lib/dentaku/ast/functions/pluck.rb +30 -0
  32. data/lib/dentaku/ast/functions/round.rb +1 -1
  33. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  34. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  35. data/lib/dentaku/ast/functions/ruby_math.rb +50 -3
  36. data/lib/dentaku/ast/functions/string_functions.rb +105 -12
  37. data/lib/dentaku/ast/functions/xor.rb +44 -0
  38. data/lib/dentaku/ast/grouping.rb +3 -1
  39. data/lib/dentaku/ast/identifier.rb +16 -4
  40. data/lib/dentaku/ast/literal.rb +10 -0
  41. data/lib/dentaku/ast/negation.rb +7 -1
  42. data/lib/dentaku/ast/nil.rb +4 -0
  43. data/lib/dentaku/ast/node.rb +8 -0
  44. data/lib/dentaku/ast/operation.rb +17 -0
  45. data/lib/dentaku/ast/string.rb +7 -0
  46. data/lib/dentaku/ast.rb +8 -0
  47. data/lib/dentaku/bulk_expression_solver.rb +38 -27
  48. data/lib/dentaku/calculator.rb +21 -8
  49. data/lib/dentaku/date_arithmetic.rb +45 -0
  50. data/lib/dentaku/exceptions.rb +11 -8
  51. data/lib/dentaku/flat_hash.rb +9 -2
  52. data/lib/dentaku/parser.rb +57 -16
  53. data/lib/dentaku/print_visitor.rb +101 -0
  54. data/lib/dentaku/token_matcher.rb +1 -1
  55. data/lib/dentaku/token_scanner.rb +9 -3
  56. data/lib/dentaku/tokenizer.rb +7 -2
  57. data/lib/dentaku/version.rb +1 -1
  58. data/lib/dentaku/visitor/infix.rb +82 -0
  59. data/lib/dentaku.rb +20 -7
  60. data/spec/ast/addition_spec.rb +7 -1
  61. data/spec/ast/all_spec.rb +25 -0
  62. data/spec/ast/and_function_spec.rb +6 -6
  63. data/spec/ast/and_spec.rb +1 -1
  64. data/spec/ast/any_spec.rb +23 -0
  65. data/spec/ast/arithmetic_spec.rb +64 -29
  66. data/spec/ast/avg_spec.rb +9 -5
  67. data/spec/ast/comparator_spec.rb +31 -1
  68. data/spec/ast/count_spec.rb +7 -7
  69. data/spec/ast/division_spec.rb +7 -1
  70. data/spec/ast/filter_spec.rb +25 -0
  71. data/spec/ast/function_spec.rb +20 -15
  72. data/spec/ast/map_spec.rb +27 -0
  73. data/spec/ast/max_spec.rb +16 -3
  74. data/spec/ast/min_spec.rb +16 -3
  75. data/spec/ast/mul_spec.rb +11 -6
  76. data/spec/ast/negation_spec.rb +48 -0
  77. data/spec/ast/node_spec.rb +11 -8
  78. data/spec/ast/numeric_spec.rb +1 -1
  79. data/spec/ast/or_spec.rb +7 -7
  80. data/spec/ast/pluck_spec.rb +32 -0
  81. data/spec/ast/round_spec.rb +14 -4
  82. data/spec/ast/rounddown_spec.rb +14 -4
  83. data/spec/ast/roundup_spec.rb +14 -4
  84. data/spec/ast/string_functions_spec.rb +73 -0
  85. data/spec/ast/sum_spec.rb +11 -6
  86. data/spec/ast/switch_spec.rb +5 -5
  87. data/spec/ast/xor_spec.rb +35 -0
  88. data/spec/bulk_expression_solver_spec.rb +37 -1
  89. data/spec/calculator_spec.rb +341 -32
  90. data/spec/dentaku_spec.rb +19 -6
  91. data/spec/external_function_spec.rb +32 -6
  92. data/spec/parser_spec.rb +100 -123
  93. data/spec/print_visitor_spec.rb +66 -0
  94. data/spec/spec_helper.rb +6 -4
  95. data/spec/token_matcher_spec.rb +8 -8
  96. data/spec/token_scanner_spec.rb +4 -4
  97. data/spec/tokenizer_spec.rb +56 -13
  98. data/spec/visitor/infix_spec.rb +31 -0
  99. data/spec/visitor_spec.rb +138 -0
  100. metadata +52 -7
@@ -3,7 +3,28 @@ require_relative '../function'
3
3
  module Dentaku
4
4
  module AST
5
5
  module StringFunctions
6
- class Left < Function
6
+ class Base < Function
7
+ def type
8
+ :string
9
+ end
10
+
11
+ def negative_argument_failure(fun, arg = 'length')
12
+ raise Dentaku::ArgumentError.for(
13
+ :invalid_value,
14
+ function_name: "#{fun}()"
15
+ ), "#{fun}() requires #{arg} to be positive"
16
+ end
17
+ end
18
+
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
+
7
28
  def initialize(*args)
8
29
  super
9
30
  @string, @length = *@args
@@ -11,12 +32,21 @@ module Dentaku
11
32
 
12
33
  def value(context = {})
13
34
  string = @string.value(context).to_s
14
- length = @length.value(context)
35
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
36
+ negative_argument_failure('LEFT') if length < 0
15
37
  string[0, length]
16
38
  end
17
39
  end
18
40
 
19
- class Right < Function
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
+
20
50
  def initialize(*args)
21
51
  super
22
52
  @string, @length = *@args
@@ -24,12 +54,21 @@ module Dentaku
24
54
 
25
55
  def value(context = {})
26
56
  string = @string.value(context).to_s
27
- length = @length.value(context)
57
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
58
+ negative_argument_failure('RIGHT') if length < 0
28
59
  string[length * -1, length] || string
29
60
  end
30
61
  end
31
62
 
32
- class Mid < Function
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
+
33
72
  def initialize(*args)
34
73
  super
35
74
  @string, @offset, @length = *@args
@@ -37,13 +76,23 @@ module Dentaku
37
76
 
38
77
  def value(context = {})
39
78
  string = @string.value(context).to_s
40
- offset = @offset.value(context)
41
- length = @length.value(context)
79
+ offset = Dentaku::AST::Function.numeric(@offset.value(context)).to_i
80
+ negative_argument_failure('MID', 'offset') if offset < 0
81
+ length = Dentaku::AST::Function.numeric(@length.value(context)).to_i
82
+ negative_argument_failure('MID') if length < 0
42
83
  string[offset - 1, length].to_s
43
84
  end
44
85
  end
45
86
 
46
- class Len < Function
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
+
47
96
  def initialize(*args)
48
97
  super
49
98
  @string = @args[0]
@@ -53,9 +102,21 @@ module Dentaku
53
102
  string = @string.value(context).to_s
54
103
  string.length
55
104
  end
105
+
106
+ def type
107
+ :numeric
108
+ end
56
109
  end
57
110
 
58
- class Find < Function
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
+
59
120
  def initialize(*args)
60
121
  super
61
122
  @needle, @haystack = *@args
@@ -68,9 +129,21 @@ module Dentaku
68
129
  pos = haystack.index(needle)
69
130
  pos && pos + 1
70
131
  end
132
+
133
+ def type
134
+ :numeric
135
+ end
71
136
  end
72
137
 
73
- class Substitute < Function
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
+
74
147
  def initialize(*args)
75
148
  super
76
149
  @original, @search, @replacement = *@args
@@ -85,7 +158,15 @@ module Dentaku
85
158
  end
86
159
  end
87
160
 
88
- class Concat < Function
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
+
89
170
  def initialize(*args)
90
171
  super
91
172
  end
@@ -95,7 +176,15 @@ module Dentaku
95
176
  end
96
177
  end
97
178
 
98
- class Contains < Function
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
+
99
188
  def initialize(*args)
100
189
  super
101
190
  @needle, @haystack = *args
@@ -104,6 +193,10 @@ module Dentaku
104
193
  def value(context = {})
105
194
  @haystack.value(context).to_s.include? @needle.value(context).to_s
106
195
  end
196
+
197
+ def type
198
+ :logical
199
+ end
107
200
  end
108
201
  end
109
202
  end
@@ -0,0 +1,44 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Xor < Function
7
+ def self.min_param_count
8
+ 1
9
+ end
10
+
11
+ def self.max_param_count
12
+ Float::INFINITY
13
+ end
14
+
15
+ def value(context = {})
16
+ if @args.empty?
17
+ raise Dentaku::ArgumentError.for(
18
+ :too_few_arguments,
19
+ function_name: 'XOR()', at_least: 1, given: 0
20
+ ), 'XOR() requires at least one argument'
21
+ end
22
+
23
+ true_arg_count = 0
24
+ @args.each do |arg|
25
+ case arg.value(context)
26
+ when TrueClass
27
+ true_arg_count += 1
28
+ break if true_arg_count > 1
29
+ when FalseClass, nil
30
+ next
31
+ else
32
+ raise Dentaku::ArgumentError.for(
33
+ :incompatible_type,
34
+ function_name: 'XOR()', expect: :logical, actual: arg.class
35
+ ), 'XOR() requires arguments to be logical expressions'
36
+ end
37
+ end
38
+ true_arg_count == 1
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ Dentaku::AST::Function.register_class(:xor, Dentaku::AST::Xor)
@@ -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,20 +20,32 @@ 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
27
31
  end
28
32
 
29
33
  def dependencies(context = {})
30
- context.key?(identifier) ? dependencies_of(context[identifier]) : [identifier]
34
+ context.key?(identifier) ? dependencies_of(context[identifier], context) : [identifier]
35
+ end
36
+
37
+ def accept(visitor)
38
+ visitor.visit_identifier(self)
39
+ end
40
+
41
+ def to_s
42
+ identifier.to_s
31
43
  end
32
44
 
33
45
  private
34
46
 
35
- def dependencies_of(node)
36
- node.respond_to?(:dependencies) ? node.dependencies : []
47
+ def dependencies_of(node, context)
48
+ node.respond_to?(:dependencies) ? node.dependencies(context) : []
37
49
  end
38
50
  end
39
51
  end
@@ -4,6 +4,7 @@ module Dentaku
4
4
  attr_reader :type
5
5
 
6
6
  def initialize(token)
7
+ @token = token
7
8
  @value = token.value
8
9
  @type = token.category
9
10
  end
@@ -15,6 +16,15 @@ module Dentaku
15
16
  def dependencies(*)
16
17
  []
17
18
  end
19
+
20
+ def accept(visitor)
21
+ visitor.visit_literal(self)
22
+ end
23
+
24
+ def quoted
25
+ @token.raw_value || value.to_s
26
+ end
27
+ alias_method :to_s, :quoted
18
28
  end
19
29
  end
20
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
@@ -38,6 +40,10 @@ module Dentaku
38
40
  @node.dependencies(context)
39
41
  end
40
42
 
43
+ def accept(visitor)
44
+ visitor.visit_negation(self)
45
+ end
46
+
41
47
  private
42
48
 
43
49
  def valid_node?(node)
@@ -4,6 +4,10 @@ module Dentaku
4
4
  def value(*)
5
5
  nil
6
6
  end
7
+
8
+ def accept(visitor)
9
+ visitor.visit_nil(self)
10
+ end
7
11
  end
8
12
  end
9
13
  end
@@ -15,6 +15,14 @@ module Dentaku
15
15
  def dependencies(context = {})
16
16
  []
17
17
  end
18
+
19
+ def type
20
+ nil
21
+ end
22
+
23
+ def name
24
+ self.class.name.to_s.split("::").last.upcase
25
+ end
18
26
  end
19
27
  end
20
28
  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
@@ -17,6 +25,15 @@ module Dentaku
17
25
  def self.right_associative?
18
26
  false
19
27
  end
28
+
29
+ def accept(visitor)
30
+ visitor.visit_operation(self)
31
+ end
32
+
33
+ def display_operator
34
+ operator.to_s
35
+ end
36
+ alias_method :to_s, :display_operator
20
37
  end
21
38
  end
22
39
  end
@@ -3,6 +3,13 @@ require_relative "./literal"
3
3
  module Dentaku
4
4
  module AST
5
5
  class String < Literal
6
+ def quoted
7
+ %Q{"#{ escaped }"}
8
+ end
9
+
10
+ def escaped
11
+ @value.gsub('"', '\"')
12
+ end
6
13
  end
7
14
  end
8
15
  end
data/lib/dentaku/ast.rb CHANGED
@@ -11,17 +11,24 @@ require_relative './ast/negation'
11
11
  require_relative './ast/comparators'
12
12
  require_relative './ast/combinators'
13
13
  require_relative './ast/access'
14
+ require_relative './ast/array'
14
15
  require_relative './ast/grouping'
15
16
  require_relative './ast/case'
16
17
  require_relative './ast/function_registry'
18
+ require_relative './ast/functions/all'
17
19
  require_relative './ast/functions/and'
20
+ require_relative './ast/functions/any'
18
21
  require_relative './ast/functions/avg'
19
22
  require_relative './ast/functions/count'
23
+ require_relative './ast/functions/duration'
24
+ require_relative './ast/functions/filter'
20
25
  require_relative './ast/functions/if'
26
+ require_relative './ast/functions/map'
21
27
  require_relative './ast/functions/max'
22
28
  require_relative './ast/functions/min'
23
29
  require_relative './ast/functions/not'
24
30
  require_relative './ast/functions/or'
31
+ require_relative './ast/functions/pluck'
25
32
  require_relative './ast/functions/round'
26
33
  require_relative './ast/functions/rounddown'
27
34
  require_relative './ast/functions/roundup'
@@ -29,3 +36,4 @@ require_relative './ast/functions/ruby_math'
29
36
  require_relative './ast/functions/string_functions'
30
37
  require_relative './ast/functions/sum'
31
38
  require_relative './ast/functions/switch'
39
+ require_relative './ast/functions/xor'
@@ -20,8 +20,9 @@ module Dentaku
20
20
  results = load_results(&error_handler)
21
21
 
22
22
  FlatHash.expand(
23
- expression_hash.each_with_object({}) do |(k, _), r|
24
- r[k] = results[k.to_s]
23
+ expression_hash.each_with_object({}) do |(k, v), r|
24
+ default = v.nil? ? v : :undefined
25
+ r[k] = results.fetch(k.to_s, default)
25
26
  end
26
27
  )
27
28
  end
@@ -52,35 +53,49 @@ module Dentaku
52
53
  end
53
54
 
54
55
  def expression_with_exception_handler(&block)
55
- ->(expr, ex) { block.call(ex) }
56
+ ->(_expr, ex) { block.call(ex) }
56
57
  end
57
58
 
58
59
  def load_results(&block)
59
- variables_in_resolve_order.each_with_object({}) do |var_name, r|
60
- begin
61
- solved = calculator.memory
62
- value_from_memory = solved[var_name.downcase]
63
-
64
- if value_from_memory.nil? &&
65
- expressions[var_name].nil? &&
66
- !solved.has_key?(var_name)
67
- next
68
- end
69
-
70
- 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!(
71
77
  expressions[var_name],
72
- expressions.merge(r).merge(solved),
78
+ context.merge(results),
73
79
  &expression_with_exception_handler(&block)
74
80
  )
75
-
76
- r[var_name] = value
77
- rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
78
- ex.recipient_variable = var_name
79
- r[var_name] = block.call(ex)
80
- rescue Dentaku::ArgumentError => ex
81
- r[var_name] = block.call(ex)
82
81
  end
83
82
  end
83
+
84
+ rescue TSort::Cyclic => ex
85
+ block.call(ex)
86
+ {}
87
+ end
88
+
89
+ def with_rescues(var_name, results, block)
90
+ yield
91
+
92
+ rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
93
+ ex.recipient_variable = var_name
94
+ results[var_name] = block.call(ex)
95
+ ensure
96
+ if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
97
+ results[var_name] = calculator.memory[var_name.downcase]
98
+ end
84
99
  end
85
100
 
86
101
  def expressions
@@ -109,9 +124,5 @@ module Dentaku
109
124
  end
110
125
  }
111
126
  end
112
-
113
- def evaluate!(expression, results, &block)
114
- calculator.evaluate!(expression, results, &block)
115
- end
116
127
  end
117
128
  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
@@ -46,7 +47,7 @@ module Dentaku
46
47
 
47
48
  def evaluate(expression, data = {}, &block)
48
49
  evaluate!(expression, data)
49
- rescue UnboundVariableError, Dentaku::ArgumentError => ex
50
+ rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
50
51
  block.call(expression, ex) if block_given?
51
52
  end
52
53
 
@@ -58,10 +59,10 @@ module Dentaku
58
59
  store(data) do
59
60
  node = expression
60
61
  node = ast(node) unless node.is_a?(AST::Node)
61
- unbound = node.dependencies - memory.keys
62
+ unbound = node.dependencies(memory)
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