keisan 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +57 -9
  3. data/lib/keisan.rb +21 -12
  4. data/lib/keisan/ast.rb +50 -0
  5. data/lib/keisan/ast/arithmetic_operator.rb +0 -3
  6. data/lib/keisan/ast/assignment.rb +72 -0
  7. data/lib/keisan/ast/bitwise_and.rb +4 -4
  8. data/lib/keisan/ast/bitwise_operator.rb +0 -3
  9. data/lib/keisan/ast/bitwise_or.rb +4 -4
  10. data/lib/keisan/ast/bitwise_xor.rb +4 -4
  11. data/lib/keisan/ast/boolean.rb +25 -1
  12. data/lib/keisan/ast/builder.rb +98 -63
  13. data/lib/keisan/ast/constant_literal.rb +13 -0
  14. data/lib/keisan/ast/exponent.rb +62 -8
  15. data/lib/keisan/ast/function.rb +37 -26
  16. data/lib/keisan/ast/functions/diff.rb +57 -0
  17. data/lib/keisan/ast/functions/if.rb +47 -0
  18. data/lib/keisan/ast/indexing.rb +44 -4
  19. data/lib/keisan/ast/list.rb +4 -0
  20. data/lib/keisan/ast/literal.rb +8 -0
  21. data/lib/keisan/ast/logical_and.rb +9 -4
  22. data/lib/keisan/ast/logical_equal.rb +4 -4
  23. data/lib/keisan/ast/logical_greater_than.rb +4 -4
  24. data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +4 -4
  25. data/lib/keisan/ast/logical_less_than.rb +4 -4
  26. data/lib/keisan/ast/logical_less_than_or_equal_to.rb +4 -4
  27. data/lib/keisan/ast/logical_not_equal.rb +4 -4
  28. data/lib/keisan/ast/logical_operator.rb +0 -3
  29. data/lib/keisan/ast/logical_or.rb +10 -5
  30. data/lib/keisan/ast/modulo.rb +4 -4
  31. data/lib/keisan/ast/node.rb +132 -20
  32. data/lib/keisan/ast/null.rb +1 -1
  33. data/lib/keisan/ast/number.rb +172 -1
  34. data/lib/keisan/ast/operator.rb +66 -8
  35. data/lib/keisan/ast/parent.rb +50 -0
  36. data/lib/keisan/ast/plus.rb +38 -4
  37. data/lib/keisan/ast/string.rb +10 -1
  38. data/lib/keisan/ast/times.rb +47 -4
  39. data/lib/keisan/ast/unary_bitwise_not.rb +9 -1
  40. data/lib/keisan/ast/unary_identity.rb +18 -1
  41. data/lib/keisan/ast/unary_inverse.rb +35 -1
  42. data/lib/keisan/ast/unary_logical_not.rb +5 -1
  43. data/lib/keisan/ast/unary_minus.rb +31 -1
  44. data/lib/keisan/ast/unary_operator.rb +29 -1
  45. data/lib/keisan/ast/unary_plus.rb +14 -1
  46. data/lib/keisan/ast/variable.rb +44 -0
  47. data/lib/keisan/calculator.rb +2 -2
  48. data/lib/keisan/context.rb +32 -16
  49. data/lib/keisan/evaluator.rb +10 -65
  50. data/lib/keisan/exceptions.rb +2 -0
  51. data/lib/keisan/function.rb +11 -5
  52. data/lib/keisan/function_definition_context.rb +34 -0
  53. data/lib/keisan/functions/default_registry.rb +13 -5
  54. data/lib/keisan/functions/diff.rb +82 -0
  55. data/lib/keisan/functions/expression_function.rb +63 -0
  56. data/lib/keisan/functions/if.rb +62 -0
  57. data/lib/keisan/functions/proc_function.rb +52 -0
  58. data/lib/keisan/functions/rand.rb +1 -1
  59. data/lib/keisan/functions/registry.rb +20 -10
  60. data/lib/keisan/functions/replace.rb +49 -0
  61. data/lib/keisan/functions/sample.rb +1 -1
  62. data/lib/keisan/parser.rb +13 -1
  63. data/lib/keisan/parsing/assignment.rb +9 -0
  64. data/lib/keisan/parsing/operator.rb +8 -0
  65. data/lib/keisan/parsing/unary_operator.rb +1 -1
  66. data/lib/keisan/tokenizer.rb +1 -0
  67. data/lib/keisan/tokens/assignment.rb +15 -0
  68. data/lib/keisan/variables/default_registry.rb +4 -4
  69. data/lib/keisan/variables/registry.rb +17 -8
  70. data/lib/keisan/version.rb +1 -1
  71. metadata +15 -3
  72. data/lib/keisan/ast/priorities.rb +0 -27
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ca29cf77de26f26d32436c705ad47f229e215c43
4
- data.tar.gz: cedc1ffcca1d1a46fbca4cee21750f2398c84ee5
3
+ metadata.gz: 32b53dd19dc29bced6d98d110f7d11c0a7d22ea5
4
+ data.tar.gz: d55ec179a6f8b1e597cb322e42bab4ed0b959fed
5
5
  SHA512:
