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 +4 -4
- data/README.md +30 -0
- data/lib/keisan.rb +1 -0
- data/lib/keisan/ast/cache.rb +31 -0
- data/lib/keisan/ast/operator.rb +0 -2
- data/lib/keisan/ast/plus.rb +3 -3
- data/lib/keisan/ast/times.rb +3 -3
- data/lib/keisan/ast/unary_logical_not.rb +26 -0
- data/lib/keisan/calculator.rb +14 -4
- data/lib/keisan/evaluator.rb +8 -7
- data/lib/keisan/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c77b933e609cf2ed878939973a183082fa01b87ac49a8bece5278c0df5e66aed
|
4
|
+
data.tar.gz: e737e883ba1fadfa8c9c9ab19f6b995e4cc33ab4f5f2f7f7750bcd0091dd9cfc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
@@ -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
|
data/lib/keisan/ast/operator.rb
CHANGED
data/lib/keisan/ast/plus.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/keisan/ast/times.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/keisan/calculator.rb
CHANGED
@@ -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)
|
data/lib/keisan/evaluator.rb
CHANGED
@@ -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.
|
29
|
+
ast.simplified(context)
|
29
30
|
end
|
30
31
|
|
31
32
|
def parse_ast(expression)
|
32
|
-
AST.parse(expression)
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
data/lib/keisan/version.rb
CHANGED
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.
|
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-
|
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
|