dentaku 3.2.0 → 3.5.1

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 (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