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.
- 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
|