keisan 0.3.0 → 0.4.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 +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
data/lib/keisan/ast/builder.rb
CHANGED
@@ -15,8 +15,10 @@ module Keisan
|
|
15
15
|
@components = Array.wrap(components)
|
16
16
|
end
|
17
17
|
|
18
|
-
@nodes =
|
19
|
-
|
18
|
+
@nodes = components_to_basic_nodes(@components)
|
19
|
+
|
20
|
+
# Negative means not an operator
|
21
|
+
@priorities = @nodes.map {|node| node.is_a?(Keisan::Parsing::Operator) ? node.priority : -1}
|
20
22
|
|
21
23
|
consume_operators!
|
22
24
|
|
@@ -35,27 +37,44 @@ module Keisan
|
|
35
37
|
|
36
38
|
private
|
37
39
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
40
|
+
# Array of AST elements, and Parsing operators
|
41
|
+
def components_to_basic_nodes(components)
|
42
|
+
nodes_components = []
|
43
|
+
|
44
|
+
components.each do |component|
|
45
|
+
if nodes_components.empty?
|
46
|
+
nodes_components << [component]
|
47
|
+
else
|
48
|
+
is_operator = [nodes_components.last.last.is_a?(Keisan::Parsing::Operator), component.is_a?(Keisan::Parsing::Operator)]
|
49
|
+
|
50
|
+
if is_operator.first == is_operator.last
|
51
|
+
nodes_components.last << component
|
52
|
+
else
|
53
|
+
nodes_components << [component]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
nodes_components.inject([]) do |nodes, node_or_component_group|
|
59
|
+
if node_or_component_group.first.is_a?(Keisan::Parsing::Operator)
|
60
|
+
node_or_component_group.each do |component|
|
61
|
+
nodes << component
|
62
|
+
end
|
63
|
+
else
|
64
|
+
nodes << node_from_components(node_or_component_group)
|
65
|
+
end
|
66
|
+
|
67
|
+
nodes
|
68
|
+
end
|
44
69
|
end
|
45
70
|
|
46
71
|
def node_from_components(components)
|
47
|
-
|
48
|
-
|
72
|
+
node, postfix_components = *node_postfixes(components)
|
49
73
|
# Apply postfix operators
|
50
74
|
postfix_components.each do |postfix_component|
|
51
75
|
node = apply_postfix_component_to_node(postfix_component, node)
|
52
76
|
end
|
53
77
|
|
54
|
-
# Apply prefix unary operators
|
55
|
-
unary_components.reverse.each do |unary_component|
|
56
|
-
node = unary_component.node_class.new(node)
|
57
|
-
end
|
58
|
-
|
59
78
|
node
|
60
79
|
end
|
61
80
|
|
@@ -69,16 +88,16 @@ module Keisan
|
|
69
88
|
}
|
70
89
|
)
|
71
90
|
when Keisan::Parsing::DotWord
|
72
|
-
Keisan::AST::Function.
|
73
|
-
|
74
|
-
|
91
|
+
Keisan::AST::Function.new(
|
92
|
+
[node],
|
93
|
+
postfix_component.name
|
75
94
|
)
|
76
95
|
when Keisan::Parsing::DotOperator
|
77
|
-
Keisan::AST::Function.
|
78
|
-
postfix_component.name,
|
96
|
+
Keisan::AST::Function.new(
|
79
97
|
[node] + postfix_component.arguments.map {|parsing_argument|
|
80
98
|
Builder.new(components: parsing_argument.components).node
|
81
|
-
}
|
99
|
+
},
|
100
|
+
postfix_component.name
|
82
101
|
)
|
83
102
|
else
|
84
103
|
raise Keisan::Exceptions::ASTError.new("Invalid postfix component #{postfix_component}")
|
@@ -86,17 +105,10 @@ module Keisan
|
|
86
105
|
end
|
87
106
|
|
88
107
|
# Returns an array of the form
|
89
|
-
# [
|
90
|
-
# unary_operators is an array of Keisan::Parsing::UnaryOperator objects
|
108
|
+
# [node, postfix_operators]
|
91
109
|
# middle_node is the main node which will be modified by prefix and postfix operators
|
92
110
|
# postfix_operators is an array of Keisan::Parsing::Indexing, DotWord, and DotOperator objects
|
93
|
-
def
|
94
|
-
index_of_unary_components = components.map.with_index {|c,i| [c,i]}.select {|c,i| c.is_a?(Keisan::Parsing::UnaryOperator)}.map(&:last)
|
95
|
-
# Must be all in the front
|
96
|
-
unless index_of_unary_components.map.with_index.all? {|i,j| i == j}
|
97
|
-
raise Keisan::Exceptions::ASTError.new("unary operators must be in front")
|
98
|
-
end
|
99
|
-
|
111
|
+
def node_postfixes(components)
|
100
112
|
index_of_postfix_components = components.map.with_index {|c,i| [c,i]}.select {|c,i|
|
101
113
|
c.is_a?(Keisan::Parsing::Indexing) || c.is_a?(Keisan::Parsing::DotWord) || c.is_a?(Keisan::Parsing::DotOperator)
|
102
114
|
}.map(&:last)
|
@@ -104,16 +116,14 @@ module Keisan
|
|
104
116
|
raise Keisan::Exceptions::ASTError.new("postfix components must be in back")
|
105
117
|
end
|
106
118
|
|
107
|
-
num_unary = index_of_unary_components.size
|
108
119
|
num_postfix = index_of_postfix_components.size
|
109
120
|
|
110
|
-
unless
|
121
|
+
unless num_postfix + 1 == components.size
|
111
122
|
raise Keisan::Exceptions::ASTError.new("have too many components")
|
112
123
|
end
|
113
124
|
|
114
125
|
[
|
115
|
-
|
116
|
-
node_of_component(components[index_of_unary_components.size]),
|
126
|
+
node_of_component(components[0]),
|
117
127
|
index_of_postfix_components.map {|i| components[i]}
|
118
128
|
]
|
119
129
|
end
|
@@ -139,23 +149,23 @@ module Keisan
|
|
139
149
|
when Keisan::Parsing::Group
|
140
150
|
Builder.new(components: component.components).node
|
141
151
|
when Keisan::Parsing::Function
|
142
|
-
Keisan::AST::Function.
|
143
|
-
component.name,
|
152
|
+
Keisan::AST::Function.new(
|
144
153
|
component.arguments.map {|parsing_argument|
|
145
154
|
Builder.new(components: parsing_argument.components).node
|
146
|
-
}
|
155
|
+
},
|
156
|
+
component.name
|
147
157
|
)
|
148
158
|
when Keisan::Parsing::DotWord
|
149
|
-
Keisan::AST::Function.
|
150
|
-
component.
|
151
|
-
|
159
|
+
Keisan::AST::Function.new(
|
160
|
+
[node_of_component(component.target)],
|
161
|
+
component.name
|
152
162
|
)
|
153
163
|
when Keisan::Parsing::DotOperator
|
154
|
-
Keisan::AST::Function.
|
155
|
-
component.name,
|
164
|
+
Keisan::AST::Function.new(
|
156
165
|
[node_of_component(component.target)] + component.arguments.map {|parsing_argument|
|
157
166
|
Builder.new(components: parsing_argument.components).node
|
158
|
-
}
|
167
|
+
},
|
168
|
+
component.name
|
159
169
|
)
|
160
170
|
else
|
161
171
|
raise Keisan::Exceptions::ASTError.new("Unhandled component, #{component}")
|
@@ -163,32 +173,57 @@ module Keisan
|
|
163
173
|
end
|
164
174
|
|
165
175
|
def consume_operators!
|
166
|
-
|
167
|
-
|
168
|
-
max_priority = priorities.
|
176
|
+
loop do
|
177
|
+
break if @priorities.empty?
|
178
|
+
max_priority = @priorities.max
|
179
|
+
break if max_priority < 0
|
180
|
+
|
169
181
|
consume_operators_with_priority!(max_priority)
|
170
182
|
end
|
171
183
|
end
|
172
184
|
|
173
185
|
def consume_operators_with_priority!(priority)
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
replacement_node = next_operator_group.first.first.node_class.new(
|
185
|
-
children = @nodes[first_index..(last_index+1)],
|
186
|
-
parsing_operators = @operators[first_index..last_index]
|
187
|
-
)
|
186
|
+
p_indexes = @priorities.map.with_index.select {|p,index| p == priority}
|
187
|
+
# :left, :right, or :none
|
188
|
+
associativity = AST::Operator.associativity_of_priority(priority)
|
189
|
+
|
190
|
+
if associativity == :right
|
191
|
+
index = p_indexes[-1][1]
|
192
|
+
else
|
193
|
+
index = p_indexes[0][1]
|
194
|
+
end
|
188
195
|
|
189
|
-
|
190
|
-
|
191
|
-
|
196
|
+
operator = @nodes[index]
|
197
|
+
|
198
|
+
# If has unary operators after, must process those first
|
199
|
+
if @nodes[index+1].is_a?(Keisan::Parsing::UnaryOperator)
|
200
|
+
loop do
|
201
|
+
break if !@nodes[index+1].is_a?(Keisan::Parsing::UnaryOperator)
|
202
|
+
index += 1
|
203
|
+
end
|
204
|
+
operator = @nodes[index]
|
205
|
+
end
|
206
|
+
|
207
|
+
# operator is the current operator to process, and index is its index
|
208
|
+
if operator.is_a?(Keisan::Parsing::UnaryOperator)
|
209
|
+
replacement_node = operator.node_class.new(
|
210
|
+
children = [@nodes[index+1]]
|
211
|
+
)
|
212
|
+
@nodes.delete_if.with_index {|node, i| i >= index && i <= index+1}
|
213
|
+
@priorities.delete_if.with_index {|node, i| i >= index && i <= index+1}
|
214
|
+
@nodes.insert(index, replacement_node)
|
215
|
+
@priorities.insert(index, -1)
|
216
|
+
elsif operator.is_a?(Keisan::Parsing::Operator)
|
217
|
+
replacement_node = operator.node_class.new(
|
218
|
+
children = [@nodes[index-1],@nodes[index+1]],
|
219
|
+
parsing_operators = [operator]
|
220
|
+
)
|
221
|
+
@nodes.delete_if.with_index {|node, i| i >= index-1 && i <= index+1}
|
222
|
+
@priorities.delete_if.with_index {|node, i| i >= index-1 && i <= index+1}
|
223
|
+
@nodes.insert(index-1, replacement_node)
|
224
|
+
@priorities.insert(index-1, -1)
|
225
|
+
else
|
226
|
+
raise Keisan::Exceptions::ASTError.new("Can only consume operators")
|
192
227
|
end
|
193
228
|
end
|
194
229
|
end
|
data/lib/keisan/ast/exponent.rb
CHANGED
@@ -1,14 +1,6 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
3
|
class Exponent < ArithmeticOperator
|
4
|
-
def arity
|
5
|
-
(2..2)
|
6
|
-
end
|
7
|
-
|
8
|
-
def associativity
|
9
|
-
:right
|
10
|
-
end
|
11
|
-
|
12
4
|
def self.symbol
|
13
5
|
:**
|
14
6
|
end
|
@@ -16,6 +8,68 @@ module Keisan
|
|
16
8
|
def blank_value
|
17
9
|
1
|
18
10
|
end
|
11
|
+
|
12
|
+
def simplify(context = nil)
|
13
|
+
context ||= Context.new
|
14
|
+
|
15
|
+
super(context)
|
16
|
+
|
17
|
+
# Reduce to basic binary exponents
|
18
|
+
reduced = children.reverse[1..-1].inject(children.last) do |total, child|
|
19
|
+
child ** total
|
20
|
+
end
|
21
|
+
|
22
|
+
unless reduced.is_a?(AST::Exponent)
|
23
|
+
return reduced.simplify(context)
|
24
|
+
end
|
25
|
+
|
26
|
+
if reduced.children.count != 2
|
27
|
+
raise Keisan::Exceptions::InternalError.new("Exponent should be binary")
|
28
|
+
end
|
29
|
+
@children = reduced.children
|
30
|
+
|
31
|
+
if children[1].is_a?(AST::Number) && children[1].value(context) == 1
|
32
|
+
return children[0]
|
33
|
+
end
|
34
|
+
|
35
|
+
if children.all? {|child| child.is_a?(AST::Number)}
|
36
|
+
(children[0] ** children[1]).simplify(context)
|
37
|
+
else
|
38
|
+
self
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def evaluate(context = nil)
|
43
|
+
children.reverse[1..-1].inject(children.last.evaluate(context)) {|total, child| child.evaluate(context) ** total}
|
44
|
+
end
|
45
|
+
|
46
|
+
# d ( a^b ) = d ( exp(b log(a)) ) = a^b * d (b log(a))
|
47
|
+
# = a^b * [ db * log(a) + da * b/a] = da b a^(b-1) + db log(a) a^b
|
48
|
+
def differentiate(variable, context = nil)
|
49
|
+
context ||= Context.new
|
50
|
+
|
51
|
+
# Reduce to binary form
|
52
|
+
unless children.count == 2
|
53
|
+
return simplify(context).differentiate(variable, context)
|
54
|
+
end
|
55
|
+
|
56
|
+
base = children[0].simplified(context)
|
57
|
+
exponent = children[1].simplified(context)
|
58
|
+
|
59
|
+
# Special simple case where exponent is a pure number
|
60
|
+
if exponent.is_a?(AST::Number)
|
61
|
+
return (exponent * base ** (exponent -1)).simplify(context)
|
62
|
+
end
|
63
|
+
|
64
|
+
base_diff = base.differentiate(variable, context)
|
65
|
+
exponent_diff = exponent.differentiate(variable, context)
|
66
|
+
|
67
|
+
res = base ** exponent * (
|
68
|
+
exponent_diff * AST::Function.new([base], "log") +
|
69
|
+
base_diff * exponent / base
|
70
|
+
)
|
71
|
+
res.simplify(context)
|
72
|
+
end
|
19
73
|
end
|
20
74
|
end
|
21
75
|
end
|
data/lib/keisan/ast/function.rb
CHANGED
@@ -9,10 +9,8 @@ module Keisan
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def value(context = nil)
|
12
|
-
context
|
13
|
-
|
14
|
-
function = function_from_context(context)
|
15
|
-
function.call(context, *argument_values)
|
12
|
+
context ||= Keisan::Context.new
|
13
|
+
function_from_context(context).value(self, context)
|
16
14
|
end
|
17
15
|
|
18
16
|
def unbound_functions(context = nil)
|
@@ -25,43 +23,56 @@ module Keisan
|
|
25
23
|
context.has_function?(name) ? functions : functions | Set.new([name])
|
26
24
|
end
|
27
25
|
|
26
|
+
def function_defined?(context = nil)
|
27
|
+
context ||= Keisan::Context.new
|
28
|
+
context.has_function?(name)
|
29
|
+
end
|
30
|
+
|
28
31
|
def function_from_context(context)
|
29
|
-
|
32
|
+
context.function(name)
|
30
33
|
end
|
31
|
-
end
|
32
34
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
def ==(other)
|
36
|
+
case other
|
37
|
+
when AST::Function
|
38
|
+
name == other.name && super
|
39
|
+
else
|
40
|
+
false
|
37
41
|
end
|
42
|
+
end
|
38
43
|
|
39
|
-
|
44
|
+
def evaluate(context = nil)
|
45
|
+
context ||= Keisan::Context.new
|
40
46
|
|
41
|
-
if
|
42
|
-
|
47
|
+
if function_defined?(context)
|
48
|
+
function_from_context(context).evaluate(self, context)
|
43
49
|
else
|
44
|
-
children
|
50
|
+
@children = children.map {|child| child.evaluate(context)}
|
51
|
+
self
|
45
52
|
end
|
46
53
|
end
|
47
54
|
|
48
|
-
def
|
49
|
-
context ||=
|
55
|
+
def simplify(context = nil)
|
56
|
+
context ||= Context.new
|
50
57
|
|
51
|
-
|
52
|
-
|
58
|
+
if function_defined?(context)
|
59
|
+
function_from_context(context).simplify(self, context)
|
60
|
+
else
|
61
|
+
@children = children.map {|child| child.simplify(context)}
|
62
|
+
self
|
53
63
|
end
|
54
64
|
end
|
55
|
-
end
|
56
65
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
66
|
+
def to_s
|
67
|
+
"#{name}(#{children.map(&:to_s).join(',')})"
|
68
|
+
end
|
69
|
+
|
70
|
+
def differentiate(variable, context = nil)
|
71
|
+
unless unbound_variables(context).include?(variable.name)
|
72
|
+
return AST::Number.new(0)
|
64
73
|
end
|
74
|
+
# Do not know how to differentiate a function in general, so leave as derivative
|
75
|
+
AST::Function.new([self, variable], "diff")
|
65
76
|
end
|
66
77
|
end
|
67
78
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
module Functions
|
4
|
+
class Diff < AST::Function
|
5
|
+
def initialize(arguments, name = "diff")
|
6
|
+
super(arguments, name)
|
7
|
+
end
|
8
|
+
|
9
|
+
def value(context = nil)
|
10
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Derivative not defined")
|
11
|
+
end
|
12
|
+
|
13
|
+
def unbound_functions(context = nil)
|
14
|
+
context ||= Keisan::Context.new
|
15
|
+
|
16
|
+
children.inject(Set.new) do |res, child|
|
17
|
+
res | child.unbound_functions(context)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def simplify(context = nil)
|
22
|
+
unless children.size > 0
|
23
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Diff requires at least one argument")
|
24
|
+
end
|
25
|
+
|
26
|
+
vars = children[1..-1]
|
27
|
+
|
28
|
+
unless vars.all? {|var| var.is_a?(AST::Variable)}
|
29
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Diff must differentiate with respect to variables")
|
30
|
+
end
|
31
|
+
|
32
|
+
result = children.first.simplify(context)
|
33
|
+
|
34
|
+
while vars.size > 0
|
35
|
+
begin
|
36
|
+
var = vars.first
|
37
|
+
if result.unbound_variables(context).include?(var.name)
|
38
|
+
result = result.differentiate(var, context)
|
39
|
+
else
|
40
|
+
return AST::Number.new(0)
|
41
|
+
end
|
42
|
+
rescue Keisan::Exceptions::NonDifferentiableError => e
|
43
|
+
return AST::Functions::Diff.new(
|
44
|
+
[result] + vars,
|
45
|
+
"diff"
|
46
|
+
)
|
47
|
+
end
|
48
|
+
|
49
|
+
vars.shift
|
50
|
+
end
|
51
|
+
|
52
|
+
result
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|