keisan 0.8.13 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '05596336b8e5f1de3f7787b6a36120152f4e13c8caa822da225fe989f6af3c72'
4
- data.tar.gz: 9a4d79fbc8ed709ec7d0f9b90d0e31f4f3e65c7e43b067800b66a0ea7c568ffb
3
+ metadata.gz: c77b933e609cf2ed878939973a183082fa01b87ac49a8bece5278c0df5e66aed
4
+ data.tar.gz: e737e883ba1fadfa8c9c9ab19f6b995e4cc33ab4f5f2f7f7750bcd0091dd9cfc
5
5
  SHA512:
6
- metadata.gz: d040a4d12325c74a8775d7a87a7d450fd0218de3e639b7ac43fe53bbb1310000cd53207e98727266f59535c5f7347d74392b941b66ad10359d97c515d5e02f75
7
- data.tar.gz: 81ce4fe88b6d215a194bd8cf45388957689353c275cb8e60ec921a26eac8cb86d10c067ed34e09f6f303482f2b913005bc07c3448aed413d291b3beb66ecbf4f
6
+ metadata.gz: 1cbf2ba0b83b1c74b6d70530dbdebe5a6519cff81c90a2e79216fff1ced7d7ded853d88691afbcdcb75b3995349d82c7b02b0cda915cb871c545080b37c25b7f
7
+ data.tar.gz: 7877b72629fe6f90c7d508451dc580f88c8f10b9d14529917fb9235c805d84c55d972188dadd85ffb6acef6a81e728911e60a0012968a66408bb8a600cfaca45
data/README.md CHANGED
@@ -64,6 +64,36 @@ ast.children.map(&:to_s)
64
64
  #=> ["x**2", "1"]
65
65
  ```
66
66
 
67
+ #### Caching AST results
68
+
69
+ Computing the AST from a string takes some non-zero amount of time.
70
+ For applications of this gem that evaluate some set of fixed expressions (possibly with different variable values, but with fixed ASTs), it might be worthwhile to cache the ASTs for faster computation.
71
+ To accomplish this, you can use the `Keisan::AST::Cache` class.
72
+ Passing an instance of this class into the `Calculator` will mean everytime a new expression is encountered it will compute the AST and store it in this cache for retrieval next time the expression is encountered.
73
+
74
+ ```ruby
75
+ cache = Keisan::AST::Cache.new
76
+ # Note: if you don't want to create the Cache instance, you can just pass `cache: true` here as well
77
+ calculator = Keisan::Calculator.new(cache: cache)
78
+ calculator.evaluate("exp(-x/T)", x: 1.0, T: 10)
79
+ #=> 0.9048374180359595
80
+ # This call will use the cached AST for "exp(-x/T)"
81
+ calculator.evaluate("exp(-x/T)", x: 2.0, T: 10)
82
+ #=> 0.8187307530779818
83
+ ```
84
+
85
+ If you just want to pre-populate the cache with some predetermined values, you can call `#fetch_or_build` on the `Cache` for each instance, `freeze` the cache, then use this frozen cache in your calculator.
86
+ A cache that has been frozen will only fetch from the cache, never write new values to it.
87
+
88
+ ```ruby
89
+ cache = Keisan::AST::Cache.new
90
+ cache.fetch_or_build("f(x) + diff(g(x), x)")
91
+ cache.freeze
92
+ # This calculator will never write new values to the cache, but when
93
+ # evaluating `"f(x) + diff(g(x), x)"` will fetch this cached AST.
94
+ calculator = Keisan::Calculator.new(cache: cache)
95
+ ```
96
+
67
97
  ##### Specifying variables
68
98
 
69
99
  Passing in a hash of variable (`name`, `value`) pairs to the `evaluate` method is one way of defining variables
data/lib/keisan.rb CHANGED
@@ -55,6 +55,7 @@ require "keisan/ast/indexing"
55
55
 
56
56
  require "keisan/ast/line_builder"