6
- metadata.gz: fae0c6a0db729b392ea89440dc0b4dff033a9f0e7159f725ba7bcaa0b3e16861c1aebfec2d2cdcfc5cde719aee995caabfb7fafa865797732a8db6c4b184ced7
7
- data.tar.gz: 02257ba4395e73e2950ff09eefa272cdd49e1299f4bcec5a558c73e3a87147a3e09b4cde59bc4fa1ab935d801d88e9282dc4cea97e53b497cb94155154c23fd8
6
+ metadata.gz: a4c87fabd551121df563306488410003e9291b1569330ca3da65d833b9e9e97f925dcbc3d07e23cb9c5e05272b8fe74028766388064e0713ff66d625261a5752
7
+ data.tar.gz: cdfef6449002d021fe83b10457959e5d1bccf81aa569be908e6ba98026f0adc2df44eeb3a60b6d013126e6d278992cd8a50b4c397c11460656a33574f0234ad7
data/README.md CHANGED
@@ -112,9 +112,14 @@ calculator.evaluate("n") # n only exists in the definition of f(x)
112
112
  #=> Keisan::Exceptions::UndefinedVariableError: n
113
113
  ```
114
114
 
115
- This form even supports recursion!
115
+ This form even supports recursion, but you must explicitly allow it.
116
116
 
117
117
  ```ruby
118
+ calculator = Keisan::Calculator.new(allow_recursive: false)
119
+ calculator.evaluate("my_fact(n) = if (n > 1, n*my_fact(n-1), 1)")
120
+ #=> Keisan::Exceptions::InvalidExpression: Unbound function definitions are not allowed by current context
121
+
122
+ calculator = Keisan::Calculator.new(allow_recursive: true)
118
123
  calculator.evaluate("my_fact(n) = if (n > 1, n*my_fact(n-1), 1)")
119
124
  calculator.evaluate("my_fact(0)")
120
125
  #=> 1
@@ -190,7 +195,7 @@ Using the prefixes `0b`, `0o`, and `0x` (standard in Ruby) indicates binary, oct
190
195
  calculator.evaluate("0b1100")
191
196
  #=> 12
192
197
  calculator.evaluate("0o775")
193
- #=> 504
198
+ #=> 509
194
199
  calculator.evaluate("0x1f0")
195
200
  #=> 496
196
201
  ```
@@ -209,8 +214,8 @@ calculator.evaluate("sample([2, 4, 6, 8])")
209
214
  If you want reproducibility, you can pass in your own `Random` object to the calculator's context.
210
215
 
211
216
  ```ruby
