keisan 0.4.0 → 0.5.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/.travis.yml +3 -5
- data/README.md +112 -8
- data/bin/keisan +7 -0
- data/lib/keisan.rb +0 -1
- data/lib/keisan/ast.rb +24 -0
- data/lib/keisan/ast/assignment.rb +36 -10
- data/lib/keisan/ast/builder.rb +7 -1
- data/lib/keisan/ast/constant_literal.rb +6 -1
- data/lib/keisan/ast/exponent.rb +1 -1
- data/lib/keisan/ast/function.rb +7 -3
- data/lib/keisan/ast/list.rb +1 -1
- data/lib/keisan/ast/node.rb +1 -1
- data/lib/keisan/ast/parent.rb +1 -1
- data/lib/keisan/ast/plus.rb +11 -1
- data/lib/keisan/ast/string.rb +8 -0
- data/lib/keisan/ast/unary_operator.rb +1 -1
- data/lib/keisan/ast/variable.rb +8 -2
- data/lib/keisan/calculator.rb +16 -0
- data/lib/keisan/context.rb +39 -8
- data/lib/keisan/evaluator.rb +11 -1
- data/lib/keisan/function.rb +4 -0
- data/lib/keisan/functions/abs.rb +9 -0
- data/lib/keisan/functions/cbrt.rb +15 -0
- data/lib/keisan/functions/cmath_function.rb +11 -0
- data/lib/keisan/functions/cos.rb +15 -0
- data/lib/keisan/functions/cosh.rb +15 -0
- data/lib/keisan/functions/cot.rb +15 -0
- data/lib/keisan/functions/coth.rb +15 -0
- data/lib/keisan/functions/csc.rb +15 -0
- data/lib/keisan/functions/csch.rb +15 -0
- data/lib/keisan/functions/default_registry.rb +95 -1
- data/lib/keisan/functions/diff.rb +36 -14
- data/lib/keisan/functions/exp.rb +15 -0
- data/lib/keisan/functions/expression_function.rb +65 -15
- data/lib/keisan/functions/filter.rb +64 -0
- data/lib/keisan/functions/if.rb +15 -12
- data/lib/keisan/functions/imag.rb +9 -0
- data/lib/keisan/functions/log.rb +15 -0
- data/lib/keisan/functions/map.rb +57 -0
- data/lib/keisan/functions/math_function.rb +34 -0
- data/lib/keisan/functions/proc_function.rb +5 -3
- data/lib/keisan/functions/real.rb +9 -0
- data/lib/keisan/functions/reduce.rb +65 -0
- data/lib/keisan/functions/registry.rb +5 -1
- data/lib/keisan/functions/sec.rb +15 -0
- data/lib/keisan/functions/sech.rb +15 -0
- data/lib/keisan/functions/sin.rb +15 -0
- data/lib/keisan/functions/sinh.rb +15 -0
- data/lib/keisan/functions/sqrt.rb +15 -0
- data/lib/keisan/functions/tan.rb +15 -0
- data/lib/keisan/functions/tanh.rb +15 -0
- data/lib/keisan/repl.rb +105 -0
- data/lib/keisan/tokenizer.rb +5 -2
- data/lib/keisan/tokens/string.rb +1 -1
- data/lib/keisan/variables/registry.rb +14 -3
- data/lib/keisan/version.rb +1 -1
- data/screenshots/repl.png +0 -0
- metadata +30 -6
- data/lib/keisan/ast/functions/diff.rb +0 -57
- data/lib/keisan/ast/functions/if.rb +0 -47
- data/lib/keisan/function_definition_context.rb +0 -34
@@ -0,0 +1,57 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class Map < Keisan::Function
|
4
|
+
# Maps (list, variable, expression)
|
5
|
+
# e.g. map([1,2,3], x, 2*x)
|
6
|
+
# should give [2,4,6]
|
7
|
+
def initialize
|
8
|
+
@name = "map"
|
9
|
+
end
|
10
|
+
|
11
|
+
def value(ast_function, context = nil)
|
12
|
+
evaluate(ast_function, context = nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
def evaluate(ast_function, context = nil)
|
16
|
+
context ||= Context.new
|
17
|
+
simplify(ast_function, context).evaluate(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def simplify(ast_function, context = nil)
|
21
|
+
context ||= Keisan::Context.new
|
22
|
+
list, variable, expression = list_variable_expression_for(ast_function, context)
|
23
|
+
|
24
|
+
local = context.spawn_child(transient: false, shadowed: [variable.name])
|
25
|
+
|
26
|
+
Keisan::AST::List.new(
|
27
|
+
list.children.map do |element|
|
28
|
+
local.register_variable!(variable, element)
|
29
|
+
expression.simplified(local)
|
30
|
+
end
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def list_variable_expression_for(ast_function, context)
|
37
|
+
unless ast_function.children.size == 3
|
38
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Require 3 arguments to map")
|
39
|
+
end
|
40
|
+
|
41
|
+
list = ast_function.children[0].simplify(context)
|
42
|
+
variable = ast_function.children[1]
|
43
|
+
expression = ast_function.children[2]
|
44
|
+
|
45
|
+
unless list.is_a?(Keisan::AST::List)
|
46
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("First argument to map must be a list")
|
47
|
+
end
|
48
|
+
|
49
|
+
unless variable.is_a?(Keisan::AST::Variable)
|
50
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Second argument to map must be a variable")
|
51
|
+
end
|
52
|
+
|
53
|
+
[list, variable, expression]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class MathFunction < ProcFunction
|
4
|
+
def initialize(name, proc_function = nil)
|
5
|
+
super(name, proc_function || Proc.new {|arg| Math.send(name, arg)})
|
6
|
+
end
|
7
|
+
|
8
|
+
def simplify(ast_function, context = nil)
|
9
|
+
simplified = super
|
10
|
+
self.class.apply_simplifications(simplified)
|
11
|
+
end
|
12
|
+
|
13
|
+
def differentiate(ast_function, variable, context = nil)
|
14
|
+
raise Keisan::Exceptions::InvalidFunctionError.new unless ast_function.children.count == 1
|
15
|
+
context ||= Context.new
|
16
|
+
|
17
|
+
argument_simplified = ast_function.children.first.simplify(context)
|
18
|
+
argument_differentiated = argument_simplified.differentiate(variable, context)
|
19
|
+
|
20
|
+
(argument_differentiated * self.class.derivative(argument_simplified)).simplify(context)
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def self.derivative(argument)
|
26
|
+
raise Keisan::Exceptions::NotImplementedError.new
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.apply_simplifications(simplified)
|
30
|
+
simplified
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module Keisan
|
2
2
|
module Functions
|
3
3
|
class ProcFunction < Keisan::Function
|
4
|
+
attr_reader :function_proc
|
5
|
+
|
4
6
|
def initialize(name, function_proc)
|
5
7
|
raise Keisan::Exceptions::InvalidFunctionError.new unless function_proc.is_a?(Proc)
|
6
8
|
|
@@ -9,13 +11,13 @@ module Keisan
|
|
9
11
|
end
|
10
12
|
|
11
13
|
def call(context, *args)
|
12
|
-
|
14
|
+
function_proc.call(*args).to_node
|
13
15
|
end
|
14
16
|
|
15
17
|
def value(ast_function, context = nil)
|
16
18
|
context ||= Keisan::Context.new
|
17
19
|
argument_values = ast_function.children.map {|child| child.value(context)}
|
18
|
-
call(context, *argument_values)
|
20
|
+
call(context, *argument_values).value(context)
|
19
21
|
end
|
20
22
|
|
21
23
|
def evaluate(ast_function, context = nil)
|
@@ -23,7 +25,7 @@ module Keisan
|
|
23
25
|
|
24
26
|
ast_function.instance_variable_set(
|
25
27
|
:@children,
|
26
|
-
ast_function.children.map {|child| child.evaluate(context)}
|
28
|
+
ast_function.children.map {|child| child.evaluate(context).to_node}
|
27
29
|
)
|
28
30
|
|
29
31
|
if ast_function.children.all? {|child| child.well_defined?(context)}
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class Reduce < Keisan::Function
|
4
|
+
# Reduces (list, initial, accumulator, variable, expression)
|
5
|
+
# e.g. reduce([1,2,3,4], 0, total, x, total+x)
|
6
|
+
# should give 10
|
7
|
+
def initialize
|
8
|
+
@name = "reduce"
|
9
|
+
end
|
10
|
+
|
11
|
+
def value(ast_function, context = nil)
|
12
|
+
evaluate(ast_function, context = nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
def evaluate(ast_function, context = nil)
|
16
|
+
context ||= Context.new
|
17
|
+
simplify(ast_function, context).evaluate(context)
|
18
|
+
end
|
19
|
+
|
20
|
+
def simplify(ast_function, context = nil)
|
21
|
+
context ||= Keisan::Context.new
|
22
|
+
list, initial, accumulator, variable, expression = list_initial_accumulator_variable_expression_for(ast_function, context)
|
23
|
+
|
24
|
+
local = context.spawn_child(transient: false, shadowed: [accumulator.name, variable.name])
|
25
|
+
local.register_variable!(accumulator, initial.simplify(context))
|
26
|
+
|
27
|
+
list.children.each do |element|
|
28
|
+
local.register_variable!(variable, element)
|
29
|
+
result = expression.simplified(local)
|
30
|
+
local.register_variable!(accumulator, result)
|
31
|
+
end
|
32
|
+
|
33
|
+
local.variable(accumulator.name)
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def list_initial_accumulator_variable_expression_for(ast_function, context)
|
39
|
+
unless ast_function.children.size == 5
|
40
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Require 5 arguments to reduce")
|
41
|
+
end
|
42
|
+
|
43
|
+
list = ast_function.children[0].simplify(context)
|
44
|
+
initial = ast_function.children[1]
|
45
|
+
accumulator = ast_function.children[2]
|
46
|
+
variable = ast_function.children[3]
|
47
|
+
expression = ast_function.children[4]
|
48
|
+
|
49
|
+
unless list.is_a?(Keisan::AST::List)
|
50
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("First argument to reduce must be a list")
|
51
|
+
end
|
52
|
+
|
53
|
+
unless accumulator.is_a?(Keisan::AST::Variable)
|
54
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Third argument to reduce is accumulator and must be a variable")
|
55
|
+
end
|
56
|
+
|
57
|
+
unless variable.is_a?(Keisan::AST::Variable)
|
58
|
+
raise Keisan::Exceptions::InvalidFunctionError.new("Fourth argument to reduce is variable and must be a variable")
|
59
|
+
end
|
60
|
+
|
61
|
+
[list, initial, accumulator, variable, expression]
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -22,6 +22,10 @@ module Keisan
|
|
22
22
|
raise Keisan::Exceptions::UndefinedFunctionError.new name
|
23
23
|
end
|
24
24
|
|
25
|
+
def locals
|
26
|
+
@hash
|
27
|
+
end
|
28
|
+
|
25
29
|
def has?(name)
|
26
30
|
!!self[name]
|
27
31
|
rescue Keisan::Exceptions::UndefinedFunctionError
|
@@ -40,7 +44,7 @@ module Keisan
|
|
40
44
|
when Proc
|
41
45
|
self[name] = Keisan::Functions::ProcFunction.new(name, function)
|
42
46
|
when Keisan::Function
|
43
|
-
self[
|
47
|
+
self[name] = function
|
44
48
|
else
|
45
49
|
raise Keisan::Exceptions::InvalidFunctionError.new
|
46
50
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class Sec < CMathFunction
|
4
|
+
def initialize
|
5
|
+
super("sec", Proc.new {|arg| 1 / CMath::cos(arg)})
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def self.derivative(argument)
|
11
|
+
Keisan::AST::Function.new([argument], "sin") * Keisan::AST::Exponent.new([Keisan::AST::Function.new([argument], "cos"), -2])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class Sech < CMathFunction
|
4
|
+
def initialize
|
5
|
+
super("sech", Proc.new {|arg| 1 / CMath::cosh(arg)})
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def self.derivative(argument)
|
11
|
+
-Keisan::AST::Function.new([argument], "sinh") * Keisan::AST::Exponent.new([Keisan::AST::Function.new([argument], "cosh"), -2])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class Tan < CMathFunction
|
4
|
+
def initialize
|
5
|
+
super("tan")
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def self.derivative(argument)
|
11
|
+
Keisan::AST::Exponent.new([Keisan::AST::Function.new([argument], "cos"), -2])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class Tanh < CMathFunction
|
4
|
+
def initialize
|
5
|
+
super("tanh")
|
6
|
+
end
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
def self.derivative(argument)
|
11
|
+
Keisan::AST::Exponent.new([Keisan::AST::Function.new([argument], "cosh"), -2])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/keisan/repl.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
require "pry"
|
2
|
+
require "readline"
|
3
|
+
|
4
|
+
module Keisan
|
5
|
+
class Repl
|
6
|
+
COMMANDS = %w(
|
7
|
+
reset
|
8
|
+
quit
|
9
|
+
variables
|
10
|
+
functions
|
11
|
+
allow_recursive
|
12
|
+
).freeze
|
13
|
+
|
14
|
+
attr_reader :calculator
|
15
|
+
|
16
|
+
def initialize
|
17
|
+
@running = true
|
18
|
+
initialize_completion_commands
|
19
|
+
reset
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset
|
23
|
+
@calculator = Keisan::Calculator.new
|
24
|
+
end
|
25
|
+
|
26
|
+
def start
|
27
|
+
while @running
|
28
|
+
command = get_command
|
29
|
+
|
30
|
+
# ctrl-d should break out
|
31
|
+
break if command.nil?
|
32
|
+
process_command command
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_command
|
37
|
+
Readline.readline("keisan> ", true)
|
38
|
+
end
|
39
|
+
|
40
|
+
def process_command(command)
|
41
|
+
command = command.strip
|
42
|
+
return if command.empty?
|
43
|
+
|
44
|
+
case command
|
45
|
+
when /\Areset\z/i
|
46
|
+
reset
|
47
|
+
when /\Aquit\z/i
|
48
|
+
@running = false
|
49
|
+
when /\Avariables\z/i
|
50
|
+
output_variables
|
51
|
+
when /\Afunctions\z/i
|
52
|
+
output_functions
|
53
|
+
when /\Aallow_recursive\z/i
|
54
|
+
calculator.allow_recursive!
|
55
|
+
else
|
56
|
+
begin
|
57
|
+
output_result calculator.simplify(command)
|
58
|
+
rescue StandardError => error
|
59
|
+
output_error error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def output_result(result)
|
65
|
+
puts "=> " + CodeRay.encode(result.to_s, :ruby, :terminal)
|
66
|
+
end
|
67
|
+
|
68
|
+
def output_error(error)
|
69
|
+
puts CodeRay.encode(error.class.to_s, :ruby, :terminal) + ": " + CodeRay.encode("\"#{error.message}\"", :ruby, :terminal)
|
70
|
+
end
|
71
|
+
|
72
|
+
def output_variables
|
73
|
+
variable_registry.locals.each do |name, variable|
|
74
|
+
puts CodeRay.encode("#{name} = #{variable.value}", :ruby, :terminal)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def output_functions
|
79
|
+
function_registry.locals.each do |name, function|
|
80
|
+
puts CodeRay.encode("#{name}(#{function.arguments.join(', ')}) = #{function.expression.to_s}", :ruby, :terminal)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def context
|
87
|
+
calculator.context
|
88
|
+
end
|
89
|
+
|
90
|
+
def variable_registry
|
91
|
+
context.variable_registry
|
92
|
+
end
|
93
|
+
|
94
|
+
def function_registry
|
95
|
+
context.function_registry
|
96
|
+
end
|
97
|
+
|
98
|
+
def initialize_completion_commands
|
99
|
+
completion_proc = Proc.new { |s| COMMANDS.grep(/^#{Regexp.escape(s)}/) }
|
100
|
+
|
101
|
+
Readline.completion_append_character = " "
|
102
|
+
Readline.completion_proc = completion_proc
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|