keisan 0.3.0 → 0.4.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 (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