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.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +3 -5
  3. data/README.md +112 -8
  4. data/bin/keisan +7 -0
  5. data/lib/keisan.rb +0 -1
  6. data/lib/keisan/ast.rb +24 -0
  7. data/lib/keisan/ast/assignment.rb +36 -10
  8. data/lib/keisan/ast/builder.rb +7 -1
  9. data/lib/keisan/ast/constant_literal.rb +6 -1
  10. data/lib/keisan/ast/exponent.rb +1 -1
  11. data/lib/keisan/ast/function.rb +7 -3
  12. data/lib/keisan/ast/list.rb +1 -1
  13. data/lib/keisan/ast/node.rb +1 -1
  14. data/lib/keisan/ast/parent.rb +1 -1
  15. data/lib/keisan/ast/plus.rb +11 -1
  16. data/lib/keisan/ast/string.rb +8 -0
  17. data/lib/keisan/ast/unary_operator.rb +1 -1
  18. data/lib/keisan/ast/variable.rb +8 -2
  19. data/lib/keisan/calculator.rb +16 -0
  20. data/lib/keisan/context.rb +39 -8
  21. data/lib/keisan/evaluator.rb +11 -1
  22. data/lib/keisan/function.rb +4 -0
  23. data/lib/keisan/functions/abs.rb +9 -0
  24. data/lib/keisan/functions/cbrt.rb +15 -0
  25. data/lib/keisan/functions/cmath_function.rb +11 -0
  26. data/lib/keisan/functions/cos.rb +15 -0
  27. data/lib/keisan/functions/cosh.rb +15 -0
  28. data/lib/keisan/functions/cot.rb +15 -0
  29. data/lib/keisan/functions/coth.rb +15 -0
  30. data/lib/keisan/functions/csc.rb +15 -0
  31. data/lib/keisan/functions/csch.rb +15 -0
  32. data/lib/keisan/functions/default_registry.rb +95 -1
  33. data/lib/keisan/functions/diff.rb +36 -14
  34. data/lib/keisan/functions/exp.rb +15 -0
  35. data/lib/keisan/functions/expression_function.rb +65 -15
  36. data/lib/keisan/functions/filter.rb +64 -0
  37. data/lib/keisan/functions/if.rb +15 -12
  38. data/lib/keisan/functions/imag.rb +9 -0
  39. data/lib/keisan/functions/log.rb +15 -0
  40. data/lib/keisan/functions/map.rb +57 -0
  41. data/lib/keisan/functions/math_function.rb +34 -0
  42. data/lib/keisan/functions/proc_function.rb +5 -3
  43. data/lib/keisan/functions/real.rb +9 -0
  44. data/lib/keisan/functions/reduce.rb +65 -0
  45. data/lib/keisan/functions/registry.rb +5 -1
  46. data/lib/keisan/functions/sec.rb +15 -0
  47. data/lib/keisan/functions/sech.rb +15 -0
  48. data/lib/keisan/functions/sin.rb +15 -0
  49. data/lib/keisan/functions/sinh.rb +15 -0
  50. data/lib/keisan/functions/sqrt.rb +15 -0
  51. data/lib/keisan/functions/tan.rb +15 -0
  52. data/lib/keisan/functions/tanh.rb +15 -0
  53. data/lib/keisan/repl.rb +105 -0
  54. data/lib/keisan/tokenizer.rb +5 -2
  55. data/lib/keisan/tokens/string.rb +1 -1
  56. data/lib/keisan/variables/registry.rb +14 -3
  57. data/lib/keisan/version.rb +1 -1
  58. data/screenshots/repl.png +0 -0
  59. metadata +30 -6
  60. data/lib/keisan/ast/functions/diff.rb +0 -57
  61. data/lib/keisan/ast/functions/if.rb +0 -47
  62. data/lib/keisan/function_definition_context.rb +0 -34
@@ -0,0 +1,15 @@
1
+ module Keisan
2
+ module Functions
3
+ class Csch < CMathFunction
4
+ def initialize
5
+ super("csch", Proc.new {|arg| 1 / CMath::sinh(arg)})
6
+ end
7
+
8
+ protected
9
+
10
+ def self.derivative(argument)
11
+ -Keisan::AST::Function.new([argument], "cosh") * Keisan::AST::Exponent.new([Keisan::AST::Function.new([argument], "sinh"), -2])
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,8 +1,32 @@
1
1
  require_relative "if"
2
2
  require_relative "diff"
