keisan 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +92 -0
  3. data/lib/keisan.rb +12 -0
  4. data/lib/keisan/ast/bitwise_and.rb +1 -5
  5. data/lib/keisan/ast/bitwise_or.rb +1 -5
  6. data/lib/keisan/ast/bitwise_xor.rb +1 -5
  7. data/lib/keisan/ast/builder.rb +57 -22
  8. data/lib/keisan/ast/exponent.rb +1 -5
  9. data/lib/keisan/ast/function.rb +50 -1
  10. data/lib/keisan/ast/logical_and.rb +1 -5
  11. data/lib/keisan/ast/logical_equal.rb +18 -0
  12. data/lib/keisan/ast/logical_greater_than.rb +4 -4
  13. data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +4 -4
  14. data/lib/keisan/ast/logical_less_than.rb +4 -4
  15. data/lib/keisan/ast/logical_less_than_or_equal_to.rb +4 -4
  16. data/lib/keisan/ast/logical_not_equal.rb +18 -0
  17. data/lib/keisan/ast/logical_or.rb +1 -5
  18. data/lib/keisan/ast/modulo.rb +18 -0
  19. data/lib/keisan/ast/node.rb +26 -0
  20. data/lib/keisan/ast/operator.rb +9 -2
  21. data/lib/keisan/ast/plus.rb +1 -5
  22. data/lib/keisan/ast/priorities.rb +27 -0
  23. data/lib/keisan/ast/times.rb +1 -5
  24. data/lib/keisan/ast/variable.rb +5 -0
  25. data/lib/keisan/calculator.rb +1 -12
  26. data/lib/keisan/context.rb +20 -5
  27. data/lib/keisan/evaluator.rb +79 -0
  28. data/lib/keisan/exceptions.rb +1 -0
  29. data/lib/keisan/functions/default_registry.rb +0 -5
  30. data/lib/keisan/functions/registry.rb +7 -0
  31. data/lib/keisan/parser.rb +42 -29
  32. data/lib/keisan/parsing/dot.rb +6 -0
  33. data/lib/keisan/parsing/dot_operator.rb +12 -0
  34. data/lib/keisan/parsing/dot_word.rb +14 -0
  35. data/lib/keisan/parsing/logical_equal.rb +9 -0
  36. data/lib/keisan/parsing/logical_not_equal.rb +9 -0
  37. data/lib/keisan/parsing/modulo.rb +9 -0
  38. data/lib/keisan/tokenizer.rb +2 -1
  39. data/lib/keisan/tokens/arithmetic_operator.rb +4 -1
  40. data/lib/keisan/tokens/dot.rb +11 -0
  41. data/lib/keisan/tokens/logical_operator.rb +7 -1
  42. data/lib/keisan/variables/registry.rb +7 -0
  43. data/lib/keisan/version.rb +1 -1
  44. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 853e03daadbd41d992a803a03c49183858f8190c
4
- data.tar.gz: '098542c4dbe94d7b95ae4b7e56c7a5d29116a3f5'
3
+ metadata.gz: ca29cf77de26f26d32436c705ad47f229e215c43
4
+ data.tar.gz: cedc1ffcca1d1a46fbca4cee21750f2398c84ee5
5
5
  SHA512:
6
- metadata.gz: 55efb1ed9bf111ac270cc6d91810f9ecfed944b6b6fa630fc63908f826f26ae806b17eb9908c48266c41e3bbedfb67c1a21ca1315cbc04441523f007c846f93e
7
- data.tar.gz: 5a6f72a02901a01ee2705bc3d12a8cc661cde9961375007bec235a40a78f402ca93b6a8417b26c04c826efb67137936b5da90c6a336d160398130f7ffa11e984
6
+ metadata.gz: fae0c6a0db729b392ea89440dc0b4dff033a9f0e7159f725ba7bcaa0b3e16861c1aebfec2d2cdcfc5cde719aee995caabfb7fafa865797732a8db6c4b184ced7
7
+ data.tar.gz: 02257ba4395e73e2950ff09eefa272cdd49e1299f4bcec5a558c73e3a87147a3e09b4cde59bc4fa1ab935d801d88e9282dc4cea97e53b497cb94155154c23fd8
data/README.md CHANGED
@@ -51,6 +51,14 @@ calculator.evaluate("x + 1")
51
51
  #=> Keisan::Exceptions::UndefinedVariableError: x