212
- calculator1 = Keisan::Calculator.new(Keisan::Context.new(random: Random.new(1234)))
213
- calculator2 = Keisan::Calculator.new(Keisan::Context.new(random: Random.new(1234)))
217
+ calculator1 = Keisan::Calculator.new(context: Keisan::Context.new(random: Random.new(1234)))
218
+ calculator2 = Keisan::Calculator.new(context: Keisan::Context.new(random: Random.new(1234)))
214
219
  5.times.map {calculator1.evaluate("rand(1000)")}
215
220
  #=> [815, 723, 294, 53, 204]
216
221
  5.times.map {calculator2.evaluate("rand(1000)")}
@@ -229,24 +234,48 @@ calculator.evaluate("log10(1000)")
229
234
  Furthermore, the following builtin constants are defined
230
235
 
231
236
  ```ruby
232
- calculator.evaluate("pi")
237
+ calculator.evaluate("PI")
233
238
  #=> 3.141592653589793
234
- calculator.evaluate("e")
239
+ calculator.evaluate("E")
235
240
  #=> 2.718281828459045
236
- calculator.evaluate("i")
241
+ calculator.evaluate("I")
237
242
  #=> (0+1i)
238
243
  ```
239
244
 
240
245
  This allows for simple calculations like
241
246
 
242
247
  ```ruby
243
- calculator.evaluate("e**(i*pi)+1")
248
+ calculator.evaluate("E**(I*PI)+1")
244
249
  => (0.0+0.0i)
245
250
  ```
246
251
 
252
+ There is a `replace` method that can replace instances of a variable in an expression with another expression. The form is `replace(original_expression, variable_to_replace, replacement_expression)`. Before the replacement is carried out, the `original_expression` and `replacement_expression` are `evaluate`d, then instances in the original expression of the given variable are replaced by the replacement expression.
253
+
254
+ ```ruby
255
+ calculator.evaluate("replace(x**2, x, 3)")
256
+ #=> 9
257
+ ```
258
+
259
+ When using `Calculator` class, all variables must be replaced before an expression can be calculated, but the ability to replace any expression is useful when working directly with the AST.
260
+
261
+ ```ruby
262
+ ast = Keisan::AST.parse("replace(replace(x**2 + y**2, x, sin(theta)), y, cos(theta))")
263
+ ast.evaluate.to_s
264
+ #=> "(sin(theta)**2)+(cos(theta)**2)"
265
+ ```
266
+
267
+ The derivative operation is also builtin to Keisan as the `diff` function.
268
+
269
+ ```ruby
270
+ calculator = Keisan::Calculator.new
271
+ calculator.evaluate("diff(4*x, x)")
272
+ calculator.evaluate("replace(diff(4*x**2, x), x, 3)")
273
+ #=> 24
274
+ ```
275
+
247
276
  ### Adding custom variables and functions
248
277
 
249
- The `Keisan::Calculator` class has a single `Keisan::Context` object in its `context` attribute. This class is used to store local variables and functions. As an example of pre-defining some variables and functions, see the following
278
+ The `Keisan::Calculator` class has a single `Keisan::Context` object in its `context` attribute. This class is used to store local variables and functions. These can be stored using either the `define_variable!` or `define_function!` methods, or by using the assignment operator `=` in an expression that is evaluated. As an example of pre-defining some variables and functions, see the following
250
279
 
251
280
  ```ruby
252
281
  calculator.define_variable!("x", 5)
@@ -257,11 +286,21 @@ calculator.evaluate("x + 1", x: 10)
257
286
  #=> 11
258
287
  calculator.evaluate("x + 1")
259
288
  #=> 6
289
+
290
+ calculator.evaluate("x = y = 10")
291
+ #=> 10
292
+ calculator.evaluate("x + y")
293
+ #=> 20
294
+ calculator.evaluate("x + y", y: 100)
295
+ #=> 110
296
+ calculator.evaluate("x + y")
297
+ #=> 20
260
298
  ```
261
299
 