3
3
  require_relative "replace"
4
+ require_relative "map"
5
+ require_relative "filter"
6
+ require_relative "reduce"
4
7
  require_relative "rand"
5
8
  require_relative "sample"
9
+ require_relative "math_function"
10
+ require_relative "cmath_function"
11
+ require_relative "exp"
12
+ require_relative "log"
13
+ require_relative "sin"
14
+ require_relative "cos"
15
+ require_relative "sec"
16
+ require_relative "tan"
17
+ require_relative "cot"
18
+ require_relative "csc"
19
+ require_relative "sinh"
20
+ require_relative "cosh"
21
+ require_relative "sech"
22
+ require_relative "tanh"
23
+ require_relative "coth"
24
+ require_relative "csch"
25
+ require_relative "sqrt"
26
+ require_relative "cbrt"
27
+ require_relative "abs"
28
+ require_relative "real"
29
+ require_relative "imag"
6
30
 
7
31
  module Keisan
8
32
  module Functions
@@ -19,10 +43,40 @@ module Keisan
19
43
  registry.register!(:if, Keisan::Functions::If.new, force: true)
20
44
  registry.register!(:diff, Keisan::Functions::Diff.new, force: true)
21
45
  registry.register!(:replace, Keisan::Functions::Replace.new, force: true)
46
+ registry.register!(:map, Keisan::Functions::Map.new, force: true)
47
+ registry.register!(:collect, Keisan::Functions::Map.new, force: true)
48
+ registry.register!(:filter, Keisan::Functions::Filter.new, force: true)
49
+ registry.register!(:select, Keisan::Functions::Filter.new, force: true)
50
+ registry.register!(:reduce, Keisan::Functions::Reduce.new, force: true)
51
+ registry.register!(:inject, Keisan::Functions::Reduce.new, force: true)
22
52
 
23
53
  register_builtin_math!(registry)
24
54
  register_array_methods!(registry)
25
55
  register_random_methods!(registry)
56
+
57
+ registry.register!(:exp, Keisan::Functions::Exp.new, force: true)
58
+ registry.register!(:log, Keisan::Functions::Log.new, force: true)
59
+
60
+ registry.register!(:sin, Keisan::Functions::Sin.new, force: true)
61
+ registry.register!(:cos, Keisan::Functions::Cos.new, force: true)
62
+ registry.register!(:tan, Keisan::Functions::Tan.new, force: true)
63
+ registry.register!(:cot, Keisan::Functions::Cot.new, force: true)
64
+ registry.register!(:sec, Keisan::Functions::Sec.new, force: true)
65
+ registry.register!(:csc, Keisan::Functions::Csc.new, force: true)
66
+
67
+ registry.register!(:sinh, Keisan::Functions::Sinh.new, force: true)
68
+ registry.register!(:cosh, Keisan::Functions::Cosh.new, force: true)
69
+ registry.register!(:tanh, Keisan::Functions::Tanh.new, force: true)
70
+ registry.register!(:coth, Keisan::Functions::Coth.new, force: true)
71
+ registry.register!(:sech, Keisan::Functions::Sech.new, force: true)
72
+ registry.register!(:csch, Keisan::Functions::Csch.new, force: true)
73
+
74
+ registry.register!(:sqrt, Keisan::Functions::Sqrt.new, force: true)
75
+ registry.register!(:cbrt, Keisan::Functions::Cbrt.new, force: true)
76
+
77
+ registry.register!(:abs, Keisan::Functions::Abs.new, force: true)
78
+ registry.register!(:real, Keisan::Functions::Real.new, force: true)
79
+ registry.register!(:imag, Keisan::Functions::Imag.new, force: true)
26
80
  end
27
81
 
28
82
  def self.register_builtin_math!(registry)
