Class: Dentaku::Calculator
- Inherits:
-
Object
- Object
- Dentaku::Calculator
- Includes:
- StringCasing
- Defined in:
- lib/dentaku/calculator.rb
Instance Attribute Summary collapse
-
#ast_cache ⇒ Object
readonly
Returns the value of attribute ast_cache.
-
#case_sensitive ⇒ Object
readonly
Returns the value of attribute case_sensitive.
-
#memory ⇒ Object
readonly
Returns the value of attribute memory.
-
#nested_data_support ⇒ Object
readonly
Returns the value of attribute nested_data_support.
-
#raw_date_literals ⇒ Object
readonly
Returns the value of attribute raw_date_literals.
-
#result ⇒ Object
readonly
Returns the value of attribute result.
-
#tokenizer ⇒ Object
readonly
Returns the value of attribute tokenizer.
Class Method Summary collapse
- .add_function(name, type, body, callback = nil, volatile: false) ⇒ Object
- .add_functions(functions) ⇒ Object
Instance Method Summary collapse
- #add_function(name, type, body, callback = nil, volatile: false) ⇒ Object
- #add_functions(functions) ⇒ Object
-
#aliases ⇒ Object
explicitly configured aliases win; otherwise the module-level default is resolved lazily so it can be set after this calculator was created.
- #ast(expression) ⇒ Object
- #cache_ast? ⇒ Boolean
- #cache_dependency_order? ⇒ Boolean
- #clear ⇒ Object
- #clear_cache(pattern = :all) ⇒ Object
- #dependencies(expression, context = {}) ⇒ Object
- #disable_cache ⇒ Object
- #empty? ⇒ Boolean
- #evaluate(expression, data = {}, &block) ⇒ Object
- #evaluate!(expression, data = {}, &block) ⇒ Object
- #evaluation_context(data, evaluation_mode) ⇒ Object
-
#identifiers(expression) ⇒ Object
every identifier the expression could reference, regardless of branching: purely syntactic, ignores stored memory, and never evaluates guards or functions.
-
#initialize(case_sensitive: false, aliases: nil, nested_data_support: true, raw_date_literals: true, ast_cache: {}, cache_ast: nil, cache_dependency_order: nil) ⇒ Calculator
constructor
A new instance of Calculator.
- #load_cache(ast_cache) ⇒ Object
- #solve(expression_hash, &block) ⇒ Object
- #solve!(expression_hash) ⇒ Object
- #store(key_or_hash, value = nil) ⇒ Object (also: #bind)
- #store_formula(key, formula) ⇒ Object
Methods included from StringCasing
Constructor Details
#initialize(case_sensitive: false, aliases: nil, nested_data_support: true, raw_date_literals: true, ast_cache: {}, cache_ast: nil, cache_dependency_order: nil) ⇒ Calculator
Returns a new instance of Calculator.
15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/dentaku/calculator.rb', line 15 def initialize(case_sensitive: false, aliases: nil, nested_data_support: true, raw_date_literals: true, ast_cache: {}, cache_ast: nil, cache_dependency_order: nil) clear @tokenizer = Tokenizer.new @case_sensitive = case_sensitive @aliases = aliases @nested_data_support = nested_data_support @raw_date_literals = raw_date_literals @ast_cache = ast_cache @cache_ast = cache_ast @cache_dependency_order = cache_dependency_order @disable_ast_cache = false @function_registry = Dentaku::AST::FunctionRegistry.new end |
Instance Attribute Details
#ast_cache ⇒ Object (readonly)
Returns the value of attribute ast_cache.
12 13 14 |
# File 'lib/dentaku/calculator.rb', line 12 def ast_cache @ast_cache end |
#case_sensitive ⇒ Object (readonly)
Returns the value of attribute case_sensitive.
12 13 14 |
# File 'lib/dentaku/calculator.rb', line 12 def case_sensitive @case_sensitive end |
#memory ⇒ Object (readonly)
Returns the value of attribute memory.
12 13 14 |
# File 'lib/dentaku/calculator.rb', line 12 def memory @memory end |
#nested_data_support ⇒ Object (readonly)
Returns the value of attribute nested_data_support.
12 13 14 |
# File 'lib/dentaku/calculator.rb', line 12 def nested_data_support @nested_data_support end |
#raw_date_literals ⇒ Object (readonly)
Returns the value of attribute raw_date_literals.
12 13 14 |
# File 'lib/dentaku/calculator.rb', line 12 def raw_date_literals @raw_date_literals end |
#result ⇒ Object (readonly)
Returns the value of attribute result.
12 13 14 |
# File 'lib/dentaku/calculator.rb', line 12 def result @result end |
#tokenizer ⇒ Object (readonly)
Returns the value of attribute tokenizer.
12 13 14 |
# File 'lib/dentaku/calculator.rb', line 12 def tokenizer @tokenizer end |
Class Method Details
.add_function(name, type, body, callback = nil, volatile: false) ⇒ Object
37 38 39 |
# File 'lib/dentaku/calculator.rb', line 37 def self.add_function(name, type, body, callback = nil, volatile: false) Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback, volatile: volatile) end |
.add_functions(functions) ⇒ Object
41 42 43 |
# File 'lib/dentaku/calculator.rb', line 41 def self.add_functions(functions) functions.each { |(name, type, body, callback, volatile)| add_function(name, type, body, callback, volatile: !!volatile) } end |
Instance Method Details
#add_function(name, type, body, callback = nil, volatile: false) ⇒ Object
45 46 47 48 |
# File 'lib/dentaku/calculator.rb', line 45 def add_function(name, type, body, callback = nil, volatile: false) @function_registry.register(name, type, body, callback, volatile: volatile) self end |
#add_functions(functions) ⇒ Object
50 51 52 53 |
# File 'lib/dentaku/calculator.rb', line 50 def add_functions(functions) functions.each { |(name, type, body, callback, volatile)| add_function(name, type, body, callback, volatile: !!volatile) } self end |
#aliases ⇒ Object
explicitly configured aliases win; otherwise the module-level default is resolved lazily so it can be set after this calculator was created
33 34 35 |
# File 'lib/dentaku/calculator.rb', line 33 def aliases @aliases || Dentaku.aliases end |
#ast(expression) ⇒ Object
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/dentaku/calculator.rb', line 133 def ast(expression) return expression if expression.is_a?(AST::Node) return expression.map { |e| ast(e) } if expression.is_a? Array @ast_cache.fetch(expression) { = { aliases: aliases, case_sensitive: case_sensitive, function_registry: @function_registry, raw_date_literals: raw_date_literals } tokens = tokenizer.tokenize(expression, ) Parser.new(tokens, ).parse.tap do |node| @ast_cache[expression] = node if cache_ast? end } end |
#cache_ast? ⇒ Boolean
231 232 233 234 235 |
# File 'lib/dentaku/calculator.rb', line 231 def cache_ast? return false if @disable_ast_cache @cache_ast.nil? ? Dentaku.cache_ast? : @cache_ast end |
#cache_dependency_order? ⇒ Boolean
237 238 239 |
# File 'lib/dentaku/calculator.rb', line 237 def cache_dependency_order? @cache_dependency_order.nil? ? Dentaku.cache_dependency_order? : @cache_dependency_order end |
#clear ⇒ Object
223 224 225 |
# File 'lib/dentaku/calculator.rb', line 223 def clear @memory = {} end |
#clear_cache(pattern = :all) ⇒ Object
156 157 158 159 160 161 162 163 164 165 166 167 |
# File 'lib/dentaku/calculator.rb', line 156 def clear_cache(pattern = :all) case pattern when :all @ast_cache = {} when String @ast_cache.delete(pattern) when Regexp @ast_cache.delete_if { |k, _| k =~ pattern } else raise ::ArgumentError end end |
#dependencies(expression, context = {}) ⇒ Object
104 105 106 107 108 109 110 111 112 113 114 115 116 117 |
# File 'lib/dentaku/calculator.rb', line 104 def dependencies(expression, context = {}) # dup inside the block: `store` now restores in place, so the caller # needs its own copy of the merged context rather than the live memory test_context = context.nil? ? {} : store(context) { memory.dup } case expression when Dentaku::AST::Node expression.dependencies(test_context) when Array expression.flat_map { |e| dependencies(e, context) } else ast(expression).dependencies(test_context) end end |
#disable_cache ⇒ Object
55 56 57 58 59 60 |
# File 'lib/dentaku/calculator.rb', line 55 def disable_cache @disable_ast_cache = true yield(self) if block_given? ensure @disable_ast_cache = false end |
#empty? ⇒ Boolean
227 228 229 |
# File 'lib/dentaku/calculator.rb', line 227 def empty? memory.empty? end |
#evaluate(expression, data = {}, &block) ⇒ Object
62 63 64 65 66 67 68 69 |
# File 'lib/dentaku/calculator.rb', line 62 def evaluate(expression, data = {}, &block) context = evaluation_context(data, :permissive) return evaluate_array(expression, context, &block) if expression.is_a?(Array) evaluate!(expression, context) rescue Dentaku::Error => ex block.call(expression, ex) if block_given? end |
#evaluate!(expression, data = {}, &block) ⇒ Object
75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/dentaku/calculator.rb', line 75 def evaluate!(expression, data = {}, &block) context = evaluation_context(data, :strict) return evaluate_array!(expression, context, &block) if expression.is_a? Array store(context) do node = ast(expression) unbound = node.dependencies(memory) unless unbound.empty? raise UnboundVariableError.new(unbound), "no value provided for variables: #{unbound.uniq.join(', ')}" end node.value(memory) end end |
#evaluation_context(data, evaluation_mode) ⇒ Object
169 170 171 |
# File 'lib/dentaku/calculator.rb', line 169 def evaluation_context(data, evaluation_mode) data.key?(:__evaluation_mode) ? data : data.merge(__evaluation_mode: evaluation_mode) end |
#identifiers(expression) ⇒ Object
every identifier the expression could reference, regardless of branching: purely syntactic, ignores stored memory, and never evaluates guards or functions
122 123 124 125 126 127 128 129 130 131 |
# File 'lib/dentaku/calculator.rb', line 122 def identifiers(expression) case expression when Dentaku::AST::Node expression.dependencies(AST::Node::STATIC_CONTEXT).uniq when Array expression.flat_map { |e| identifiers(e) }.uniq else ast(expression).dependencies(AST::Node::STATIC_CONTEXT).uniq end end |
#load_cache(ast_cache) ⇒ Object
152 153 154 |
# File 'lib/dentaku/calculator.rb', line 152 def load_cache(ast_cache) @ast_cache = ast_cache end |
#solve(expression_hash, &block) ⇒ Object
100 101 102 |
# File 'lib/dentaku/calculator.rb', line 100 def solve(expression_hash, &block) BulkExpressionSolver.new(expression_hash, self).solve(&block) end |
#solve!(expression_hash) ⇒ Object
96 97 98 |
# File 'lib/dentaku/calculator.rb', line 96 def solve!(expression_hash) BulkExpressionSolver.new(expression_hash, self).solve! end |
#store(key_or_hash, value = nil) ⇒ Object Also known as: bind
173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 |
# File 'lib/dentaku/calculator.rb', line 173 def store(key_or_hash, value = nil) pairs = pairs_to_store(key_or_hash, value) unless block_given? pairs.each { |key, val| memory[key] = val } return self end # `evaluate!` routes every call through here, so snapshotting the whole # memory hash makes each evaluation scale with the number of stored # variables (#336). Undoing just the keys this call touched is O(pairs) # instead of O(memory) -- but it is only equivalent while nothing else # writes into memory during the block. The identifier cache does exactly # that, so when it is enabled we still need the wholesale snapshot to # keep cached values scoped to a single evaluation. if Dentaku.cache_identifier? restore = Hash[memory] pairs.each { |key, val| memory[key] = val } begin yield ensure @memory = restore end else undo = pairs.map { |key, _| [key, memory.key?(key), memory[key]] } pairs.each { |key, val| memory[key] = val } begin yield ensure undo.each { |key, present, val| present ? memory[key] = val : memory.delete(key) } end end end |
#store_formula(key, formula) ⇒ Object
219 220 221 |
# File 'lib/dentaku/calculator.rb', line 219 def store_formula(key, formula) store(key, ast(formula)) end |