262
300
  Notice how when passing variable values directly to the `evaluate` method, it only shadows the value of 5 for that specific calculation. The same thing works for functions
263
301
 
264
302
  ```ruby
303
+ calculator = Keisan::Calculator.new
265
304
  calculator.define_function!("f", Proc.new {|x| 3*x})
266
305
  #=> #<Keisan::Function:0x005570f935ecc8 @function_proc=#<Proc:0x005570f935ecf0@(pry):6>, @name="f">
267
306
  calculator.evaluate("f(2)")
@@ -270,6 +309,15 @@ calculator.evaluate("f(2)", f: Proc.new {|x| 10*x})
270
309
  #=> 20
271
310
  calculator.evaluate("f(2)")
272
311
  #=> 6
312
+
313
+ calculator.evaluate("f(x) = x + x**2")
314
+ #=> nil
315
+ calculator.evaluate("f(3)")
316
+ #=> 12
317
+ calculator.evaluate("f(3)", f: Proc.new {|x| 10*x})
318
+ #=> 30
319
+ calculator.evaluate("f(3)")
320
+ #=> 12
273
321
  ```
274
322
 
275
323
  ## Supported elements/operators
@@ -4,32 +4,26 @@ require "active_support/core_ext"
4
4
  require "keisan/version"
5
5
  require "keisan/exceptions"
6
6
 
7
- require "keisan/function"
8
- require "keisan/functions/registry"
9
- require "keisan/functions/default_registry"
10
- require "keisan/variables/registry"
11
- require "keisan/variables/default_registry"
12
- require "keisan/context"
13
-
14
- require "keisan/ast/priorities"
15
7
  require "keisan/ast/node"
16
8
 
17
9
  require "keisan/ast/literal"
10
+ require "keisan/ast/variable"
11
+ require "keisan/ast/constant_literal"
18
12
  require "keisan/ast/number"
19
13
  require "keisan/ast/string"
20
14
  require "keisan/ast/null"
21
15
  require "keisan/ast/boolean"
22
- require "keisan/ast/variable"
23
16
 
24
17
  require "keisan/ast/parent"
18
+ require "keisan/ast/operator"
19
+ require "keisan/ast/assignment"
25
20
  require "keisan/ast/unary_operator"
21
+ require "keisan/ast/unary_identity"
26
22
  require "keisan/ast/unary_plus"
27
23
  require "keisan/ast/unary_minus"
28
24
  require "keisan/ast/unary_inverse"
29
25
  require "keisan/ast/unary_bitwise_not"
30
26
  require "keisan/ast/unary_logical_not"
31
- require "keisan/ast/unary_identity"
32
- require "keisan/ast/operator"
33
27
  require "keisan/ast/arithmetic_operator"
34
28
  require "keisan/ast/plus"
35
29
  require "keisan/ast/times"
@@ -54,6 +48,17 @@ require "keisan/ast/list"
54
48
  require "keisan/ast/indexing"
55
49
 
56
50
  require "keisan/ast/builder"
51
+ require "keisan/ast"
52
+
53
+ require "keisan/function"
54
+ require "keisan/functions/proc_function"
55
+ require "keisan/functions/expression_function"
56
+ require "keisan/functions/registry"
57
+ require "keisan/functions/default_registry"
58
+ require "keisan/variables/registry"
59
+ require "keisan/variables/default_registry"
60
+ require "keisan/context"
61
+ require "keisan/function_definition_context"
57
62
 
58
63
  require "keisan/token"
59
64
  require "keisan/tokens/comma"
@@ -64,6 +69,7 @@ require "keisan/tokens/operator"
64
69
  require "keisan/tokens/string"
65
70
  require "keisan/tokens/null"
66
71
  require "keisan/tokens/boolean"
72
+ require "keisan/tokens/assignment"
67
73
  require "keisan/tokens/arithmetic_operator"
68
74
  require "keisan/tokens/logical_operator"