@@ -30,6 +84,7 @@ module Keisan
30
84
  registry.register!(
31
85
  method,
32
86
  Proc.new {|*args|
87
+ args = args.map(&:value)
33
88
  Math.send(method, *args)
34
89
  },
35
90
  force: true
@@ -38,9 +93,48 @@ module Keisan
38
93
  end
39
94
 
40
95
  def self.register_array_methods!(registry)
41
- %i(min max size).each do |method|
96
+ %i(min max size flatten reverse).each do |method|
42
97
  registry.register!(method, Proc.new {|a| a.send(method)}, force: true)
43
98
  end
99
+
100
+ # range(10) => Integers from 0 inclusive to 10 exclusively
101
+ # range(5, 15) => Integers from 5 inclusive to 15 exclusive
102
+ # range(10, -1, -2) => Integers from 10 inclusive to -1 exclusive, decreasing by twos
103
+ # i.e.: [10, 8, 6, 4, 2, 0]
104
+ registry.register!("range", Proc.new {|*args|
105
+ case args.count
106
+ when 1
107
+ (0...args[0]).to_a
108
+ when 2
109
+ (args[0]...args[1]).to_a
110
+ when 3
111
+ current = args[0]
112
+ final = args[1]
113
+ shift = args[2]
114
+
115
+ if shift == 0 or !shift.is_a?(Integer)
116
+ raise Keisan::Exceptions::InvalidFunctionError.new("range's 3rd argument must be non-zero integer")
117
+ end
118
+
119
+ result = []
120
+
121
+ if shift > 0
122
+ while current < final
123
+ result << current
124
+ current += shift
125
+ end
126
+ else
127
+ while current > final
128
+ result << current
129
+ current += shift
130
+ end
131
+ end
132
+
133
+ result
134
+ else
135
+ raise Keisan::Exceptions::InvalidFunctionError.new("range takes 1 to 3 arguments")
136
+ end
137
+ }, force: true)
44
138
  end
45
139
 
46
140
  def self.register_random_methods!(registry)
@@ -17,29 +17,46 @@ module Keisan
17
17
  end
18
18
 
19
19
  def evaluate(ast_function, context = nil)
20
- context ||= Context.new
21
- function, vars = function_and_vars(ast_function)
20
+ context ||= Keisan::Context.new
21
+ function, variables = function_and_variables(ast_function)
22
+ local = context_from(variables, context)
22
23
 
23
- vars.inject(function.evaluate(context)) do |result, variable|
24
- result = differentiate(result, variable, context)
24
+ result = variables.inject(function.evaluate(local)) do |result, variable|
25
+ result = differentiate(result, variable, local)
25
26
  if !is_ast_derivative?(result)
26
- result = result.evaluate(context)
27
+ result = result.evaluate(local)
27
28
  end
28
29
  result
29
30
  end
31
+
32
+ case result
33
+ when Keisan::AST::Function
34
+ result.name == "diff" ? result : result.simplify(context)
35
+ else
36
+ result.simplify(context)
37
+ end
30
38
  end
31
39
 
32
40
  def simplify(ast_function, context = nil)
33
- context ||= Context.new
34
- function, vars = function_and_vars(ast_function)
41
+ raise Keisan::Exceptions::InternalError.new("received non-diff function") unless ast_function.name == "diff"
42
+ function, variables = function_and_variables(ast_function)
43
+ context ||= Keisan::Context.new
44
+ local = context_from(variables, context)
35
45
 
36
- vars.inject(function.simplify(context)) do |result, variable|
37
- result = differentiate(result, variable, context)
46
+ result = variables.inject(function.simplify(local)) do |result, variable|
47
+ result = differentiate(result, variable, local)
38
48
  if !is_ast_derivative?(result)
39
- result = result.simplify(context)
49
+ result = result.simplify(local)
40
50
  end
41
51
  result
42
52
  end
53
+
54
+ case result
55
+ when Keisan::AST::Function
56
+ result.name == "diff" ? result : result.simplify(context)
57
+ else
58
+ result.simplify(context)
59
+ end
43
60
  end
44
61
 
45
62
  private
@@ -61,22 +78,27 @@ module Keisan
61
78
  )
62
79
  end
63
80
 
64
- def function_and_vars(ast_function)
81
+ def function_and_variables(ast_function)
65
82
  unless ast_function.is_a?(Keisan::AST::Function) && ast_function.name == name
66
83
  raise Keisan::Exceptions::InvalidFunctionError.new("Must receive diff function")
67
84
  end
68
85
 
69
- vars = ast_function.children[1..-1]
86
+ variables = ast_function.children[1..-1]
70
87
 
71
- unless vars.all? {|var| var.is_a?(AST::Variable)}
88
+ unless variables.all? {|var| var.is_a?(AST::Variable)}
72
89
  raise Keisan::Exceptions::InvalidFunctionError.new("Diff must differentiate with respect to variables")
73
90
  end
74
91
 
75
92
  [
76
93
  ast_function.children.first,
77
- vars
94
+ variables
78
95
  ]
79
96
  end