57
57
  require "keisan/ast/builder"
58
+ require "keisan/ast/cache"
58
59
  require "keisan/ast"
59
60
 
60
61
  require "keisan/function"
@@ -0,0 +1,31 @@
1
+ module Keisan
2
+ module AST
3
+ class Cache
4
+ def initialize
5
+ @cache = {}
6
+ end
7
+
8
+ def fetch_or_build(string)
9
+ return @cache[string] if @cache.has_key?(string)
10
+
11
+ build_from_scratch(string).tap do |ast|
12
+ unless frozen?
13
+ # Freeze the AST to keep it from changing in the cache
14
+ ast.freeze
15
+ @cache[string] = ast
16
+ end
17
+ end
18
+ end
19
+
20
+ def has_key?(string)
21
+ @cache.has_key?(string)
22
+ end
23
+
24
+ private
25
+
26
+ def build_from_scratch(string)
27
+ Builder.new(string: string).ast
28
+ end
29
+ end
30
+ end
31
+ end
@@ -45,8 +45,6 @@ module Keisan
45
45
 
46
46
  children = Array(children)
47
47
  super(children)
48
-
49
- @parsing_operators = parsing_operators
50
48
  end
51
49
 
52
50
  def evaluate_assignments(context = nil)
@@ -3,7 +3,7 @@ module Keisan
3
3
  class Plus < ArithmeticOperator
4
4
  def initialize(children = [], parsing_operators = [])
5
5
  super
6
- convert_minus_to_plus!
6
+ convert_minus_to_plus!(parsing_operators)
7
7
  end
8
8
 
9
9
  def self.symbol
@@ -87,8 +87,8 @@ module Keisan
87
87
  date_time + others.inject(0, &:+)
88
88
  end
89
89
 
90
- def convert_minus_to_plus!
91
- @parsing_operators.each.with_index do |parsing_operator, index|
90
+ def convert_minus_to_plus!(parsing_operators)
91
+ parsing_operators.each.with_index do |parsing_operator, index|
92
92
  if parsing_operator.is_a?(Parsing::Minus)
93
93
  @children[index+1] = UnaryMinus.new(@children[index+1])
94
94
  end
@@ -3,7 +3,7 @@ module Keisan
3
3
  class Times < ArithmeticOperator
4
4
  def initialize(children = [], parsing_operators = [])
5
5
  super
6
- convert_divide_to_inverse!
6
+ convert_divide_to_inverse!(parsing_operators)
7
7
  end
8
8
 
9
9
  def self.symbol
@@ -63,8 +63,8 @@ module Keisan
63
63
 
64
64
  private
65
65
 
66
- def convert_divide_to_inverse!
67
- @parsing_operators.each.with_index do |parsing_operator, index|
66
+ def convert_divide_to_inverse!(parsing_operators)
67
+ parsing_operators.each.with_index do |parsing_operator, index|
68
68
  if parsing_operator.is_a?(Parsing::Divide)
69
69
  @children[index+1] = UnaryInverse.new(@children[index+1])
70
70
  end
@@ -8,6 +8,32 @@ module Keisan
8
8
  def self.symbol
9
9
  :"!"
10
10
  end
11
+
12
+ def evaluate(context = nil)
13
+ context ||= Context.new
14
+ node = child.evaluate(context).to_node
15
+ case node
16
+ when AST::Boolean
17
+ AST::Boolean.new(!node.value)
18
+ else
19
+ if node.is_constant?
20
+ raise Keisan::Exceptions::InvalidFunctionError.new("Cannot take unary logical not of non-boolean constant")
21
+ else
22
+ super
23
+ end
24
+ end
25
+ end
26
+
27
+ def simplify(context = nil)
28
+ context ||= Context.new
29
+ node = child.simplify(context).to_node
30
+ case node
31
+ when AST::Boolean
32
+ AST::Boolean.new(!node.value)
33
+ else
34
+ super
35
+ end
36
+ end
11
37
  end
