keisan 0.7.0 → 0.8.4
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 +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
|