keisan 0.7.0 → 0.8.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +6 -3
- data/README.md +47 -3
- data/keisan.gemspec +5 -5
- data/lib/keisan.rb +9 -3
- data/lib/keisan/ast.rb +25 -0
- data/lib/keisan/ast/bitwise_left_shift.rb +17 -0
- data/lib/keisan/ast/bitwise_right_shift.rb +17 -0
- data/lib/keisan/ast/block.rb +4 -0
- data/lib/keisan/ast/boolean.rb +1 -1
- data/lib/keisan/ast/builder.rb +2 -2
- data/lib/keisan/ast/cell.rb +10 -0
- data/lib/keisan/ast/date.rb +23 -0
- data/lib/keisan/ast/date_time_methods.rb +75 -0
- data/lib/keisan/ast/function.rb +9 -0
- data/lib/keisan/ast/function_assignment.rb +16 -6
- data/lib/keisan/ast/hash.rb +4 -0
- data/lib/keisan/ast/logical_and.rb +20 -3
- data/lib/keisan/ast/logical_equal.rb +6 -5
- data/lib/keisan/ast/logical_greater_than.rb +6 -4
- data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +6 -4
- data/lib/keisan/ast/logical_less_than.rb +6 -4
- data/lib/keisan/ast/logical_less_than_or_equal_to.rb +6 -4
- data/lib/keisan/ast/logical_not_equal.rb +6 -5
- data/lib/keisan/ast/logical_operator.rb +24 -0
- data/lib/keisan/ast/logical_or.rb +18 -1
- data/lib/keisan/ast/node.rb +25 -0
- data/lib/keisan/ast/number.rb +24 -0
- data/lib/keisan/ast/operator.rb +3 -1
- data/lib/keisan/ast/parent.rb +5 -1
- data/lib/keisan/ast/plus.rb +10 -0
- data/lib/keisan/ast/time.rb +23 -0
- data/lib/keisan/ast/unary_inverse.rb +1 -1
- data/lib/keisan/ast/unary_operator.rb +1 -1
- data/lib/keisan/ast/variable.rb +10 -9
- data/lib/keisan/calculator.rb +17 -3
- data/lib/keisan/context.rb +27 -10
- data/lib/keisan/evaluator.rb +16 -4
- data/lib/keisan/exceptions.rb +3 -0
- data/lib/keisan/function.rb +6 -0
- data/lib/keisan/functions/break.rb +11 -0
- data/lib/keisan/functions/cmath_function.rb +3 -1
- data/lib/keisan/functions/continue.rb +11 -0
- data/lib/keisan/functions/default_registry.rb +39 -0
- data/lib/keisan/functions/enumerable_function.rb +10 -2
- data/lib/keisan/functions/expression_function.rb +16 -9
- data/lib/keisan/functions/filter.rb +6 -0
- data/lib/keisan/functions/loop_control_flow_function.rb +22 -0
- data/lib/keisan/functions/map.rb +6 -0
- data/lib/keisan/functions/proc_function.rb +2 -2
- data/lib/keisan/functions/reduce.rb +5 -0
- data/lib/keisan/functions/replace.rb +6 -6
- data/lib/keisan/functions/while.rb +7 -1
- data/lib/keisan/parser.rb +7 -5
- data/lib/keisan/parsing/bitwise_left_shift.rb +9 -0
- data/lib/keisan/parsing/bitwise_right_shift.rb +9 -0
- data/lib/keisan/parsing/function.rb +1 -1
- data/lib/keisan/parsing/hash.rb +2 -2
- data/lib/keisan/string_and_group_parser.rb +229 -0
- data/lib/keisan/token.rb +1 -1
- data/lib/keisan/tokenizer.rb +20 -18
- data/lib/keisan/tokens/assignment.rb +3 -1
- data/lib/keisan/tokens/bitwise_shift.rb +23 -0
- data/lib/keisan/tokens/group.rb +1 -7
- data/lib/keisan/tokens/string.rb +2 -4
- data/lib/keisan/util.rb +19 -0
- data/lib/keisan/variables/default_registry.rb +2 -1
- data/lib/keisan/version.rb +1 -1
- metadata +40 -28
@@ -2,7 +2,7 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryOperator < Operator
|
4
4
|
def initialize(children = [])
|
5
|
-
children = Array
|
5
|
+
children = Array(children)
|
6
6
|
super(children)
|
7
7
|
if children.count != 1
|
8
8
|
raise Exceptions::ASTError.new("Unary operator takes has a single child")
|
data/lib/keisan/ast/variable.rb
CHANGED
@@ -13,7 +13,13 @@ module Keisan
|
|
13
13
|
|
14
14
|
def value(context = nil)
|
15
15
|
context ||= Context.new
|
16
|
-
variable_node_from_context(context)
|
16
|
+
node = variable_node_from_context(context)
|
17
|
+
case node
|
18
|
+
when Variable
|
19
|
+
node
|
20
|
+
else
|
21
|
+
node.value(context)
|
22
|
+
end
|
17
23
|
end
|
18
24
|
|
19
25
|
def unbound_variables(context = nil)
|
@@ -60,11 +66,8 @@ module Keisan
|
|
60
66
|
end
|
61
67
|
|
62
68
|
def replace(variable, replacement)
|
63
|
-
|
64
|
-
|
65
|
-
else
|
66
|
-
self
|
67
|
-
end
|
69
|
+
to_replace_name = variable.is_a?(::String) ? variable : variable.name
|
70
|
+
name == to_replace_name ? replacement : self
|
68
71
|
end
|
69
72
|
|
70
73
|
def differentiate(variable, context = nil)
|
@@ -81,9 +84,7 @@ module Keisan
|
|
81
84
|
|
82
85
|
def variable_node_from_context(context)
|
83
86
|
variable = context.variable(name)
|
84
|
-
if variable.is_a?(Cell)
|
85
|
-
variable = variable.node
|
86
|
-
end
|
87
|
+
variable = variable.node if variable.is_a?(Cell)
|
87
88
|
variable
|
88
89
|
end
|
89
90
|
end
|
data/lib/keisan/calculator.rb
CHANGED
@@ -2,8 +2,14 @@ module Keisan
|
|
2
2
|
class Calculator
|
3
3
|
attr_reader :context
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
# Note, allow_recursive would be more appropriately named:
|
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)
|
8
|
+
@context = context || Context.new(
|
9
|
+
allow_recursive: allow_recursive,
|
10
|
+
allow_blocks: allow_blocks,
|
11
|
+
allow_multiline: allow_multiline
|
12
|
+
)
|
7
13
|
end
|
8
14
|
|
9
15
|
def allow_recursive
|
@@ -14,6 +20,14 @@ module Keisan
|
|
14
20
|
context.allow_recursive!
|
15
21
|
end
|
16
22
|
|
23
|
+
def allow_blocks
|
24
|
+
context.allow_blocks
|
25
|
+
end
|
26
|
+
|
27
|
+
def allow_multiline
|
28
|
+
context.allow_multiline
|
29
|
+
end
|
30
|
+
|
17
31
|
def evaluate(expression, definitions = {})
|
18
32
|
Evaluator.new(self).evaluate(expression, definitions)
|
19
33
|
end
|
@@ -23,7 +37,7 @@ module Keisan
|
|
23
37
|
end
|
24
38
|
|
25
39
|
def ast(expression)
|
26
|
-
Evaluator.new(self).
|
40
|
+
Evaluator.new(self).parse_ast(expression)
|
27
41
|
end
|
28
42
|
|
29
43
|
def define_variable!(name, value)
|
data/lib/keisan/context.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Context
|
3
|
-
attr_reader :function_registry,
|
4
|
-
|
5
|
-
|
3
|
+
attr_reader :function_registry,
|
4
|
+
:variable_registry,
|
5
|
+
:allow_recursive,
|
6
|
+
:allow_multiline,
|
7
|
+
:allow_blocks
|
8
|
+
|
9
|
+
def initialize(parent: nil,
|
10
|
+
random: nil,
|
11
|
+
allow_recursive: false,
|
12
|
+
allow_multiline: true,
|
13
|
+
allow_blocks: true,
|
14
|
+
shadowed: [])
|
6
15
|
@parent = parent
|
7
|
-
@function_registry = Functions::Registry.new(parent: @parent
|
8
|
-
@variable_registry = Variables::Registry.new(parent: @parent
|
16
|
+
@function_registry = Functions::Registry.new(parent: @parent&.function_registry)
|
17
|
+
@variable_registry = Variables::Registry.new(parent: @parent&.variable_registry, shadowed: shadowed)
|
9
18
|
@random = random
|
10
19
|
@allow_recursive = allow_recursive
|
20
|
+
@allow_multiline = allow_multiline
|
21
|
+
@allow_blocks = allow_blocks
|
11
22
|
end
|
12
23
|
|
13
24
|
def allow_recursive!
|
@@ -47,7 +58,7 @@ module Keisan
|
|
47
58
|
|
48
59
|
def transient_definitions
|
49
60
|
return {} unless @transient
|
50
|
-
parent_definitions = @parent.
|
61
|
+
parent_definitions = @parent.nil? ? {} : @parent.transient_definitions
|
51
62
|
parent_definitions.merge(
|
52
63
|
@variable_registry.locals
|
53
64
|
).merge(
|
@@ -73,7 +84,7 @@ module Keisan
|
|
73
84
|
|
74
85
|
def register_variable!(name, value, local: false)
|
75
86
|
if !@variable_registry.shadowed.member?(name) && (transient? || !local && @parent&.variable_is_modifiable?(name))
|
76
|
-
@parent.register_variable!(name, value)
|
87
|
+
@parent.register_variable!(name, value, local: local)
|
77
88
|
else
|
78
89
|
@variable_registry.register!(name, value)
|
79
90
|
end
|
@@ -93,14 +104,14 @@ module Keisan
|
|
93
104
|
|
94
105
|
def register_function!(name, function, local: false)
|
95
106
|
if transient? || !local && @parent&.function_is_modifiable?(name)
|
96
|
-
@parent.register_function!(name, function)
|
107
|
+
@parent.register_function!(name, function, local: local)
|
97
108
|
else
|
98
109
|
@function_registry.register!(name.to_s, function)
|
99
110
|
end
|
100
111
|
end
|
101
112
|
|
102
113
|
def random
|
103
|
-
@random || @parent
|
114
|
+
@random || @parent&.random || Random.new
|
104
115
|
end
|
105
116
|
|
106
117
|
protected
|
@@ -110,7 +121,13 @@ module Keisan
|
|
110
121
|
end
|
111
122
|
|
112
123
|
def pure_child(shadowed: [])
|
113
|
-
self.class.new(
|
124
|
+
self.class.new(
|
125
|
+
parent: self,
|
126
|
+
shadowed: shadowed,
|
127
|
+
allow_recursive: allow_recursive,
|
128
|
+
allow_multiline: allow_multiline,
|
129
|
+
allow_blocks: allow_blocks
|
130
|
+
)
|
114
131
|
end
|
115
132
|
end
|
116
133
|
end
|
data/lib/keisan/evaluator.rb
CHANGED
@@ -8,7 +8,7 @@ module Keisan
|
|
8
8
|
|
9
9
|
def evaluate(expression, definitions = {})
|
10
10
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
11
|
-
ast =
|
11
|
+
ast = parse_ast(expression)
|
12
12
|
last_line = last_line(ast)
|
13
13
|
|
14
14
|
evaluation = ast.evaluated(context)
|
@@ -24,16 +24,28 @@ module Keisan
|
|
24
24
|
|
25
25
|
def simplify(expression, definitions = {})
|
26
26
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
27
|
-
ast =
|
27
|
+
ast = parse_ast(expression)
|
28
28
|
ast.simplify(context)
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
32
|
-
AST.parse(expression)
|
31
|
+
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
|
37
|
+
end
|
33
38
|
end
|
34
39
|
|
35
40
|
private
|
36
41
|
|
42
|
+
def disallowed_nodes
|
43
|
+
disallowed = []
|
44
|
+
disallowed << Keisan::AST::Block unless calculator.allow_blocks
|
45
|
+
disallowed << Keisan::AST::MultiLine unless calculator.allow_multiline
|
46
|
+
disallowed
|
47
|
+
end
|
48
|
+
|
37
49
|
def last_line(ast)
|
38
50
|
ast.is_a?(AST::MultiLine) ? ast.children.last : ast
|
39
51
|
end
|
data/lib/keisan/exceptions.rb
CHANGED
data/lib/keisan/function.rb
CHANGED
@@ -23,6 +23,12 @@ module Keisan
|
|
23
23
|
raise Exceptions::NotImplementedError.new
|
24
24
|
end
|
25
25
|
|
26
|
+
def unbound_variables(children, context)
|
27
|
+
children.inject(Set.new) do |vars, child|
|
28
|
+
vars | child.unbound_variables(context)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
26
32
|
protected
|
27
33
|
|
28
34
|
def validate_arguments!(count)
|
@@ -4,7 +4,9 @@ module Keisan
|
|
4
4
|
module Functions
|
5
5
|
class CMathFunction < MathFunction
|
6
6
|
def initialize(name, proc_function = nil)
|
7
|
-
super(name, proc_function || Proc.new {|arg|
|
7
|
+
super(name, proc_function || Proc.new {|arg|
|
8
|
+
CMath.send(name, arg)
|
9
|
+
})
|
8
10
|
end
|
9
11
|
end
|
10
12
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require_relative "let"
|
2
2
|
require_relative "puts"
|
3
|
+
require_relative "break"
|
4
|
+
require_relative "continue"
|
3
5
|
|
4
6
|
require_relative "if"
|
5
7
|
require_relative "while"
|
@@ -49,6 +51,8 @@ module Keisan
|
|
49
51
|
def self.register_defaults!(registry)
|
50
52
|
registry.register!(:let, Let.new, force: true)
|
51
53
|
registry.register!(:puts, Puts.new, force: true)
|
54
|
+
registry.register!(:break, Break.new, force: true)
|
55
|
+
registry.register!(:continue, Continue.new, force: true)
|
52
56
|
|
53
57
|
registry.register!(:if, If.new, force: true)
|
54
58
|
registry.register!(:while, While.new, force: true)
|
@@ -65,6 +69,7 @@ module Keisan
|
|
65
69
|
register_math!(registry)
|
66
70
|
register_array_methods!(registry)
|
67
71
|
register_random_methods!(registry)
|
72
|
+
register_date_time_methods!(registry)
|
68
73
|
end
|
69
74
|
|
70
75
|
def self.register_math!(registry)
|
@@ -124,6 +129,40 @@ module Keisan
|
|
124
129
|
registry.register!(:rand, Rand.new, force: true)
|
125
130
|
registry.register!(:sample, Sample.new, force: true)
|
126
131
|
end
|
132
|
+
|
133
|
+
def self.register_date_time_methods!(registry)
|
134
|
+
register_date_time!(registry)
|
135
|
+
|
136
|
+
registry.register!(:today, Proc.new { ::Date.today }, force: true)
|
137
|
+
registry.register!(:day, Proc.new {|d| d.mday }, force: true)
|
138
|
+
registry.register!(:weekday, Proc.new {|d| d.wday }, force: true)
|
139
|
+
registry.register!(:month, Proc.new {|d| d.month }, force: true)
|
140
|
+
registry.register!(:year, Proc.new {|d| d.year }, force: true)
|
141
|
+
|
142
|
+
registry.register!(:now, Proc.new { ::Time.now }, force: true)
|
143
|
+
registry.register!(:hour, Proc.new {|t| t.hour }, force: true)
|
144
|
+
registry.register!(:minute, Proc.new {|t| t.min }, force: true)
|
145
|
+
registry.register!(:second, Proc.new {|t| t.sec }, force: true)
|
146
|
+
registry.register!(:strftime, Proc.new {|*args| args.first.strftime(*args[1..-1]) }, force: true)
|
147
|
+
|
148
|
+
registry.register!(:to_time, Proc.new {|d| d.to_time }, force: true)
|
149
|
+
registry.register!(:to_date, Proc.new {|t| t.to_date }, force: true)
|
150
|
+
|
151
|
+
registry.register!(:epoch_seconds, Proc.new {|d| d.to_time - Time.new(1970, 1, 1, 0, 0, 0) }, force: true)
|
152
|
+
registry.register!(:epoch_days, Proc.new {|t| t.to_date - Date.new(1970, 1, 1) }, force: true)
|
153
|
+
end
|
154
|
+
|
155
|
+
def self.register_date_time!(registry)
|
156
|
+
[::Date, ::Time].each do |klass|
|
157
|
+
registry.register!(klass.to_s.downcase.to_sym, Proc.new {|*args|
|
158
|
+
if args.count == 1 && args.first.is_a?(::String)
|
159
|
+
AST.const_get(klass.to_s).new(klass.parse(args.first))
|
160
|
+
else
|
161
|
+
AST.const_get(klass.to_s).new(klass.new(*args))
|
162
|
+
end
|
163
|
+
}, force: true)
|
164
|
+
end
|
165
|
+
end
|
127
166
|
end
|
128
167
|
end
|
129
168
|
end
|
@@ -12,6 +12,10 @@ module Keisan
|
|
12
12
|
evaluate(ast_function, context)
|
13
13
|
end
|
14
14
|
|
15
|
+
def unbound_variables(children, context)
|
16
|
+
super - Set.new(shadowing_variable_names(children).map(&:name))
|
17
|
+
end
|
18
|
+
|
15
19
|
def evaluate(ast_function, context = nil)
|
16
20
|
validate_arguments!(ast_function.children.count)
|
17
21
|
context ||= Context.new
|
@@ -20,9 +24,9 @@ module Keisan
|
|
20
24
|
|
21
25
|
case operand
|
22
26
|
when AST::List
|
23
|
-
evaluate_list(operand, arguments, expression, context)
|
27
|
+
evaluate_list(operand, arguments, expression, context).evaluate(context)
|
24
28
|
when AST::Hash
|
25
|
-
evaluate_hash(operand, arguments, expression, context)
|
29
|
+
evaluate_hash(operand, arguments, expression, context).evaluate(context)
|
26
30
|
else
|
27
31
|
raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{operand}")
|
28
32
|
end
|
@@ -34,6 +38,10 @@ module Keisan
|
|
34
38
|
|
35
39
|
protected
|
36
40
|
|
41
|
+
def shadowing_variable_names(children)
|
42
|
+
raise Exceptions::NotImplementedError.new
|
43
|
+
end
|
44
|
+
|
37
45
|
def verify_arguments!(arguments)
|
38
46
|
unless arguments.all? {|argument| argument.is_a?(AST::Variable)}
|
39
47
|
raise Exceptions::InvalidFunctionError.new("Middle arguments to #{name} must be variables")
|
@@ -5,7 +5,11 @@ module Keisan
|
|
5
5
|
|
6
6
|
def initialize(name, arguments, expression, transient_definitions)
|
7
7
|
super(name, arguments.count)
|
8
|
-
|
8
|
+
if expression.is_a?(::String)
|
9
|
+
@expression = AST::parse(expression)
|
10
|
+
else
|
11
|
+
@expression = expression.deep_dup
|
12
|
+
end
|
9
13
|
@arguments = arguments
|
10
14
|
@transient_definitions = transient_definitions
|
11
15
|
end
|
@@ -70,17 +74,20 @@ module Keisan
|
|
70
74
|
|
71
75
|
local = local_context_for(context)
|
72
76
|
|
73
|
-
|
74
|
-
|
75
|
-
argument_values = ast_function.children.map {|child| child.evaluate(local)}
|
77
|
+
argument_values = ast_function.children.map {|child| child.evaluated(local)}
|
76
78
|
|
77
79
|
argument_derivatives = ast_function.children.map do |child|
|
78
|
-
child.
|
80
|
+
child.differentiated(variable, context)
|
79
81
|
end
|
80
82
|
|
83
|
+
partial_derivatives = calculate_partial_derivatives(context)
|
84
|
+
|
81
85
|
AST::Plus.new(
|
82
86
|
argument_derivatives.map.with_index {|argument_derivative, i|
|
83
|
-
partial_derivative = partial_derivatives[i]
|
87
|
+
partial_derivative = partial_derivatives[i]
|
88
|
+
argument_variables.each.with_index {|argument_variable, j|
|
89
|
+
partial_derivative = partial_derivative.replaced(argument_variable, argument_values[j])
|
90
|
+
}
|
84
91
|
AST::Times.new([argument_derivative, partial_derivative])
|
85
92
|
}
|
86
93
|
)
|
@@ -92,9 +99,9 @@ module Keisan
|
|
92
99
|
@argument_variables ||= arguments.map {|argument| AST::Variable.new(argument)}
|
93
100
|
end
|
94
101
|
|
95
|
-
def
|
96
|
-
|
97
|
-
partial_derivative = expression.
|
102
|
+
def calculate_partial_derivatives(context)
|
103
|
+
argument_variables.map.with_index do |variable, i|
|
104
|
+
partial_derivative = expression.differentiated(variable, context)
|
98
105
|
end
|
99
106
|
end
|
100
107
|
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class LoopControlFlowFuntion < Function
|
4
|
+
def initialize(name, exception_class)
|
5
|
+
super(name, 0)
|
6
|
+
@exception_class = exception_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def value(ast_function, context = nil)
|
10
|
+
raise @exception_class.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def evaluate(ast_function, context = nil)
|
14
|
+
raise @exception_class.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def simplify(ast_function, context = nil)
|
18
|
+
raise @exception_class.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|