12
38
  end
13
39
  end
@@ -4,13 +4,23 @@ module Keisan
4
4
 
5
5
  # Note, allow_recursive would be more appropriately named:
6
6
  # allow_unbound_functions_in_function_definitions, but it is too late for that.
7
- def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true, allow_random: true)
7
+ def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true, allow_random: true, cache: nil)
8
8
  @context = context || Context.new(
9
9
  allow_recursive: allow_recursive,
10
10
  allow_blocks: allow_blocks,
11
11
  allow_multiline: allow_multiline,
12
12
  allow_random: allow_random
13
13
  )
14
+ @cache = case cache
15
+ when nil, false
16
+ nil
17
+ when true
18
+ AST::Cache.new
19
+ when AST::Cache
20
+ cache
21
+ else
22
+ raise Exceptions::StandardError.new("cache must be either nil, false, true, or an instance of Keisan::AST::Cache")
23
+ end
14
24
  end
15
25
 
16
26
  def allow_recursive
@@ -34,15 +44,15 @@ module Keisan
34
44
  end
35
45
 
36
46
  def evaluate(expression, definitions = {})
37
- Evaluator.new(self).evaluate(expression, definitions)
47
+ Evaluator.new(self, cache: @cache).evaluate(expression, definitions)
38
48
  end
39
49
 
40
50
  def simplify(expression, definitions = {})
41
- Evaluator.new(self).simplify(expression, definitions)
51
+ Evaluator.new(self, cache: @cache).simplify(expression, definitions)
42
52
  end
43
53
 
44
54
  def ast(expression)
45
- Evaluator.new(self).parse_ast(expression)
55
+ Evaluator.new(self, cache: @cache).parse_ast(expression)
46
56
  end
47
57
 
48
58
  def define_variable!(name, value)
@@ -2,8 +2,9 @@ module Keisan
2
2
  class Evaluator
3
3
  attr_reader :calculator
4
4
 
5
- def initialize(calculator)
5
+ def initialize(calculator, cache: nil)
6
6
  @calculator = calculator
7
+ @cache = cache
7
8
  end
8
9
 
9
10
  def evaluate(expression, definitions = {})
@@ -25,16 +26,16 @@ module Keisan
25
26
  def simplify(expression, definitions = {})
26
27
  context = calculator.context.spawn_child(definitions: definitions, transient: true)
27
28
  ast = parse_ast(expression)
28
- ast.simplify(context)
29
+ ast.simplified(context)
29
30
  end
30
31
 
31
32
  def parse_ast(expression)
32
- AST.parse(expression).tap do |ast|
33
- disallowed = disallowed_nodes
34
- if !disallowed.empty? && ast.contains_a?(disallowed)
35
- raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with #{disallowed}")
36
- end
33
+ ast = @cache.nil? ? AST.parse(expression) : @cache.fetch_or_build(expression)
34
+ disallowed = disallowed_nodes
35
+ if !disallowed.empty? && ast.contains_a?(disallowed)
36
+ raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with #{disallowed}")
37
37
  end
38
+ ast
38
39
  end
39
40
 
40
41
  private
@@ -1,3 +1,3 @@
1
1
  module Keisan
2
- VERSION = "0.8.13"
2
+ VERSION = "0.9.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: keisan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.13
4
+ version: 0.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christopher Locke
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-05-21 00:00:00.000000000 Z
11
+ date: 2021-05-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmath
@@ -139,6 +139,7 @@ files:
139
139
  - lib/keisan/ast/block.rb
140
140
  - lib/keisan/ast/boolean.rb
141
141
  - lib/keisan/ast/builder.rb
142
+ - lib/keisan/ast/cache.rb
142
143
  - lib/keisan/ast/cell.rb
143
144
  - lib/keisan/ast/cell_assignment.rb
144
145
  - lib/keisan/ast/constant_literal.rb