52
52
  ```
53
53
 
54
+ It is also possible to define variables in the string expression itself
55
+
56
+ ```ruby
57
+ calculator.evaluate("x = 10*n", n: 2)
58
+ calculator.evaluate("3*x + 1")
59
+ #=> 61
60
+ ```
61
+
54
62
  ##### Specifying functions
55
63
 
56
64
  Just like variables, functions can be defined by passing a `Proc` object as follows
@@ -67,6 +75,57 @@ calculator.evaluate("f(2) + 1")
67
75
  #=> Keisan::Exceptions::UndefinedFunctionError: f
68
76
  ```
69
77
 
78
+ Note that functions work in both regular (`f(x)`) and postfix (`x.f()`) notation. The postfix notation requires the function to take at least one argument. In the case of `a.f(b,c)`, this is translated internally to `f(a,b,c)`. If there is only a single argument to the function, the braces can be left off: `x.f`.
79
+
80
+ ```ruby
81
+ calculator.evaluate("[1,3,5,7].size()")
82
+ #=> 4
83
+ calculator.evaluate("[1,3,5,7].size")
84
+ #=> 4
85
+ ```
86
+
87
+ It is even possible to do more complicated things like follows
88
+
89
+ ```ruby
90
+ calculator.define_function!("f", Proc.new {|x| [[x-1,x+1], [x-2,x,x+2]]})
91
+ calculator.evaluate("4.f")
92
+ #=> [[3,5], [2,4,6]]
93
+ calculator.evaluate("4.f[0]")
94
+ #=> [3,5]
95
+ calculator.evaluate("4.f[0].size")
96
+ #=> 2
97
+ calculator.evaluate("4.f[1]")
98
+ #=> [2,4,6]
99
+ calculator.evaluate("4.f[1].size")
100
+ #=> 3
101
+ ```
102
+
103
+ Like variables, it is also possible to define functions in the string expression itself.
104
+
105
+ ```ruby
106
+ calculator.evaluate("f(x) = n*x", n: 10) # n is local to this definition only
107
+ calculator.evaluate("f(3)")
108
+ #=> 30
109
+ calculator.evaluate("f(0-a)", a: 2)
110
+ #=> -20
111
+ calculator.evaluate("n") # n only exists in the definition of f(x)
112
+ #=> Keisan::Exceptions::UndefinedVariableError: n
113
+ ```
114
+
115
+ This form even supports recursion!
116
+
117
+ ```ruby
118
+ calculator.evaluate("my_fact(n) = if (n > 1, n*my_fact(n-1), 1)")
119
+ calculator.evaluate("my_fact(0)")
120
+ #=> 1
121
+ calculator.evaluate("my_fact(1)")
122
+ #=> 1
123
+ calculator.evaluate("my_fact(2)")
124
+ #=> 2
125
+ calculator.evaluate("my_fact(5)")
126
+ #=> 120
127
+ ```
128
+
70
129
  ##### Lists
71
130
 
72
131
  Just like in Ruby, lists can be defined using square brackets, and indexed using square brackets
@@ -213,6 +272,39 @@ calculator.evaluate("f(2)")
213
272
  #=> 6
