hayadentaku 3.5.7

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 (132) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rspec.yml +26 -0
  3. data/.github/workflows/rubocop.yml +14 -0
  4. data/.gitignore +14 -0
  5. data/.pryrc +2 -0
  6. data/.rubocop.yml +114 -0
  7. data/.travis.yml +10 -0
  8. data/CHANGELOG.md +328 -0
  9. data/Gemfile +4 -0
  10. data/LICENSE +21 -0
  11. data/README.md +352 -0
  12. data/Rakefile +31 -0
  13. data/hayadentaku.gemspec +35 -0
  14. data/lib/dentaku/ast/access.rb +44 -0
  15. data/lib/dentaku/ast/arithmetic.rb +292 -0
  16. data/lib/dentaku/ast/array.rb +38 -0
  17. data/lib/dentaku/ast/bitwise.rb +42 -0
  18. data/lib/dentaku/ast/case/case_conditional.rb +38 -0
  19. data/lib/dentaku/ast/case/case_else.rb +35 -0
  20. data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
  21. data/lib/dentaku/ast/case/case_then.rb +35 -0
  22. data/lib/dentaku/ast/case/case_when.rb +39 -0
  23. data/lib/dentaku/ast/case.rb +93 -0
  24. data/lib/dentaku/ast/combinators.rb +50 -0
  25. data/lib/dentaku/ast/comparators.rb +88 -0
  26. data/lib/dentaku/ast/datetime.rb +8 -0
  27. data/lib/dentaku/ast/function.rb +56 -0
  28. data/lib/dentaku/ast/function_registry.rb +107 -0
  29. data/lib/dentaku/ast/functions/abs.rb +5 -0
  30. data/lib/dentaku/ast/functions/all.rb +19 -0
  31. data/lib/dentaku/ast/functions/and.rb +25 -0
  32. data/lib/dentaku/ast/functions/any.rb +19 -0
  33. data/lib/dentaku/ast/functions/avg.rb +13 -0
  34. data/lib/dentaku/ast/functions/count.rb +26 -0
  35. data/lib/dentaku/ast/functions/duration.rb +51 -0
  36. data/lib/dentaku/ast/functions/enum.rb +54 -0
  37. data/lib/dentaku/ast/functions/filter.rb +21 -0
  38. data/lib/dentaku/ast/functions/if.rb +47 -0
  39. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  40. data/lib/dentaku/ast/functions/map.rb +19 -0
  41. data/lib/dentaku/ast/functions/max.rb +5 -0
  42. data/lib/dentaku/ast/functions/min.rb +5 -0
  43. data/lib/dentaku/ast/functions/mul.rb +12 -0
  44. data/lib/dentaku/ast/functions/not.rb +5 -0
  45. data/lib/dentaku/ast/functions/or.rb +25 -0
  46. data/lib/dentaku/ast/functions/pluck.rb +34 -0
  47. data/lib/dentaku/ast/functions/reduce.rb +60 -0
  48. data/lib/dentaku/ast/functions/round.rb +5 -0
  49. data/lib/dentaku/ast/functions/rounddown.rb +8 -0
  50. data/lib/dentaku/ast/functions/roundup.rb +8 -0
  51. data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
  52. data/lib/dentaku/ast/functions/string_functions.rb +212 -0
  53. data/lib/dentaku/ast/functions/sum.rb +12 -0
  54. data/lib/dentaku/ast/functions/switch.rb +8 -0
  55. data/lib/dentaku/ast/functions/xor.rb +44 -0
  56. data/lib/dentaku/ast/grouping.rb +23 -0
  57. data/lib/dentaku/ast/identifier.rb +52 -0
  58. data/lib/dentaku/ast/literal.rb +30 -0
  59. data/lib/dentaku/ast/logical.rb +8 -0
  60. data/lib/dentaku/ast/negation.rb +54 -0
  61. data/lib/dentaku/ast/nil.rb +13 -0
  62. data/lib/dentaku/ast/node.rb +29 -0
  63. data/lib/dentaku/ast/numeric.rb +8 -0
  64. data/lib/dentaku/ast/operation.rb +44 -0
  65. data/lib/dentaku/ast/string.rb +15 -0
  66. data/lib/dentaku/ast.rb +42 -0
  67. data/lib/dentaku/bulk_expression_solver.rb +158 -0
  68. data/lib/dentaku/calculator.rb +192 -0
  69. data/lib/dentaku/date_arithmetic.rb +60 -0
  70. data/lib/dentaku/dependency_resolver.rb +29 -0
  71. data/lib/dentaku/exceptions.rb +116 -0
  72. data/lib/dentaku/flat_hash.rb +161 -0
  73. data/lib/dentaku/parser.rb +318 -0
  74. data/lib/dentaku/print_visitor.rb +112 -0
  75. data/lib/dentaku/string_casing.rb +7 -0
  76. data/lib/dentaku/token.rb +48 -0
  77. data/lib/dentaku/token_matcher.rb +138 -0
  78. data/lib/dentaku/token_matchers.rb +29 -0
  79. data/lib/dentaku/token_scanner.rb +240 -0
  80. data/lib/dentaku/tokenizer.rb +127 -0
  81. data/lib/dentaku/version.rb +3 -0
  82. data/lib/dentaku/visitor/infix.rb +86 -0
  83. data/lib/dentaku.rb +69 -0
  84. data/spec/ast/abs_spec.rb +26 -0
  85. data/spec/ast/addition_spec.rb +67 -0
  86. data/spec/ast/all_spec.rb +38 -0
  87. data/spec/ast/and_function_spec.rb +35 -0
  88. data/spec/ast/and_spec.rb +32 -0
  89. data/spec/ast/any_spec.rb +36 -0
  90. data/spec/ast/arithmetic_spec.rb +147 -0
  91. data/spec/ast/avg_spec.rb +42 -0
  92. data/spec/ast/case_spec.rb +84 -0
  93. data/spec/ast/comparator_spec.rb +87 -0
  94. data/spec/ast/count_spec.rb +40 -0
  95. data/spec/ast/division_spec.rb +64 -0
  96. data/spec/ast/filter_spec.rb +25 -0
  97. data/spec/ast/function_spec.rb +69 -0
  98. data/spec/ast/intercept_spec.rb +30 -0
  99. data/spec/ast/map_spec.rb +40 -0
  100. data/spec/ast/max_spec.rb +33 -0
  101. data/spec/ast/min_spec.rb +33 -0
  102. data/spec/ast/mul_spec.rb +43 -0
  103. data/spec/ast/negation_spec.rb +48 -0
  104. data/spec/ast/node_spec.rb +43 -0
  105. data/spec/ast/numeric_spec.rb +16 -0
  106. data/spec/ast/or_spec.rb +35 -0
  107. data/spec/ast/pluck_spec.rb +49 -0
  108. data/spec/ast/reduce_spec.rb +22 -0
  109. data/spec/ast/round_spec.rb +35 -0
  110. data/spec/ast/rounddown_spec.rb +35 -0
  111. data/spec/ast/roundup_spec.rb +35 -0
  112. data/spec/ast/string_functions_spec.rb +217 -0
  113. data/spec/ast/sum_spec.rb +43 -0
  114. data/spec/ast/switch_spec.rb +30 -0
  115. data/spec/ast/xor_spec.rb +35 -0
  116. data/spec/benchmark.rb +70 -0
  117. data/spec/bulk_expression_solver_spec.rb +241 -0
  118. data/spec/calculator_spec.rb +1003 -0
  119. data/spec/dentaku_spec.rb +52 -0
  120. data/spec/dependency_resolver_spec.rb +18 -0
  121. data/spec/exceptions_spec.rb +9 -0
  122. data/spec/external_function_spec.rb +177 -0
  123. data/spec/parser_spec.rb +183 -0
  124. data/spec/print_visitor_spec.rb +77 -0
  125. data/spec/spec_helper.rb +69 -0
  126. data/spec/token_matcher_spec.rb +134 -0
  127. data/spec/token_scanner_spec.rb +49 -0
  128. data/spec/token_spec.rb +16 -0
  129. data/spec/tokenizer_spec.rb +375 -0
  130. data/spec/visitor/infix_spec.rb +52 -0
  131. data/spec/visitor_spec.rb +139 -0
  132. metadata +353 -0