97
+
98
+ def context_from(variables, context = nil)
99
+ context ||= Keisan::Context.new(shadowed: variables.map(&:name))
100
+ context.spawn_child(shadowed: variables.map(&:name))
101
+ end
80
102
  end
81
103
  end
82
104
  end
@@ -0,0 +1,15 @@
1
+ module Keisan
2
+ module Functions
3
+ class Exp < CMathFunction
4
+ def initialize
5
+ super("exp")
6
+ end
7
+
8
+ protected
9
+
10
+ def self.derivative(argument)
11
+ Keisan::AST::Function.new([argument], "exp")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,19 +3,17 @@ module Keisan
3
3
  class ExpressionFunction < Keisan::Function
4
4
  attr_reader :arguments, :expression
5
5
 
6
- def initialize(name, arguments, expression, local_context)
6
+ def initialize(name, arguments, expression, transient_definitions)
7
7
  super(name)
8
8
  @expression = expression.deep_dup
9
9
  @arguments = arguments
10
- @local_context = local_context
10
+ @transient_definitions = transient_definitions
11
11
  end
12
12
 
13
13
  def call(context, *args)
14
- unless @arguments.count == args.count
15
- raise Keisan::Exceptions::InvalidFunctionError.new("Invalid number of arguments for #{name} function")
16
- end
14
+ verify_argument_size!(args.count)
17
15
 
18
- local = @local_context.spawn_child
16
+ local = local_context_for(context)
19
17
  arguments.each.with_index do |arg_name, i|
20
18
  local.register_variable!(arg_name, args[i])
21
19
  end
@@ -24,28 +22,30 @@ module Keisan
24
22
  end
25
23
 
26
24
  def value(ast_function, context = nil)
25
+ verify_argument_size!(ast_function.children.count)
26
+
27
27
  context ||= Keisan::Context.new
28
28
  argument_values = ast_function.children.map {|child| child.value(context)}
29
29
  call(context, *argument_values)
30
30
  end
31
31
 
32
32
  def evaluate(ast_function, context = nil)
33
+ verify_argument_size!(ast_function.children.count)
34
+
33
35
  context ||= Keisan::Context.new
36
+ local = local_context_for(context)
34
37
 
35
- ast_function.instance_variable_set(
36
- :@children,
37
- ast_function.children.map {|child| child.evaluate(context)}
38
- )
38
+ argument_values = ast_function.children.map {|child| child.evaluate(context)}
39
39
 
40
- if ast_function.children.all? {|child| child.well_defined?(context)}
41
- value(ast_function, context).to_node.evaluate(context)
42
- else
43
- ast_function
40
+ arguments.each.with_index do |arg_name, i|
41
+ local.register_variable!(arg_name, argument_values[i].evaluate(context))
44
42
  end
43
+
44
+ expression.evaluate(local)
45
45
  end
46
46
 
47
47
  def simplify(ast_function, context = nil)
48
- context ||= Context.new
48
+ verify_argument_size!(ast_function.children.count)
49
49
 