214
273
  ```
215
274
 
275
+ ## Supported elements/operators
276
+
277
+ `keisan` supports the following operators and elements.
278
+
279
+ #### Numbers, variables, functions, lists
280
+ - `150`, `-5.67`, `6e-5`: regular numbers
281
+ - `x`, `_myvar1`: variables
282
+ - `(` and `)`: round brackets for grouping parts to evaluate first
283
+ - `[0, 3, 6, 9]`: square brackets with comma separated values to denote lists
284
+ - `f(x,y,z)`, `my_function(max([2.5, 5.5]))`, `[2,4,6,8].size`: functions using `(` `)` brackets (optional if using postfix notation and only takes a single argument)
285
+
286
+ #### Arithmetic operators
287
+ - `+`, `-`, `*`, `/`: regular arithmetic operators
288
+ - `**`: Ruby style exponent notation (to avoid conflict with bitwise xor `^`)
289
+ - `%`: Ruby modulo operator, sign of `a % b` is same as sign of `b`
290
+ - `+`, `-`: Unary plus and minus
291
+
292
+ #### Logical operators
293
+ - `<`, `>`, `<=`, `>=`: comparison operators
294
+ - `==` and `!=`: logical equality check operators
295
+ - `&&` and `||`: logical operators, **and** and **or**
296
+ - `!`: unary logical not
297
+
298
+ #### Bitwise operators
299
+ - `&`, `|`, `^`: bitwise **and**, **or**, **xor** operators
300
+ - `~`: unary bitwise not
301
+
302
+ #### Indexing of arrays
303
+ - `list[i]`: for accessing elements in an array
304
+
305
+ #### Assignment
306
+ - `=`: can be used to define variables and functions
307
+
216
308
  ## Development
217
309
 
218
310
  After checking out the repository, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
data/lib/keisan.rb CHANGED
@@ -11,6 +11,7 @@ require "keisan/variables/registry"
11
11
  require "keisan/variables/default_registry"
12
12
  require "keisan/context"
13
13
 
14
+ require "keisan/ast/priorities"
14
15
  require "keisan/ast/node"
15
16
 
16
17
  require "keisan/ast/literal"
@@ -33,6 +34,7 @@ require "keisan/ast/arithmetic_operator"
33
34
  require "keisan/ast/plus"
34
35
  require "keisan/ast/times"
35
36
  require "keisan/ast/exponent"
37
+ require "keisan/ast/modulo"
36
38
  require "keisan/ast/function"
37
39
  require "keisan/ast/bitwise_operator"
38
40
  require "keisan/ast/bitwise_and"
@@ -41,6 +43,8 @@ require "keisan/ast/bitwise_xor"
41
43
  require "keisan/ast/logical_operator"
42
44
  require "keisan/ast/logical_and"
43
45
  require "keisan/ast/logical_or"
46
+ require "keisan/ast/logical_equal"
47
+ require "keisan/ast/logical_not_equal"
44
48
  require "keisan/ast/logical_less_than"
45
49
  require "keisan/ast/logical_greater_than"
46
50
  require "keisan/ast/logical_less_than_or_equal_to"
@@ -53,6 +57,7 @@ require "keisan/ast/builder"
53
57
 
54
58
  require "keisan/token"
55
59
  require "keisan/tokens/comma"
60
+ require "keisan/tokens/dot"
56
61
  require "keisan/tokens/group"
57
62
  require "keisan/tokens/number"
58
63
  require "keisan/tokens/operator"
@@ -73,6 +78,9 @@ require "keisan/parsing/number"
73
78
  require "keisan/parsing/string"
74
79
  require "keisan/parsing/null"
75
80
  require "keisan/parsing/boolean"
81
+ require "keisan/parsing/dot"
82
+ require "keisan/parsing/dot_word"
83
+ require "keisan/parsing/dot_operator"
76
84
  require "keisan/parsing/variable"
77
85
  require "keisan/parsing/function"
78
86
  require "keisan/parsing/group"
@@ -93,6 +101,7 @@ require "keisan/parsing/minus"
93
101
  require "keisan/parsing/times"
94
102
  require "keisan/parsing/divide"
95
103
  require "keisan/parsing/exponent"
104
+ require "keisan/parsing/modulo"
96
105
  require "keisan/parsing/bitwise_operator"
97
106
  require "keisan/parsing/bitwise_and"
98
107
  require "keisan/parsing/bitwise_or"
@@ -106,12 +115,15 @@ require "keisan/parsing/logical_less_than_or_equal_to"
106
115
  require "keisan/parsing/logical_greater_than_or_equal_to"
107
116
  require "keisan/parsing/logical_and"
108
117
  require "keisan/parsing/logical_or"
118
+ require "keisan/parsing/logical_equal"
119
+ require "keisan/parsing/logical_not_equal"
109
120
  require "keisan/parsing/logical_not"
110
121
  require "keisan/parsing/logical_not_not"
111
122
 
112
123
  require "keisan/parser"
113
124
 
114
125
  require "keisan/calculator"
126
+ require "keisan/evaluator"
115
127
 
116
128
  module Keisan
117
129
  end
@@ -1,15 +1,11 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class BitwiseAnd < BitwiseOperator
4
- def self.priority
5
- 31
6
- end
7
-
8
4
  def arity
9
5
  2..Float::INFINITY
10
6
  end
11
7
 
12
- def symbol
8
+ def self.symbol
13
9
  :"&"
14
10
  end
15
11
 
@@ -1,15 +1,11 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class BitwiseOr < BitwiseOperator
4
- def self.priority
5
- 11
6
- end
7
-
8
4
  def arity
9
5
  2..Float::INFINITY
10
6
  end
11
7
 
12
- def symbol
8
+ def self.symbol
13
9
  :"|"
14
10
  end
15
11
 
@@ -1,15 +1,11 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class BitwiseXor < BitwiseOperator
4
- def self.priority
5
- 21
6
- end
7
-
8
4
  def arity
9
5
  2..Float::INFINITY
10
6
  end
11
7
 
12
- def symbol
8
+ def self.symbol
13
9
  :"^"
14
10
  end
15
11
 
@@ -44,16 +44,11 @@ module Keisan
44
44
  end
45
45
 
46
46
  def node_from_components(components)
47
- unary_components, node, indexing_components = *unarys_node_indexings(components)
47
+ unary_components, node, postfix_components = *unarys_node_postfixes(components)
48
48
 
49
- # Apply postfix indexing operators
50
- indexing_components.each do |indexing_component|
51
- node = indexing_component.node_class.new(
52
- node,
53
- indexing_component.arguments.map {|parsing_argument|
54
- Builder.new(components: parsing_argument.components).node
55
- }
56
- )
49
+ # Apply postfix operators
50
+ postfix_components.each do |postfix_component|
51
+ node = apply_postfix_component_to_node(postfix_component, node)
57
52
  end
58
53
 
59
54
  # Apply prefix unary operators
@@ -64,34 +59,62 @@ module Keisan
64
59
  node
65
60
  end
66
61
 
62
+ def apply_postfix_component_to_node(postfix_component, node)
63
+ case postfix_component
64
+ when Keisan::Parsing::Indexing
65
+ postfix_component.node_class.new(
66
+ node,
67
+ postfix_component.arguments.map {|parsing_argument|
68
+ Builder.new(components: parsing_argument.components).node
69
+ }
70
+ )
71
+ when Keisan::Parsing::DotWord
72
+ Keisan::AST::Function.build(
73
+ postfix_component.name,
74
+ [node]
75
+ )
76
+ when Keisan::Parsing::DotOperator
77
+ Keisan::AST::Function.build(
78
+ postfix_component.name,
79
+ [node] + postfix_component.arguments.map {|parsing_argument|
80
+ Builder.new(components: parsing_argument.components).node
81
+ }
82
+ )
83
+ else
84
+ raise Keisan::Exceptions::ASTError.new("Invalid postfix component #{postfix_component}")
85
+ end
86
+ end
87
+
67
88
  # Returns an array of the form
68
- # [unary_operators, middle_node, postfix_indexings]
89
+ # [unary_operators, middle_node, postfix_operators]
69
90
  # unary_operators is an array of Keisan::Parsing::UnaryOperator objects
70
91
  # middle_node is the main node which will be modified by prefix and postfix operators
71
- # postfix_indexings is an array of Keisan::Parsing::Indexing objects
72
- def unarys_node_indexings(components)
92
+ # postfix_operators is an array of Keisan::Parsing::Indexing, DotWord, and DotOperator objects
93
+ def unarys_node_postfixes(components)
73
94
  index_of_unary_components = components.map.with_index {|c,i| [c,i]}.select {|c,i| c.is_a?(Keisan::Parsing::UnaryOperator)}.map(&:last)
74
95
  # Must be all in the front
75
96
  unless index_of_unary_components.map.with_index.all? {|i,j| i == j}
76
97
  raise Keisan::Exceptions::ASTError.new("unary operators must be in front")
77
98
  end
78
99
 
79
- index_of_indexing_components = components.map.with_index {|c,i| [c,i]}.select {|c,i| c.is_a?(Keisan::Parsing::Indexing)}.map(&:last)
80
- unless index_of_indexing_components.reverse.map.with_index.all? {|i,j| i + j == components.size - 1 }
81
- raise Keisan::Exceptions::ASTError.new("indexing components must be in back")
100
+ index_of_postfix_components = components.map.with_index {|c,i| [c,i]}.select {|c,i|
101
+ c.is_a?(Keisan::Parsing::Indexing) || c.is_a?(Keisan::Parsing::DotWord) || c.is_a?(Keisan::Parsing::DotOperator)
102
+ }.map(&:last)
103
+ unless index_of_postfix_components.reverse.map.with_index.all? {|i,j| i + j == components.size - 1 }
104
+ raise Keisan::Exceptions::ASTError.new("postfix components must be in back")
82
105
  end
83
106
 
84
- num_unary = index_of_unary_components.size
85
- num_indexing = index_of_indexing_components.size
107
+ num_unary = index_of_unary_components.size
108
+ num_postfix = index_of_postfix_components.size
86
109
 
87
- unless num_unary + 1 + num_indexing == components.size
110
+ unless num_unary + 1 + num_postfix == components.size
88
111
  raise Keisan::Exceptions::ASTError.new("have too many components")
89
112
  end
90
113
 
91
114
  [
92
115
  index_of_unary_components.map {|i| components[i]},
93
116
  node_of_component(components[index_of_unary_components.size]),
94
- index_of_indexing_components.map {|i| components[i]}
117
+ index_of_postfix_components.map {|i| components[i]}
95
118
  ]
96
119
  end
97
120
 
@@ -116,11 +139,23 @@ module Keisan
116
139
  when Keisan::Parsing::Group
117
140
  Builder.new(components: component.components).node
118
141
  when Keisan::Parsing::Function
119
- Keisan::AST::Function.new(
142
+ Keisan::AST::Function.build(
143
+ component.name,
120
144
  component.arguments.map {|parsing_argument|
121
145
  Builder.new(components: parsing_argument.components).node
122
- },
123
- component.name
146
+ }
147
+ )
148
+ when Keisan::Parsing::DotWord
149
+ Keisan::AST::Function.build(
150
+ component.name,
151
+ [node_of_component(component.target)]
152
+ )
153
+ when Keisan::Parsing::DotOperator
154
+ Keisan::AST::Function.build(
155
+ component.name,
156
+ [node_of_component(component.target)] + component.arguments.map {|parsing_argument|
157
+ Builder.new(components: parsing_argument.components).node
158
+ }
124
159
  )
125
160
  else
126
161
  raise Keisan::Exceptions::ASTError.new("Unhandled component, #{component}")
@@ -1,10 +1,6 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class Exponent < ArithmeticOperator
4
- def self.priority
5
- 30
6
- end
7
-
8
4
  def arity
9
5
  (2..2)
10
6
  end
@@ -13,7 +9,7 @@ module Keisan
13
9
  :right
14
10
  end
15
11
 
16
- def symbol
12
+ def self.symbol
17
13
  :**
18
14
  end
19
15
 
@@ -11,9 +11,58 @@ module Keisan
11
11
  def value(context = nil)
12
12
  context = Keisan::Context.new if context.nil?
13
13
  argument_values = children.map {|child| child.value(context)}
14
- function = context.function(name)
14
+ function = function_from_context(context)
15
15
  function.call(context, *argument_values)
16
16
  end
17
+
18
+ def unbound_functions(context = nil)
19
+ context ||= Keisan::Context.new
20
+
21
+ functions = children.inject(Set.new) do |res, child|
22
+ res | child.unbound_functions(context)
23
+ end
24
+
25
+ context.has_function?(name) ? functions : functions | Set.new([name])
26
+ end
27
+
28
+ def function_from_context(context)
29
+ @override || context.function(name)
30
+ end
31
+ end
32
+
33
+ class If < Function
34
+ def value(context = nil)
35
+ unless (2..3).cover? children.size
36
+ raise Keisan::Exceptions::InvalidFunctionError.new("Require 2 or 3 arguments to if")
37
+ end
38
+
39
+ bool = children[0].value(context)
40
+
41
+ if bool
42
+ children[1].value(context)
43
+ else
44
+ children.size == 3 ? children[2].value(context) : nil
45
+ end
46
+ end
47
+
48
+ def unbound_functions(context = nil)
49
+ context ||= Keisan::Context.new
50
+
51
+ children.inject(Set.new) do |res, child|
52
+ res | child.unbound_functions(context)
53
+ end
54
+ end
55
+ end
56
+
57
+ class Function
58
+ def self.build(name, arguments = [])
59
+ case name.downcase
60
+ when "if"
61
+ If.new(arguments, name)
62
+ else
63
+ Function.new(arguments, name)
64
+ end
65
+ end
17
66
  end
18
67
  end
19
68
  end
@@ -1,15 +1,11 @@
1
1
  module Keisan
2
2
  module AST
3
3
  class LogicalAnd < LogicalOperator
4
- def self.priority
5
- 22
6
- end
7
-
8
4
  def arity
9
5
  2..Float::INFINITY
10
6
  end
11
7
 
12
- def symbol
8
+ def self.symbol
13
9
  :"&"
14
10
  end
15
11