keisan 0.2.1 → 0.3.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 (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