50
50
  ast_function.instance_variable_set(
51
51
  :@children,
@@ -58,6 +58,56 @@ module Keisan
58
58
  ast_function
59
59
  end
60
60
  end
61
+
62
+ # Multi-argument functions work as follows:
63
+ # Given f(x, y), in general we will take the derivative with respect to t,
64
+ # and x = x(t), y = y(t). For instance d/dt f(2*t, t+1).
65
+ # In this case, chain rule gives derivative:
66
+ # dx(t)/dt * f_x(x(t), y(t)) + dy(t)/dt * f_y(x(t), y(t)),
67
+ # where f_x and f_y are the x and y partial derivatives respectively.
68
+ def differentiate(ast_function, variable, context = nil)
69
+ verify_argument_size!(ast_function.children.count)
70
+
71
+ local = local_context_for(context)
72
+
73
+ # expression.differentiate(variable, context)
74
+
75
+ argument_values = ast_function.children.map {|child| child.evaluate(local)}
76
+
77
+ argument_derivatives = ast_function.children.map do |child|
78
+ child.differentiate(variable, context)
79
+ end
80
+
81
+ Keisan::AST::Plus.new(
82
+ argument_derivatives.map.with_index {|argument_derivative, i|
83
+ partial_derivative = partial_derivatives[i].replace(argument_variables[i], argument_values[i])
84
+ Keisan::AST::Times.new([argument_derivative, partial_derivative])
85
+ }
86
+ )
87
+ end
88
+
89
+ private
90
+
91
+ def argument_variables
92
+ @argument_variables ||= arguments.map {|argument| Keisan::AST::Variable.new(argument)}
93
+ end
94
+
95
+ def partial_derivatives
96
+ @partial_derivatives ||= argument_variables.map.with_index do |variable, i|
97
+ partial_derivative = expression.differentiate(variable)
98
+ end
99
+ end
100
+
101
+ def verify_argument_size!(argument_size)
102
+ unless @arguments.count == argument_size
103
+ raise Keisan::Exceptions::InvalidFunctionError.new("Invalid number of arguments for #{name} function")
104
+ end
105
+ end
106
+
107
+ def local_context_for(context = nil)
108
+ context ||= Keisan::Context.new
109
+ context.spawn_child(definitions: @transient_definitions, shadowed: @arguments, transient: true)
110
+ end
61
111
  end
62
112
  end
63
113
  end
@@ -0,0 +1,64 @@
1
+ module Keisan
2
+ module Functions
3
+ class Filter < Keisan::Function
4
+ # Filters (list, variable, expression)
5
+ # e.g. filter([1,2,3,4], x, x % 2 == 0)
6
+ # should give [2,4]
7
+ def initialize
8
+ @name = "filter"
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
+ list, variable, expression = list_variable_expression_for(ast_function, context)
22
+
23
+ context ||= Keisan::Context.new
24
+ local = context.spawn_child(transient: false, shadowed: [variable.name])
25
+
26
+ Keisan::AST::List.new(
27
+ list.children.select do |element|
28
+ local.register_variable!(variable, element)
29
+ result = expression.evaluate(local)
30
+
31
+ case result
32
+ when Keisan::AST::Boolean
33
+ result.value
34
+ else
35
+ raise Keisan::Exceptions::InvalidFunctionError.new("Filter requires expression to be a logical expression")
36
+ end
37
+ end
38
+ )
39
+ end
40
+
41
+ private
42
+
43
+ def list_variable_expression_for(ast_function, context)
44
+ unless ast_function.children.size == 3
45
+ raise Keisan::Exceptions::InvalidFunctionError.new("Require 3 arguments to filter")
46
+ end
47
+
48
+ list = ast_function.children[0].simplify(context)
49
+ variable = ast_function.children[1]
50
+ expression = ast_function.children[2]
51
+
52
+ unless list.is_a?(Keisan::AST::List)
53
+ raise Keisan::Exceptions::InvalidFunctionError.new("First argument to filter must be a list")
54
+ end
55
+
56
+ unless variable.is_a?(Keisan::AST::Variable)
57
+ raise Keisan::Exceptions::InvalidFunctionError.new("Second argument to filter must be a variable")
58
+ end
59
+
60
+ [list, variable, expression]
61
+ end
62
+ end
63
+ end
64
+ end
@@ -27,17 +27,8 @@ module Keisan
27
27
  bool = ast_function.children[0].evaluate(context)
28
28
 
29
29
  if bool.is_a?(Keisan::AST::Boolean)
30
- if bool.value
31
- ast_function.children[1].evaluate(context)
32
- else
33
- ast_function.children[2].to_node.evaluate(context)
34
- end
35
- else
36
- ast_function
37
- end
38
-
39
- if ast_function.children.all? {|child| child.well_defined?(context)}
40
- value(ast_function, context).to_node.evaluate(context)
30
+ node = bool.value ? ast_function.children[1] : ast_function.children[2]
31
+ node.to_node.evaluate(context)
41
32
  else
42
33
  ast_function
43
34
  end
@@ -49,7 +40,7 @@ module Keisan
49
40
 
50
41
  if bool.is_a?(Keisan::AST::Boolean)
51
42
  if bool.value
52
- ast_function.children[1].simplify(context)
43
+ ast_function.children[1].to_node.simplify(context)
53
44
  else
54
45
  ast_function.children[2].to_node.simplify(context)
55
46
  end
@@ -57,6 +48,18 @@ module Keisan
57
48
  ast_function
58
49
  end
59
50
  end
51
+
52
+ def differentiate(ast_function, variable, context = nil)
53
+ context ||= Context.new
54
+ Keisan::AST::Function.new(
55
+ [
56
+ ast_function.children[0],
57
+ ast_function.children[1].differentiate(variable, context),
58
+ ast_function.children[2].differentiate(variable, context)
59
+ ],
60
+ @name
61
+ )
62
+ end
60
63
  end
61
64
  end
62
65
  end