keisan 0.3.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/README.md +57 -9
- data/lib/keisan.rb +21 -12
- data/lib/keisan/ast.rb +50 -0
- data/lib/keisan/ast/arithmetic_operator.rb +0 -3
- data/lib/keisan/ast/assignment.rb +72 -0
- data/lib/keisan/ast/bitwise_and.rb +4 -4
- data/lib/keisan/ast/bitwise_operator.rb +0 -3
- data/lib/keisan/ast/bitwise_or.rb +4 -4
- data/lib/keisan/ast/bitwise_xor.rb +4 -4
- data/lib/keisan/ast/boolean.rb +25 -1
- data/lib/keisan/ast/builder.rb +98 -63
- data/lib/keisan/ast/constant_literal.rb +13 -0
- data/lib/keisan/ast/exponent.rb +62 -8
- data/lib/keisan/ast/function.rb +37 -26
- data/lib/keisan/ast/functions/diff.rb +57 -0
- data/lib/keisan/ast/functions/if.rb +47 -0
- data/lib/keisan/ast/indexing.rb +44 -4
- data/lib/keisan/ast/list.rb +4 -0
- data/lib/keisan/ast/literal.rb +8 -0
- data/lib/keisan/ast/logical_and.rb +9 -4
- data/lib/keisan/ast/logical_equal.rb +4 -4
- data/lib/keisan/ast/logical_greater_than.rb +4 -4
- data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +4 -4
- data/lib/keisan/ast/logical_less_than.rb +4 -4
- data/lib/keisan/ast/logical_less_than_or_equal_to.rb +4 -4
- data/lib/keisan/ast/logical_not_equal.rb +4 -4
- data/lib/keisan/ast/logical_operator.rb +0 -3
- data/lib/keisan/ast/logical_or.rb +10 -5
- data/lib/keisan/ast/modulo.rb +4 -4
- data/lib/keisan/ast/node.rb +132 -20
- data/lib/keisan/ast/null.rb +1 -1
- data/lib/keisan/ast/number.rb +172 -1
- data/lib/keisan/ast/operator.rb +66 -8
- data/lib/keisan/ast/parent.rb +50 -0
- data/lib/keisan/ast/plus.rb +38 -4
- data/lib/keisan/ast/string.rb +10 -1
- data/lib/keisan/ast/times.rb +47 -4
- data/lib/keisan/ast/unary_bitwise_not.rb +9 -1
- data/lib/keisan/ast/unary_identity.rb +18 -1
- data/lib/keisan/ast/unary_inverse.rb +35 -1
- data/lib/keisan/ast/unary_logical_not.rb +5 -1
- data/lib/keisan/ast/unary_minus.rb +31 -1
- data/lib/keisan/ast/unary_operator.rb +29 -1
- data/lib/keisan/ast/unary_plus.rb +14 -1
- data/lib/keisan/ast/variable.rb +44 -0
- data/lib/keisan/calculator.rb +2 -2
- data/lib/keisan/context.rb +32 -16
- data/lib/keisan/evaluator.rb +10 -65
- data/lib/keisan/exceptions.rb +2 -0
- data/lib/keisan/function.rb +11 -5
- data/lib/keisan/function_definition_context.rb +34 -0
- data/lib/keisan/functions/default_registry.rb +13 -5
- data/lib/keisan/functions/diff.rb +82 -0
- data/lib/keisan/functions/expression_function.rb +63 -0
- data/lib/keisan/functions/if.rb +62 -0
- data/lib/keisan/functions/proc_function.rb +52 -0
- data/lib/keisan/functions/rand.rb +1 -1
- data/lib/keisan/functions/registry.rb +20 -10
- data/lib/keisan/functions/replace.rb +49 -0
- data/lib/keisan/functions/sample.rb +1 -1
- data/lib/keisan/parser.rb +13 -1
- data/lib/keisan/parsing/assignment.rb +9 -0
- data/lib/keisan/parsing/operator.rb +8 -0
- data/lib/keisan/parsing/unary_operator.rb +1 -1
- data/lib/keisan/tokenizer.rb +1 -0
- data/lib/keisan/tokens/assignment.rb +15 -0
- data/lib/keisan/variables/default_registry.rb +4 -4
- data/lib/keisan/variables/registry.rb +17 -8
- data/lib/keisan/version.rb +1 -1
- metadata +15 -3
- data/lib/keisan/ast/priorities.rb +0 -27
@@ -2,7 +2,15 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryBitwiseNot < UnaryOperator
|
4
4
|
def value(context = nil)
|
5
|
-
return ~
|
5
|
+
return ~child.value(context)
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate(context = nil)
|
9
|
+
~child.evaluate(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.symbol
|
13
|
+
:"~"
|
6
14
|
end
|
7
15
|
end
|
8
16
|
end
|
@@ -2,7 +2,24 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryIdentity < UnaryOperator
|
4
4
|
def value(context = nil)
|
5
|
-
return
|
5
|
+
return child.value(context)
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate(context = nil)
|
9
|
+
child.evaluate(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.symbol
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
|
16
|
+
def simplify(context = nil)
|
17
|
+
child.simplify(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def differentiate(variable, context = nil)
|
21
|
+
context ||= Context.new
|
22
|
+
child.differentiate(variable, context).simplify(context)
|
6
23
|
end
|
7
24
|
end
|
8
25
|
end
|
@@ -2,7 +2,41 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryInverse < UnaryOperator
|
4
4
|
def value(context = nil)
|
5
|
-
return Rational(1,
|
5
|
+
return Rational(1, child.value(context))
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
"(#{child.to_s})**(-1)"
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(context = nil)
|
13
|
+
1.to_node / child.evaluate(context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def simplify(context = nil)
|
17
|
+
context ||= Context.new
|
18
|
+
|
19
|
+
@children = [child.simplify(context)]
|
20
|
+
case child
|
21
|
+
when AST::Number
|
22
|
+
AST::Number.new(Rational(1,child.value(context))).simplify(context)
|
23
|
+
else
|
24
|
+
(child ** -1).simplify(context)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def differentiate(variable, context = nil)
|
29
|
+
context ||= Context.new
|
30
|
+
AST::Times.new(
|
31
|
+
[
|
32
|
+
AST::UnaryMinus.new(child.differentiate(variable, context)),
|
33
|
+
AST::UnaryInverse.new(
|
34
|
+
AST::Exponent.new([
|
35
|
+
child.deep_dup, AST::Number.new(2)
|
36
|
+
])
|
37
|
+
)
|
38
|
+
]
|
39
|
+
).simplify(context)
|
6
40
|
end
|
7
41
|
end
|
8
42
|
end
|
@@ -2,7 +2,37 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryMinus < UnaryOperator
|
4
4
|
def value(context = nil)
|
5
|
-
return -1 *
|
5
|
+
return -1 * child.value(context)
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate(context = nil)
|
9
|
+
-child.evaluate(context)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.symbol
|
13
|
+
:"-"
|
14
|
+
end
|
15
|
+
|
16
|
+
def simplify(context = nil)
|
17
|
+
context ||= Context.new
|
18
|
+
|
19
|
+
case child
|
20
|
+
when AST::Number
|
21
|
+
AST::Number.new(-child.value(context)).simplify(context)
|
22
|
+
else
|
23
|
+
AST::Times.new([
|
24
|
+
AST::Number.new(-1),
|
25
|
+
child
|
26
|
+
]).simplify(context)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def differentiate(variable, context = nil)
|
31
|
+
context ||= Context.new
|
32
|
+
AST::Times.new([
|
33
|
+
-1.to_node,
|
34
|
+
child.differentiate(variable, context)
|
35
|
+
]).simplify(context)
|
6
36
|
end
|
7
37
|
end
|
8
38
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
|
-
class UnaryOperator <
|
3
|
+
class UnaryOperator < Operator
|
4
4
|
def initialize(children = [])
|
5
5
|
children = Array.wrap(children)
|
6
6
|
super
|
@@ -8,6 +8,34 @@ module Keisan
|
|
8
8
|
raise Keisan::Exceptions::ASTError.new("Unary operator takes has a single child")
|
9
9
|
end
|
10
10
|
end
|
11
|
+
|
12
|
+
def child
|
13
|
+
children.first
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.arity
|
17
|
+
ARITIES[:"u#{symbol}"]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.priority
|
21
|
+
PRIORITIES[:"u#{symbol}"]
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.associativity
|
25
|
+
ASSOCIATIVITIES[:"u#{symbol}"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def to_s
|
29
|
+
if child.is_a?(AST::Operator)
|
30
|
+
"#{symbol.to_s}(#{child.to_s})"
|
31
|
+
else
|
32
|
+
"#{symbol.to_s}#{child.to_s}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def simplify(context = nil)
|
37
|
+
self
|
38
|
+
end
|
11
39
|
end
|
12
40
|
end
|
13
41
|
end
|
@@ -1,9 +1,22 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
|
-
class UnaryPlus <
|
3
|
+
class UnaryPlus < UnaryIdentity
|
4
4
|
def value(context = nil)
|
5
5
|
return children.first.value(context)
|
6
6
|
end
|
7
|
+
|
8
|
+
def self.symbol
|
9
|
+
:"+"
|
10
|
+
end
|
11
|
+
|
12
|
+
def simplify(context = nil)
|
13
|
+
case child
|
14
|
+
when AST::Number
|
15
|
+
AST::Number.new(child.value(context)).simplify(context)
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
7
20
|
end
|
8
21
|
end
|
9
22
|
end
|
data/lib/keisan/ast/variable.rb
CHANGED
@@ -16,6 +16,50 @@ module Keisan
|
|
16
16
|
context ||= Keisan::Context.new
|
17
17
|
context.has_variable?(name) ? Set.new : Set.new([name])
|
18
18
|
end
|
19
|
+
|
20
|
+
def ==(other)
|
21
|
+
name == other.name
|
22
|
+
end
|
23
|
+
|
24
|
+
def to_s
|
25
|
+
name.to_s
|
26
|
+
end
|
27
|
+
|
28
|
+
def evaluate(context = nil)
|
29
|
+
context ||= Keisan::Context.new
|
30
|
+
if context.has_variable?(name)
|
31
|
+
context.variable(name).to_node.evaluate(context)
|
32
|
+
else
|
33
|
+
self
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def simplify(context = nil)
|
38
|
+
context ||= Keisan::Context.new
|
39
|
+
if context.has_variable?(name)
|
40
|
+
context.variable(name).to_node.simplify(context)
|
41
|
+
else
|
42
|
+
self
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def replace(variable, replacement)
|
47
|
+
if name == variable.name
|
48
|
+
replacement
|
49
|
+
else
|
50
|
+
self
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def differentiate(variable, context = nil)
|
55
|
+
context ||= Keisan::Context.new
|
56
|
+
|
57
|
+
if name == variable.name && !context.has_variable?(name)
|
58
|
+
1.to_node
|
59
|
+
else
|
60
|
+
0.to_node
|
61
|
+
end
|
62
|
+
end
|
19
63
|
end
|
20
64
|
end
|
21
65
|
end
|
data/lib/keisan/calculator.rb
CHANGED
@@ -2,8 +2,8 @@ module Keisan
|
|
2
2
|
class Calculator
|
3
3
|
attr_reader :context
|
4
4
|
|
5
|
-
def initialize(context
|
6
|
-
@context = context || Context.new
|
5
|
+
def initialize(context: nil, allow_recursive: false)
|
6
|
+
@context = context || Context.new(allow_recursive: allow_recursive)
|
7
7
|
end
|
8
8
|
|
9
9
|
def evaluate(expression, definitions = {})
|
data/lib/keisan/context.rb
CHANGED
@@ -1,16 +1,17 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Context
|
3
|
-
attr_reader :function_registry, :variable_registry
|
3
|
+
attr_reader :function_registry, :variable_registry, :allow_recursive
|
4
4
|
|
5
|
-
def initialize(parent: nil, random: nil)
|
5
|
+
def initialize(parent: nil, random: nil, allow_recursive: false)
|
6
6
|
@parent = parent
|
7
7
|
@function_registry = Functions::Registry.new(parent: @parent.try(:function_registry))
|
8
8
|
@variable_registry = Variables::Registry.new(parent: @parent.try(:variable_registry))
|
9
9
|
@random = random
|
10
|
+
@allow_recursive = allow_recursive
|
10
11
|
end
|
11
12
|
|
12
|
-
def spawn_child(definitions
|
13
|
-
child =
|
13
|
+
def spawn_child(definitions: {}, transient: false)
|
14
|
+
child = Context.new(parent: self, allow_recursive: allow_recursive)
|
14
15
|
|
15
16
|
definitions.each do |name, value|
|
16
17
|
case value
|
@@ -21,35 +22,50 @@ module Keisan
|
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
25
|
+
child.set_transient! if transient
|
24
26
|
child
|
25
27
|
end
|
26
28
|
|
27
|
-
def
|
28
|
-
@
|
29
|
+
def variable(name)
|
30
|
+
@variable_registry[name.to_s]
|
29
31
|
end
|
30
32
|
|
31
|
-
def
|
32
|
-
@
|
33
|
+
def has_variable?(name)
|
34
|
+
@variable_registry.has?(name)
|
33
35
|
end
|
34
36
|
|
35
|
-
def
|
36
|
-
@
|
37
|
+
def register_variable!(name, value)
|
38
|
+
if @transient
|
39
|
+
@parent.register_variable!(name, value)
|
40
|
+
else
|
41
|
+
@variable_registry.register!(name.to_s, value)
|
42
|
+
end
|
37
43
|
end
|
38
44
|
|
39
|
-
def
|
40
|
-
@
|
45
|
+
def function(name)
|
46
|
+
@function_registry[name.to_s]
|
41
47
|
end
|
42
48
|
|
43
|
-
def
|
44
|
-
@
|
49
|
+
def has_function?(name)
|
50
|
+
@function_registry.has?(name)
|
45
51
|
end
|
46
52
|
|
47
|
-
def
|
48
|
-
@
|
53
|
+
def register_function!(name, function)
|
54
|
+
if @transient
|
55
|
+
@parent.register_function!(name, function)
|
56
|
+
else
|
57
|
+
@function_registry.register!(name.to_s, function)
|
58
|
+
end
|
49
59
|
end
|
50
60
|
|
51
61
|
def random
|
52
62
|
@random || @parent.try(:random) || Random.new
|
53
63
|
end
|
64
|
+
|
65
|
+
protected
|
66
|
+
|
67
|
+
def set_transient!
|
68
|
+
@transient = true
|
69
|
+
end
|
54
70
|
end
|
55
71
|
end
|
data/lib/keisan/evaluator.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Evaluator
|
3
|
-
WORD = /[a-zA-Z_]\w*/
|
4
|
-
VARIABLE_DEFINITION = /\A\s*(#{WORD})\s*=\s*(.+)\z/
|
5
|
-
FUNCTION_DEFINITION = /\A\s*(#{WORD})\s*\(((?:\s*#{WORD}\s*)(?:,\s*#{WORD}\s*)*)\)\s*=\s*(.+)\z/
|
6
|
-
|
7
3
|
attr_reader :calculator
|
8
4
|
|
9
5
|
def initialize(calculator)
|
@@ -11,69 +7,18 @@ module Keisan
|
|
11
7
|
end
|
12
8
|
|
13
9
|
def evaluate(expression, definitions = {})
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
private
|
25
|
-
|
26
|
-
def variable_evaluate(expression, definitions = {})
|
27
|
-
match = expression.match VARIABLE_DEFINITION
|
28
|
-
name = match[1]
|
29
|
-
expression = match[2]
|
30
|
-
ast = Keisan::AST::Builder.new(string: expression).ast
|
31
|
-
|
32
|
-
context = calculator.context.spawn_child(definitions)
|
33
|
-
unbound_variables = ast.unbound_variables(context)
|
34
|
-
unless unbound_variables <= Set.new([name])
|
35
|
-
raise Keisan::Exceptions::InvalidExpression.new("Unbound variables found in variable definition")
|
36
|
-
end
|
37
|
-
|
38
|
-
calculator.context.register_variable!(name, ast.value(context))
|
39
|
-
end
|
40
|
-
|
41
|
-
def function_evaluate(expression, definitions = {})
|
42
|
-
match = expression.match FUNCTION_DEFINITION
|
43
|
-
name = match[1]
|
44
|
-
args = match[2].split(",").map(&:strip)
|
45
|
-
expression = match[3]
|
46
|
-
ast = Keisan::AST::Builder.new(string: expression).ast
|
47
|
-
|
48
|
-
context = calculator.context.spawn_child(definitions)
|
49
|
-
unbound_variables = ast.unbound_variables(context)
|
50
|
-
unbound_functions = ast.unbound_functions(context)
|
51
|
-
# Ensure the variables are contained within the arguments
|
52
|
-
unless unbound_variables <= Set.new(args) && unbound_functions <= Set.new([name])
|
53
|
-
raise Keisan::Exceptions::InvalidExpression.new("Unbound variables found in function definition")
|
54
|
-
end
|
55
|
-
|
56
|
-
function = lambda do |*received_args|
|
57
|
-
unless args.count == received_args.count
|
58
|
-
raise Keisan::Exceptions::InvalidFunctionError.new("Invalid number of arguments for #{name} function")
|
59
|
-
end
|
60
|
-
|
61
|
-
local = calculator.context.spawn_child(definitions)
|
62
|
-
args.each.with_index do |arg, i|
|
63
|
-
local.register_variable!(arg, received_args[i])
|
10
|
+
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
11
|
+
ast = Keisan::AST.parse(expression)
|
12
|
+
evaluation = ast.evaluate(context)
|
13
|
+
|
14
|
+
case ast
|
15
|
+
when Keisan::AST::Assignment
|
16
|
+
if ast.children.first.is_a?(Keisan::AST::Variable)
|
17
|
+
context.variable(ast.children.first.name)
|
64
18
|
end
|
65
|
-
|
66
|
-
|
19
|
+
else
|
20
|
+
evaluation.value(context)
|
67
21
|
end
|
68
|
-
|
69
|
-
calculator.context.register_function!(
|
70
|
-
name,
|
71
|
-
Keisan::Function.new(name, function)
|
72
|
-
)
|
73
|
-
end
|
74
|
-
|
75
|
-
def pure_evaluate(expression, definitions = {})
|
76
|
-
Keisan::AST::Builder.new(string: expression).ast.value(calculator.context.spawn_child(definitions))
|
77
22
|
end
|
78
23
|
end
|
79
24
|
end
|
data/lib/keisan/exceptions.rb
CHANGED
@@ -16,5 +16,7 @@ module Keisan
|
|
16
16
|
class UndefinedVariableError < StandardError; end
|
17
17
|
class UnmodifiableError < StandardError; end
|
18
18
|
class InvalidExpression < StandardError; end
|
19
|
+
class TypeError < StandardError; end
|
20
|
+
class NonDifferentiableError < StandardError; end
|
19
21
|
end
|
20
22
|
end
|