@@ -0,0 +1,44 @@
1
+ require_relative './node'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Operation < Node
6
+ attr_reader :left, :right
7
+
8
+ def self.min_param_count
9
+ arity
10
+ end
11
+
12
+ def self.max_param_count
13
+ arity
14
+ end
15
+
16
+ def initialize(left, right)
17
+ @left = left
18
+ @right = right
19
+ end
20
+
21
+ def dependencies(context = {})
22
+ (left.dependencies(context) + right.dependencies(context)).uniq
23
+ end
24
+
25
+ def self.right_associative?
26
+ false
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
+
37
+ alias_method :to_s, :display_operator
38
+
39
+ def operator_spacing
40
+ " "
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ require_relative "./literal"
2
+
3
+ module Dentaku
4
+ module AST
5
+ class String < Literal
6
+ def quoted
7
+ %Q{"#{ escaped }"}
8
+ end
9
+
10
+ def escaped
11
+ @value.gsub('"', '\"')
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ require_relative './ast/node'
2
+ require_relative './ast/nil'
3
+ require_relative './ast/datetime'
4
+ require_relative './ast/numeric'
5
+ require_relative './ast/logical'
6
+ require_relative './ast/string'
7
+ require_relative './ast/identifier'
8
+ require_relative './ast/arithmetic'
9
+ require_relative './ast/bitwise'
10
+ require_relative './ast/negation'
11
+ require_relative './ast/comparators'
12
+ require_relative './ast/combinators'
13
+ require_relative './ast/access'
14
+ require_relative './ast/array'
15
+ require_relative './ast/grouping'
16
+ require_relative './ast/case'
17
+ require_relative './ast/function_registry'
18
+ require_relative './ast/functions/abs'
19
+ require_relative './ast/functions/all'
20
+ require_relative './ast/functions/and'
21
+ require_relative './ast/functions/any'
22
+ require_relative './ast/functions/avg'
23
+ require_relative './ast/functions/count'
24
+ require_relative './ast/functions/duration'
25
+ require_relative './ast/functions/filter'
26
+ require_relative './ast/functions/if'
27
+ require_relative './ast/functions/intercept'
28
+ require_relative './ast/functions/map'
29
+ require_relative './ast/functions/max'
30
+ require_relative './ast/functions/min'
31
+ require_relative './ast/functions/not'
32
+ require_relative './ast/functions/or'
33
+ require_relative './ast/functions/pluck'
34
+ require_relative './ast/functions/reduce'
35
+ require_relative './ast/functions/round'
36
+ require_relative './ast/functions/rounddown'
37
+ require_relative './ast/functions/roundup'
38
+ require_relative './ast/functions/ruby_math'
39
+ require_relative './ast/functions/string_functions'
40
+ require_relative './ast/functions/sum'
41
+ require_relative './ast/functions/switch'
42
+ require_relative './ast/functions/xor'
@@ -0,0 +1,158 @@
1
+ require 'dentaku/dependency_resolver'
2
+ require 'dentaku/exceptions'
3
+ require 'dentaku/flat_hash'
4
+ require 'dentaku/parser'
5
+ require 'dentaku/tokenizer'
6
+
7
+ module Dentaku
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
+
32
+ def initialize(expressions, calculator)
33
+ @expression_hash = FlatHash.from_hash(expressions)
34
+ @calculator = calculator
35
+ end
36
+
37
+ def solve!
38
+ @evaluator = StrictEvaluator.new(calculator)
39
+ solve(&raise_exception_handler)
40
+ end
41
+
42
+ def solve(&block)
43
+ @evaluator ||= PermissiveEvaluator.new(calculator, block)
44
+ error_handler = block || return_undefined_handler
45
+ results = load_results(&error_handler)
46
+
47
+ FlatHash.expand(
48
+ expression_hash.each_with_object({}) do |(k, v), r|
49
+ default = v.nil? ? v : :undefined
50
+ r[k] = results.fetch(k.to_s, default)
51
+ end
52
+ )
53
+ end
54
+
55
+ def dependencies
56
+ Hash[expression_deps].tap do |d|
57
+ d.values.each do |deps|
58
+ unresolved = deps.reject { |ud| d.has_key?(ud) }
59
+ unresolved.each { |u| add_dependencies(d, u) }
60
+ end
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def self.dependency_cache
67
+ @dep_cache ||= {}
68
+ end
69
+
70
+ attr_reader :expression_hash, :calculator, :evaluator
71
+
72
+ def return_undefined_handler
73
+ ->(*) { :undefined }
74
+ end
75
+
76
+ def raise_exception_handler
77
+ ->(ex) { raise ex }
78
+ end
79
+
80
+ def expression_with_exception_handler(var_name, &block)
81
+ ->(_expr, ex) {
82
+ ex.recipient_variable = var_name
83
+ block.call(ex)
84
+ }
85
+ end
86
+
87
+ def load_results(&block)
88
+ facts, _formulas = expressions.transform_keys(&:downcase)
89
+ .transform_values { |v| calculator.ast(v) }
90
+ .partition { |_, v| calculator.dependencies(v, nil).empty? }
91
+
92
+ evaluated_facts = facts.to_h.each_with_object({}) do |(var_name, ast), h|
93
+ with_rescues(var_name, h, block) do
94
+ h[var_name] = ast.is_a?(Array) ? ast.map(&:value) : ast.value
95
+ end
96
+ end
97
+
98
+ context = calculator.memory.merge(evaluated_facts)
99
+
100
+ variables_in_resolve_order.each_with_object({}) do |var_name, results|
101
+ next if expressions[var_name].nil?
102
+
103
+ with_rescues(var_name, results, block) do
104
+ results[var_name] = evaluated_facts[var_name] || evaluator.evaluate(
105
+ expressions[var_name],
106
+ context.merge(results),
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
+ }
112
+ end
113
+ end
114
+
115
+ rescue TSort::Cyclic => ex
116
+ block.call(ex)
117
+ {}
118
+ end
119
+
120
+ def with_rescues(var_name, results, block)
121
+ yield
122
+ rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
123
+ ex.recipient_variable = var_name
124
+ results[var_name] = block.call(ex)
125
+ ensure
126
+ if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
127
+ results[var_name] = calculator.memory[var_name.downcase]
128
+ end
129
+ end
130
+
131
+ def expressions
132
+ @expressions ||= Hash[expression_hash.map { |k, v| [k.to_s, v] }]
133
+ end
134
+
135
+ def expression_deps
136
+ expressions.map do |var, expr|
137
+ [var, calculator.dependencies(expr)]
138
+ end
139
+ end
140
+
141
+ def add_dependencies(current_dependencies, variable)
142
+ node = calculator.memory[variable]
143
+ if node.respond_to?(:dependencies)
144
+ current_dependencies[variable] = node.dependencies
145
+ node.dependencies.each { |d| add_dependencies(current_dependencies, d) }
146
+ end
147
+ end
148
+
149
+ def variables_in_resolve_order
150
+ cache_key = expressions.keys.map(&:to_s).sort.join("|")
151
+ @ordered_deps ||= self.class.dependency_cache.fetch(cache_key) {
152
+ DependencyResolver.find_resolve_order(dependencies).tap do |d|
153
+ self.class.dependency_cache[cache_key] = d if Dentaku.cache_dependency_order?
154
+ end
155
+ }
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,192 @@
1
+ require 'dentaku/bulk_expression_solver'
2
+ require 'dentaku/dependency_resolver'
3
+ require 'dentaku/exceptions'
4
+ require 'dentaku/flat_hash'
5
+ require 'dentaku/parser'
6
+ require 'dentaku/string_casing'
7
+ require 'dentaku/token'
8
+
9
+ module Dentaku
10
+ class Calculator
11
+ include StringCasing
12
+ attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
13
+ :nested_data_support, :ast_cache, :raw_date_literals
14
+
15
+ def initialize(options = {})
16
+ clear
17
+ @tokenizer = Tokenizer.new
18
+ @case_sensitive = options.delete(:case_sensitive)
19
+ @aliases = options.delete(:aliases) || Dentaku.aliases
20
+ @nested_data_support = options.fetch(:nested_data_support, true)
21
+ options.delete(:nested_data_support)
22
+ @raw_date_literals = options.fetch(:raw_date_literals, true)
23
+ options.delete(:raw_date_literals)
24
+ @ast_cache = options
25
+ @disable_ast_cache = false
26
+ @function_registry = Dentaku::AST::FunctionRegistry.new
27
+ end
28
+
29
+ def self.add_function(name, type, body, callback = nil)
30
+ Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback)
31
+ end
32
+
33
+ def self.add_functions(functions)
34
+ functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
35
+ end
36
+
37
+ def add_function(name, type, body, callback = nil)
38
+ @function_registry.register(name, type, body, callback)
39
+ self
40
+ end
41
+
42
+ def add_functions(functions)
43
+ functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
44
+ self
45
+ end
46
+
47
+ def disable_cache
48
+ @disable_ast_cache = true
49
+ yield(self) if block_given?
50
+ ensure
51
+ @disable_ast_cache = false
52
+ end
53
+
54
+ def evaluate(expression, data = {}, &block)
55
+ context = evaluation_context(data, :permissive)
56
+ return evaluate_array(expression, context, &block) if expression.is_a?(Array)
57
+
58
+ evaluate!(expression, context)
59
+ rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
60
+ block.call(expression, ex) if block_given?
61
+ end
62
+
63
+ private def evaluate_array(expression, data = {}, &block)
64
+ expression.map { |e| evaluate(e, data, &block) }
65
+ end
66
+
67
+ def evaluate!(expression, data = {}, &block)
68
+ context = evaluation_context(data, :strict)
69
+ return evaluate_array!(expression, context, &block) if expression.is_a? Array
70
+
71
+ store(context) do
72
+ node = ast(expression)
73
+ unbound = node.dependencies(memory)
74
+
75
+ unless unbound.empty?
76
+ raise UnboundVariableError.new(unbound),
77
+ "no value provided for variables: #{unbound.uniq.join(', ')}"
78
+ end
79
+
80
+ node.value(memory)
81
+ end
82
+ end
83
+
84
+ private def evaluate_array!(expression, data = {}, &block)
85
+ expression.map { |e| evaluate!(e, data, &block) }
86
+ end
87
+
88
+ def solve!(expression_hash)
89
+ BulkExpressionSolver.new(expression_hash, self).solve!
90
+ end
91
+
92
+ def solve(expression_hash, &block)
93
+ BulkExpressionSolver.new(expression_hash, self).solve(&block)
94
+ end
95
+
96
+ def dependencies(expression, context = {})
97
+ test_context = context.nil? ? {} : store(context) { memory }
98
+
99
+ case expression
100
+ when Dentaku::AST::Node
101
+ expression.dependencies(test_context)
102
+ when Array
103
+ expression.flat_map { |e| dependencies(e, context) }
104
+ else
105
+ ast(expression).dependencies(test_context)
106
+ end
107
+ end
108
+
109
+ def ast(expression)
110
+ return expression if expression.is_a?(AST::Node)
111
+ return expression.map { |e| ast(e) } if expression.is_a? Array
112
+
113
+ @ast_cache.fetch(expression) {
114
+ options = {
115
+ aliases: aliases,
116
+ case_sensitive: case_sensitive,
117
+ function_registry: @function_registry,
118
+ raw_date_literals: raw_date_literals
119
+ }
120
+
121
+ tokens = tokenizer.tokenize(expression, options)
122
+ Parser.new(tokens, options).parse.tap do |node|
123
+ @ast_cache[expression] = node if cache_ast?
124
+ end
125
+ }
126
+ end
127
+
128
+ def load_cache(ast_cache)
129
+ @ast_cache = ast_cache
130
+ end
131
+
132
+ def clear_cache(pattern = :all)
133
+ case pattern
134
+ when :all
135
+ @ast_cache = {}
136
+ when String
137
+ @ast_cache.delete(pattern)
138
+ when Regexp
139
+ @ast_cache.delete_if { |k, _| k =~ pattern }
140
+ else
141
+ raise ::ArgumentError
142
+ end
143
+ end
144
+
145
+ def evaluation_context(data, evaluation_mode)
146
+ data.key?(:__evaluation_mode) ? data : data.merge(__evaluation_mode: evaluation_mode)
147
+ end
148
+
149
+ def store(key_or_hash, value = nil)
150
+ restore = Hash[memory]
151
+
152
+ if value.nil?
153
+ key_or_hash = FlatHash.from_hash_with_intermediates(key_or_hash) if nested_data_support
154
+ key_or_hash.each do |key, val|
155
+ memory[standardize_case(key.to_s)] = val
156
+ end
157
+ else
158
+ memory[standardize_case(key_or_hash.to_s)] = value
159
+ end
160
+
161
+ if block_given?
162
+ begin
163
+ result = yield
164
+ @memory = restore
165
+ return result
166
+ rescue => e
167
+ @memory = restore
168
+ raise e
169
+ end
170
+ end
171
+
172
+ self
173
+ end
174
+ alias_method :bind, :store
175
+
176
+ def store_formula(key, formula)
177
+ store(key, ast(formula))
178
+ end
179
+
180
+ def clear
181
+ @memory = {}
182
+ end
183
+
184
+ def empty?
185
+ memory.empty?
186
+ end
187
+
188
+ def cache_ast?
189
+ Dentaku.cache_ast? && !@disable_ast_cache
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,60 @@
1
+ module Dentaku
2
+ class DateArithmetic
3
+ def initialize(date)
4
+ if date.respond_to?(:strftime)
5
+ @base = date
6
+ else
7
+ @base = Time.parse(date).to_datetime
8
+ end
9
+ end
10
+
11
+ def add(duration)
12
+ case duration
13
+ when Numeric
14
+ @base + duration
15
+ when Dentaku::AST::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)
21
+ end
22
+ else
23
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
24
+ "'#{duration || duration.class}' is not coercible for date arithmetic"
25
+ end
26
+ end
27
+
28
+ def sub(duration)
29
+ case duration
30
+ when Date, DateTime, Numeric, Time
31
+ @base - duration
32
+ when Dentaku::AST::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)
38
+ end
39
+ when Dentaku::TokenScanner::DATE_TIME_REGEXP
40
+ @base - Time.parse(duration).to_datetime
41
+ else
42
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
43
+ "'#{duration || duration.class}' is not coercible for date arithmetic"
44
+ end
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
59
+ end
60
+ end
@@ -0,0 +1,29 @@
1
+ require 'tsort'
2
+
3
+ module Dentaku
4
+ class DependencyResolver
5
+ include TSort
6
+
7
+ def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
8
+ self.new(vars_to_dependencies_hash).sort
9
+ end
10
+
11
+ def initialize(vars_to_dependencies_hash)
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) }
19
+ end
20
+
21
+ def tsort_each_node(&block)
22
+ @vars_to_deps.each_key(&block)
23
+ end
24
+
25
+ def tsort_each_child(node, &block)
26
+ @vars_to_deps.fetch(node.to_s, []).each(&block)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,116 @@
1
+ module Dentaku
2
+ class Error < StandardError
3
+ attr_accessor :recipient_variable
4
+ end
5
+
6
+ class UnboundVariableError < Error
7
+ attr_reader :unbound_variables
8
+
9
+ def initialize(unbound_variables)
10
+ @unbound_variables = unbound_variables
11
+ end
12
+ end
13
+
14
+ class MathDomainError < Error
15
+ attr_reader :function_name, :args
16
+
17
+ def initialize(function_name, args)
18
+ @function_name = function_name
19
+ @args = args
20
+ end
21
+ end
22
+
23
+ class NodeError < Error
24
+ attr_reader :child, :expect, :actual
25
+
26
+ def initialize(expect, actual, child)
27
+ @expect = Array(expect)
28
+ @actual = actual
29
+ @child = child
30
+ end
31
+ end
32
+
33
+ class ParseError < Error
34
+ attr_reader :reason, :meta
35
+
36
+ def initialize(reason, **meta)
37
+ @reason = reason
38
+ @meta = meta
39
+ end
40
+
41
+ private_class_method :new
42
+
43
+ VALID_REASONS = %i[
44
+ node_invalid too_few_operands too_many_operands undefined_function
45
+ unprocessed_token unknown_case_token unbalanced_bracket
46
+ unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
47
+ invalid_statement
48
+ ].freeze
49
+
50
+ def self.for(reason, **meta)
51
+ unless VALID_REASONS.include?(reason)
52
+ raise ::ArgumentError, "Unhandled #{reason}"
53
+ end
54
+
55
+ new(reason, **meta)
56
+ end
57
+ end
58
+
59
+ class TokenizerError < Error
60
+ attr_reader :reason, :meta
61
+
62
+ def initialize(reason, **meta)
63
+ @reason = reason
64
+ @meta = meta
65
+ end
66
+
67
+ private_class_method :new
68
+
69
+ VALID_REASONS = %i[
70
+ parse_error
71
+ too_many_closing_parentheses
72
+ too_many_opening_parentheses
73
+ unexpected_zero_width_match
74
+ ].freeze
75
+
76
+ def self.for(reason, **meta)
77
+ unless VALID_REASONS.include?(reason)
78
+ raise ::ArgumentError, "Unhandled #{reason}"
79
+ end
80
+
81
+ new(reason, **meta)
82
+ end
83
+ end
84
+
85
+ class ArgumentError < ::ArgumentError
86
+ attr_reader :reason, :meta
87
+ attr_accessor :recipient_variable
88
+
89
+ def initialize(reason, **meta)
90
+ @reason = reason
91
+ @meta = meta
92
+ end
93
+
94
+ private_class_method :new
95
+
96
+ VALID_REASONS = %i[
97
+ incompatible_type
98
+ invalid_operator
99
+ invalid_value
100
+ too_few_arguments
101
+ wrong_number_of_arguments
102
+ ].freeze
103
+
104
+ def self.for(reason, **meta)
105
+ unless VALID_REASONS.include?(reason)
106
+ raise ::ArgumentError, "Unhandled #{reason}"
107
+ end
108
+
109
+ new(reason, **meta)
110
+ end
111
+ end
112
+
113
+ class ZeroDivisionError < ::ZeroDivisionError
114
+ attr_accessor :recipient_variable
115
+ end
116
+ end