69
75
  require "keisan/tokens/bitwise_operator"
@@ -90,11 +96,14 @@ require "keisan/parsing/list"
90
96
  require "keisan/parsing/indexing"
91
97
  require "keisan/parsing/argument"
92
98
 
99
+ require "keisan/parsing/operator"
100
+
101
+ require "keisan/parsing/assignment"
102
+
93
103
  require "keisan/parsing/unary_operator"
94
104
  require "keisan/parsing/unary_plus"
95
105
  require "keisan/parsing/unary_minus"
96
106
 
97
- require "keisan/parsing/operator"
98
107
  require "keisan/parsing/arithmetic_operator"
99
108
  require "keisan/parsing/plus"
100
109
  require "keisan/parsing/minus"
@@ -0,0 +1,50 @@
1
+ module Keisan
2
+ module AST
3
+ def self.parse(expression)
4
+ AST::Builder.new(string: expression).ast
5
+ end
6
+ end
7
+ end
8
+
9
+ module KeisanNumeric
10
+ def to_node
11
+ Keisan::AST::Number.new(self)
12
+ end
13
+ end
14
+
15
+ module KeisanString
16
+ def to_node
17
+ Keisan::AST::String.new(self)
18
+ end
19
+ end
20
+
21
+ module KeisanTrueClass
22
+ def to_node
23
+ Keisan::AST::Boolean.new(true)
24
+ end
25
+ end
26
+
27
+ module KeisanFalseClass
28
+ def to_node
29
+ Keisan::AST::Boolean.new(false)
30
+ end
31
+ end
32
+
33
+ module KeisanNilClass
34
+ def to_node
35
+ Keisan::AST::Null.new
36
+ end
37
+ end
38
+
39
+ module KeisanArray
40
+ def to_node
41
+ Keisan::AST::List.new(map {|n| n.to_node})
42
+ end
43
+ end
44
+
45
+ class Numeric; prepend KeisanNumeric; end
46
+ class String; prepend KeisanString; end
47
+ class TrueClass; prepend KeisanTrueClass; end
48
+ class FalseClass; prepend KeisanFalseClass; end
49
+ class NilClass; prepend KeisanNilClass; end
50
+ class Array; prepend KeisanArray; end
@@ -1,9 +1,6 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class ArithmeticOperator < Operator
4
- def associativity
5
- :left
6
- end
7
4
  end
8
5
  end
9
6
  end
@@ -0,0 +1,72 @@
1
+ module Keisan
2
+ module AST
3
+ class Assignment < Operator
4
+ def self.symbol
5
+ :"="
6
+ end
7
+
8
+ def evaluate(context = nil)
9
+ context ||= Keisan::Context.new
10
+
11
+ lhs = children.first
12
+ rhs = children.last
13
+
14
+ case lhs
15
+ when Keisan::AST::Variable
16
+ evaluate_variable(context, lhs, rhs)
17
+ when Keisan::AST::Function
18
+ evaluate_function(context, lhs, rhs)
19
+ else
20
+ raise Keisan::Exceptions::InvalidExpression.new("Unhandled left hand side #{lhs} in assignment")
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def evaluate_variable(context, lhs, rhs)
27
+ rhs = rhs.evaluate(context)
28
+
29
+ unless rhs.well_defined?
30
+ raise Keisan::Exceptions::InvalidExpression.new("Right hand side of assignment to variable must be well defined")
31
+ end
32
+
33
+ rhs_value = rhs.value(context)
34
+ context.register_variable!(lhs.name, rhs_value)
35
+ rhs
36
+ end
37
+
38
+ def evaluate_function(context, lhs, rhs)
39
+ unless lhs.children.all? {|arg| arg.is_a?(Keisan::AST::Variable)}
40
+ raise Keisan::Exceptions::InvalidExpression.new("Left hand side function must have variables as arguments")
41
+ end
42
+
43
+ argument_names = lhs.children.map(&:name)
44
+ function_definition_context = Keisan::FunctionDefinitionContext.new(
45
+ parent: context,
46
+ arguments: argument_names
47
+ )
48
+ rhs = rhs.evaluate(function_definition_context)
49
+
50
+ unless rhs.unbound_variables(context) <= Set.new(argument_names)
51
+ raise Keisan::Exceptions::InvalidExpression.new("Unbound variables found in function definition")
52
+ end
53
+
54
+ unless context.allow_recursive || rhs.unbound_functions(context).empty?
55
+ raise Keisan::Exceptions::InvalidExpression.new("Unbound function definitions are not allowed by current context")
56
+ end
57
+
58
+ context.register_function!(
59
+ lhs.name,
60
+ Keisan::Functions::ExpressionFunction.new(
61
+ lhs.name,
62
+ argument_names,
63
+ rhs,
64
+ function_definition_context
65
+ )
66
+ )
67
+
68
+ rhs
69
+ end
70
+ end
71
+ end
72
+ end
@@ -1,10 +1,6 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class BitwiseAnd < BitwiseOperator
4
- def arity
5
- 2..Float::INFINITY
6
- end
7
-
8
4
  def self.symbol
