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
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
|