keisan 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +92 -0
- data/lib/keisan.rb +12 -0
- data/lib/keisan/ast/bitwise_and.rb +1 -5
- data/lib/keisan/ast/bitwise_or.rb +1 -5
- data/lib/keisan/ast/bitwise_xor.rb +1 -5
- data/lib/keisan/ast/builder.rb +57 -22
- data/lib/keisan/ast/exponent.rb +1 -5
- data/lib/keisan/ast/function.rb +50 -1
- data/lib/keisan/ast/logical_and.rb +1 -5
- data/lib/keisan/ast/logical_equal.rb +18 -0
- data/lib/keisan/ast/logical_greater_than.rb +4 -4
- data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +4 -4
- data/lib/keisan/ast/logical_less_than.rb +4 -4
- data/lib/keisan/ast/logical_less_than_or_equal_to.rb +4 -4
- data/lib/keisan/ast/logical_not_equal.rb +18 -0
- data/lib/keisan/ast/logical_or.rb +1 -5
- data/lib/keisan/ast/modulo.rb +18 -0
- data/lib/keisan/ast/node.rb +26 -0
- data/lib/keisan/ast/operator.rb +9 -2
- data/lib/keisan/ast/plus.rb +1 -5
- data/lib/keisan/ast/priorities.rb +27 -0
- data/lib/keisan/ast/times.rb +1 -5
- data/lib/keisan/ast/variable.rb +5 -0
- data/lib/keisan/calculator.rb +1 -12
- data/lib/keisan/context.rb +20 -5
- data/lib/keisan/evaluator.rb +79 -0
- data/lib/keisan/exceptions.rb +1 -0
- data/lib/keisan/functions/default_registry.rb +0 -5
- data/lib/keisan/functions/registry.rb +7 -0
- data/lib/keisan/parser.rb +42 -29
- data/lib/keisan/parsing/dot.rb +6 -0
- data/lib/keisan/parsing/dot_operator.rb +12 -0
- data/lib/keisan/parsing/dot_word.rb +14 -0
- data/lib/keisan/parsing/logical_equal.rb +9 -0
- data/lib/keisan/parsing/logical_not_equal.rb +9 -0
- data/lib/keisan/parsing/modulo.rb +9 -0
- data/lib/keisan/tokenizer.rb +2 -1
- data/lib/keisan/tokens/arithmetic_operator.rb +4 -1
- data/lib/keisan/tokens/dot.rb +11 -0
- data/lib/keisan/tokens/logical_operator.rb +7 -1
- data/lib/keisan/variables/registry.rb +7 -0
- data/lib/keisan/version.rb +1 -1
- metadata +14 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ca29cf77de26f26d32436c705ad47f229e215c43
|
4
|
+
data.tar.gz: cedc1ffcca1d1a46fbca4cee21750f2398c84ee5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/keisan/ast/builder.rb
CHANGED
@@ -44,16 +44,11 @@ module Keisan
|
|
44
44
|
end
|
45
45
|
|
46
46
|
def node_from_components(components)
|
47
|
-
unary_components, node,
|
47
|
+
unary_components, node, postfix_components = *unarys_node_postfixes(components)
|
48
48
|
|
49
|
-
# Apply postfix
|
50
|
-
|
51
|
-
node =
|
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,
|
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
|
-
#
|
72
|
-
def
|
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
|
-
|
80
|
-
|
81
|
-
|
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
|
85
|
-
|
107
|
+
num_unary = index_of_unary_components.size
|
108
|
+
num_postfix = index_of_postfix_components.size
|
86
109
|
|
87
|
-
unless num_unary + 1 +
|
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
|
-
|
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.
|
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
|
-
|
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}")
|
data/lib/keisan/ast/exponent.rb
CHANGED
data/lib/keisan/ast/function.rb
CHANGED
@@ -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
|
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
|