9
5
  :"&"
10
6
  end
@@ -12,6 +8,10 @@ module Keisan
12
8
  def blank_value
13
9
  ~0
14
10
  end
11
+
12
+ def evaluate(context = nil)
13
+ children[1..-1].inject(children.first.evaluate(context)) {|total, child| total & child.evaluate(context)}
14
+ end
15
15
  end
16
16
  end
17
17
  end
@@ -1,9 +1,6 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class BitwiseOperator < Operator
4
- def associativity
5
- :left
6
- end
7
4
  end
8
5
  end
9
6
  end
@@ -1,10 +1,6 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class BitwiseOr < BitwiseOperator
4
- def arity
5
- 2..Float::INFINITY
6
- end
7
-
8
4
  def self.symbol
9
5
  :"|"
10
6
  end
@@ -12,6 +8,10 @@ module Keisan
12
8
  def blank_value
13
9
  0
14
10
  end
11
+
12
+ def evaluate(context = nil)
13
+ children[1..-1].inject(children.first.evaluate(context)) {|total, child| total | child.evaluate(context)}
14
+ end
15
15
  end
16
16
  end
17
17
  end
@@ -1,10 +1,6 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class BitwiseXor < BitwiseOperator
4
- def arity
5
- 2..Float::INFINITY
6
- end
7
-
8
4
  def self.symbol
9
5
  :"^"
10
6
  end
@@ -12,6 +8,10 @@ module Keisan
12
8
  def blank_value
13
9
  0
14
10
  end
11
+
12
+ def evaluate(context = nil)
13
+ children[1..-1].inject(children.first.evaluate(context)) {|total, child| total ^ child.evaluate(context)}
14
+ end
15
15
  end
16
16
  end
17
17
  end
@@ -1,6 +1,6 @@
1
1
  module Keisan
2
2
  module AST
3
- class Boolean < Literal
3
+ class Boolean < ConstantLiteral
4
4
  attr_reader :bool
5
5
 
6
6
  def initialize(bool)
@@ -10,6 +10,30 @@ module Keisan
10
10
  def value(context = nil)
11
11
  bool
12
12
  end
13
+
14
+ def !
15
+ AST::Boolean.new(!bool)
16
+ end
17
+
18
+ def and(other)
19
+ other = other.to_node
20
+ case other
21
+ when AST::Boolean
22
+ AST::Boolean.new(bool && other.bool)
23
+ else
24
+ super
25
+ end
26
+ end
27
+
28
+ def or(other)
29
+ other = other.to_node
30
+ case other
31
+ when AST::Boolean
32
+ AST::Boolean.new(bool || other.bool)
33
+ else
34
+ super
35
+ end
36
+ end
13
37
  end
14
38
  end
15
39
  end