keisan 0.1.0 → 0.2.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/.gitignore +1 -0
- data/README.md +37 -1
- data/lib/keisan.rb +0 -1
- data/lib/keisan/ast/builder.rb +56 -36
- data/lib/keisan/calculator.rb +10 -8
- data/lib/keisan/context.rb +5 -1
- data/lib/keisan/functions/default_registry.rb +0 -9
- data/lib/keisan/functions/registry.rb +1 -1
- data/lib/keisan/parser.rb +4 -2
- data/lib/keisan/tokenizer.rb +18 -3
- data/lib/keisan/tokens/number.rb +7 -4
- data/lib/keisan/tokens/word.rb +1 -1
- data/lib/keisan/variables/registry.rb +1 -1
- data/lib/keisan/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cdf00ba25b7bc64cde85408ef0f56ad56bece933
|
4
|
+
data.tar.gz: ebccd470d71300d5968b2efaf38cd7dccd39b675
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 64889b4e5947a90b46ad1a404b8ee4401cc0ac34d7ef18ca26f150f44208dd61a130cc5c2fed6dcba3c5ffa9ce4ff36b26721e5c64516feb146d15afe8ebaf93
|
7
|
+
data.tar.gz: 114a84f7120b53c028d7f0fa5226aa01e034e593c911713bd5a394feef3ff436b52045c753a1c1dd796652f2cf9680b7c1d8a4dc6fa44fe5c53e2caedcd1775a
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# Keisan
|
2
2
|
|
3
|
+
[](https://badge.fury.io/rb/keisan)
|
3
4
|
[](https://travis-ci.org/project-eutopia/keisan)
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
5
6
|
[](https://hakiri.io/github/project-eutopia/keisan)
|
6
7
|
|
7
8
|
Keisan ([計算, to calculate](https://en.wiktionary.org/wiki/%E8%A8%88%E7%AE%97#Japanese)) is a Ruby library for parsing equations into an abstract syntax tree. This allows for safe evaluation of string representations of mathematical/logical expressions.
|
@@ -122,6 +123,41 @@ calculator.evaluate("'hello'[1]")
|
|
122
123
|
#=> "e"
|
123
124
|
```
|
124
125
|
|
126
|
+
##### Binary, octal, and hexadecimal numbers
|
127
|
+
|
128
|
+
Using the prefixes `0b`, `0o`, and `0x` (standard in Ruby) indicates binary, octal, and hexadecimal numbers respectively.
|
129
|
+
|
130
|
+
```ruby
|
131
|
+
calculator.evaluate("0b1100")
|
132
|
+
#=> 12
|
133
|
+
calculator.evaluate("0o775")
|
134
|
+
#=> 504
|
135
|
+
calculator.evaluate("0x1f0")
|
136
|
+
#=> 496
|
137
|
+
```
|
138
|
+
|
139
|
+
##### Random numbers
|
140
|
+
|
141
|
+
`keisan` has a couple methods for doing random operations, `rand` and `sample`. For example,
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
calculator.evaluate("rand(10)")
|
145
|
+
#=> 3
|
146
|
+
calculator.evaluate("sample([2, 4, 6, 8])")
|
147
|
+
#=> 8
|
148
|
+
```
|
149
|
+
|
150
|
+
If you want reproducibility, you can pass in your own `Random` object to the calculator's context.
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
calculator1 = Keisan::Calculator.new(Keisan::Context.new(random: Random.new(1234)))
|
154
|
+
calculator2 = Keisan::Calculator.new(Keisan::Context.new(random: Random.new(1234)))
|
155
|
+
5.times.map {calculator1.evaluate("rand(1000)")}
|
156
|
+
#=> [815, 723, 294, 53, 204]
|
157
|
+
5.times.map {calculator2.evaluate("rand(1000)")}
|
158
|
+
#=> [815, 723, 294, 53, 204]
|
159
|
+
```
|
160
|
+
|
125
161
|
##### Builtin variables and functions
|
126
162
|
|
127
163
|
`keisan` includes all standard methods given by the Ruby `Math` class.
|
data/lib/keisan.rb
CHANGED
data/lib/keisan/ast/builder.rb
CHANGED
@@ -3,32 +3,22 @@ module Keisan
|
|
3
3
|
class Builder
|
4
4
|
# Build from parser
|
5
5
|
def initialize(string: nil, parser: nil, components: nil)
|
6
|
-
if string
|
7
|
-
raise Keisan::Exceptions::InternalError.new("Require parser or components")
|
6
|
+
if [string, parser, components].select(&:nil?).size != 2
|
7
|
+
raise Keisan::Exceptions::InternalError.new("Require one of string, parser or components")
|
8
8
|
end
|
9
9
|
|
10
|
-
if string.
|
10
|
+
if !string.nil?
|
11
11
|
@components = Keisan::Parser.new(string: string).components
|
12
|
-
elsif parser.
|
12
|
+
elsif !parser.nil?
|
13
13
|
@components = parser.components
|
14
14
|
else
|
15
15
|
@components = Array.wrap(components)
|
16
16
|
end
|
17
17
|
|
18
|
-
@nodes = @components
|
19
|
-
component.is_a?(Keisan::Parsing::Operator)
|
20
|
-
}.map {|group_of_components|
|
21
|
-
node_from_components(group_of_components)
|
22
|
-
}
|
18
|
+
@nodes = nodes_split_by_operators(@components)
|
23
19
|
@operators = @components.select {|component| component.is_a?(Keisan::Parsing::Operator)}
|
24
20
|
|
25
|
-
|
26
|
-
|
27
|
-
while @operators.count > 0
|
28
|
-
priorities = @operators.map(&:priority)
|
29
|
-
max_priority = priorities.uniq.max
|
30
|
-
consume_operators_with_priority!(max_priority)
|
31
|
-
end
|
21
|
+
consume_operators!
|
32
22
|
|
33
23
|
unless @nodes.count == 1
|
34
24
|
raise Keisan::Exceptions::ASTError.new("Should end up with a single node")
|
@@ -45,7 +35,41 @@ module Keisan
|
|
45
35
|
|
46
36
|
private
|
47
37
|
|
38
|
+
def nodes_split_by_operators(components)
|
39
|
+
components.split {|component|
|
40
|
+
component.is_a?(Keisan::Parsing::Operator)
|
41
|
+
}.map {|group_of_components|
|
42
|
+
node_from_components(group_of_components)
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
48
46
|
def node_from_components(components)
|
47
|
+
unary_components, node, indexing_components = *unarys_node_indexings(components)
|
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
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Apply prefix unary operators
|
60
|
+
unary_components.reverse.each do |unary_component|
|
61
|
+
node = unary_component.node_class.new(node)
|
62
|
+
end
|
63
|
+
|
64
|
+
node
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns an array of the form
|
68
|
+
# [unary_operators, middle_node, postfix_indexings]
|
69
|
+
# unary_operators is an array of Keisan::Parsing::UnaryOperator objects
|
70
|
+
# 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)
|
49
73
|
index_of_unary_components = components.map.with_index {|c,i| [c,i]}.select {|c,i| c.is_a?(Keisan::Parsing::UnaryOperator)}.map(&:last)
|
50
74
|
# Must be all in the front
|
51
75
|
unless index_of_unary_components.map.with_index.all? {|i,j| i == j}
|
@@ -64,25 +88,11 @@ module Keisan
|
|
64
88
|
raise Keisan::Exceptions::ASTError.new("have too many components")
|
65
89
|
end
|
66
90
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
indexing_components.each do |indexing_component|
|
73
|
-
node = indexing_component.node_class.new(
|
74
|
-
node,
|
75
|
-
indexing_component.arguments.map {|parsing_argument|
|
76
|
-
Builder.new(components: parsing_argument.components).node
|
77
|
-
}
|
78
|
-
)
|
79
|
-
end
|
80
|
-
|
81
|
-
unary_components.reverse.each do |unary_component|
|
82
|
-
node = unary_component.node_class.new(node)
|
83
|
-
end
|
84
|
-
|
85
|
-
node
|
91
|
+
[
|
92
|
+
index_of_unary_components.map {|i| components[i]},
|
93
|
+
node_of_component(components[index_of_unary_components.size]),
|
94
|
+
index_of_indexing_components.map {|i| components[i]}
|
95
|
+
]
|
86
96
|
end
|
87
97
|
|
88
98
|
def node_of_component(component)
|
@@ -117,10 +127,20 @@ module Keisan
|
|
117
127
|
end
|
118
128
|
end
|
119
129
|
|
130
|
+
def consume_operators!
|
131
|
+
while @operators.count > 0
|
132
|
+
priorities = @operators.map(&:priority)
|
133
|
+
max_priority = priorities.uniq.max
|
134
|
+
consume_operators_with_priority!(max_priority)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
120
138
|
def consume_operators_with_priority!(priority)
|
121
139
|
# Treat back-to-back operators with same priority as one single call (e.g. 1 + 2 + 3 is add(1,2,3))
|
122
140
|
while @operators.any? {|operator| operator.priority == priority}
|
123
|
-
next_operator_group = @operators.each.with_index.to_a.split {|operator,i|
|
141
|
+
next_operator_group = @operators.each.with_index.to_a.split {|operator,i|
|
142
|
+
operator.priority != priority
|
143
|
+
}.select {|ops| !ops.empty?}.first
|
124
144
|
operator_group_indexes = next_operator_group.map(&:last)
|
125
145
|
|
126
146
|
first_index = operator_group_indexes.first
|
data/lib/keisan/calculator.rb
CHANGED
@@ -7,16 +7,18 @@ module Keisan
|
|
7
7
|
end
|
8
8
|
|
9
9
|
def evaluate(expression, definitions = {})
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
10
|
+
context.spawn_child do |local|
|
11
|
+
definitions.each do |name, value|
|
12
|
+
case value
|
13
|
+
when Proc
|
14
|
+
local.register_function!(name, value)
|
15
|
+
else
|
16
|
+
local.register_variable!(name, value)
|
17
|
+
end
|
17
18
|
end
|
19
|
+
|
20
|
+
Keisan::AST::Builder.new(string: expression).ast.value(local)
|
18
21
|
end
|
19
|
-
Keisan::AST::Builder.new(string: expression).ast.value(local_context)
|
20
22
|
end
|
21
23
|
|
22
24
|
def define_variable!(name, value)
|
data/lib/keisan/context.rb
CHANGED
@@ -13,7 +13,7 @@ module Keisan
|
|
13
13
|
|
14
14
|
def [](name)
|
15
15
|
return @hash[name] if @hash.has_key?(name)
|
16
|
-
return @parent[name] if
|
16
|
+
return @parent[name] if !@parent.nil? && @parent.has_name?(name)
|
17
17
|
return default_registry[name] if @use_defaults && default_registry.has_name?(name)
|
18
18
|
raise Keisan::Exceptions::UndefinedFunctionError.new name
|
19
19
|
end
|
data/lib/keisan/parser.rb
CHANGED
@@ -7,7 +7,7 @@ module Keisan
|
|
7
7
|
raise Keisan::Exceptions::InternalError.new("Invalid arguments")
|
8
8
|
end
|
9
9
|
|
10
|
-
if string.
|
10
|
+
if !string.nil?
|
11
11
|
@tokens = Tokenizer.new(string).tokens
|
12
12
|
else
|
13
13
|
raise Keisan::Exceptions::InternalError.new("Invalid argument: tokens = #{tokens}") if tokens.nil? || !tokens.is_a?(Array)
|
@@ -67,8 +67,10 @@ module Keisan
|
|
67
67
|
end
|
68
68
|
|
69
69
|
elsif @components[-1].is_a?(Parsing::Element)
|
70
|
+
# A word followed by a "round group" is actually a function: e.g. sin(x)
|
70
71
|
if @components[-1].is_a?(Parsing::Variable) && token.type == :group && token.group_type == :round
|
71
72
|
add_function_to_components!(token)
|
73
|
+
# Here it is a postfix Indexing (access elements by index)
|
72
74
|
elsif token.type == :group && token.group_type == :square
|
73
75
|
add_indexing_to_components!(token)
|
74
76
|
else
|
@@ -78,7 +80,7 @@ module Keisan
|
|
78
80
|
end
|
79
81
|
|
80
82
|
else
|
81
|
-
raise Keisan::Exceptions::
|
83
|
+
raise Keisan::Exceptions::ParseError.new("Token cannot be parsed, #{token.string}")
|
82
84
|
end
|
83
85
|
end
|
84
86
|
|
data/lib/keisan/tokenizer.rb
CHANGED
@@ -20,13 +20,26 @@ module Keisan
|
|
20
20
|
attr_reader :expression, :tokens
|
21
21
|
|
22
22
|
def initialize(expression)
|
23
|
-
@expression =
|
24
|
-
|
23
|
+
@expression = self.class.strip_whitespace(expression)
|
25
24
|
@scan = @expression.scan(TOKEN_REGEX)
|
25
|
+
@tokens = tokenize!
|
26
|
+
end
|
26
27
|
|
28
|
+
def self.strip_whitespace(expression)
|
29
|
+
# Do not allow whitespace between variables, numbers, and the like; they must be joined by operators
|
30
|
+
raise Keisan::Exceptions::TokenizingError.new if expression.gsub(Tokens::String.regex, "").match /\w\s+\w/
|
31
|
+
|
32
|
+
# Only strip whitespace outside of strings, e.g.
|
33
|
+
# "1 + 2 + 'hello world'" => "1+2+'hello world'"
|
34
|
+
expression.split(Keisan::Tokens::String.regex).map.with_index {|s,i| i.even? ? s.gsub(/\s+/, "") : s}.join
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def tokenize!
|
27
40
|
tokenizing_check = ""
|
28
41
|
|
29
|
-
|
42
|
+
tokens = @scan.map do |scan_result|
|
30
43
|
i = scan_result.find_index {|token| !token.nil?}
|
31
44
|
token_string = scan_result[i]
|
32
45
|
tokenizing_check << token_string
|
@@ -36,6 +49,8 @@ module Keisan
|
|
36
49
|
unless tokenizing_check == @expression
|
37
50
|
raise Keisan::Exceptions::TokenizingError.new("Expected \"#{@expression}\", tokenized \"#{tokenizing_check}\"")
|
38
51
|
end
|
52
|
+
|
53
|
+
tokens
|
39
54
|
end
|
40
55
|
end
|
41
56
|
end
|
data/lib/keisan/tokens/number.rb
CHANGED
@@ -2,10 +2,13 @@ module Keisan
|
|
2
2
|
module Tokens
|
3
3
|
class Number < Token
|
4
4
|
INTEGER_REGEX = /\d+/
|
5
|
+
BINARY_REGEX = /0b[0-1]+/
|
6
|
+
OCTAL_REGEX = /0o[0-7]+/
|
7
|
+
HEX_REGEX = /0x[0-9a-fA-F]+/
|
5
8
|
FLOATING_POINT_REGEX = /\d+\.\d+/
|
6
|
-
|
9
|
+
SCIENTIFIC_NOTATION_REGEX = /\d+(?:\.\d+)?e(?:\+|\-)?\d+/
|
7
10
|
|
8
|
-
REGEX = /(
|
11
|
+
REGEX = /(#{BINARY_REGEX}|#{OCTAL_REGEX}|#{HEX_REGEX}|\d+(?:\.\d+)?(?:e(?:\+|\-)?\d+)?)/
|
9
12
|
|
10
13
|
def self.regex
|
11
14
|
REGEX
|
@@ -13,9 +16,9 @@ module Keisan
|
|
13
16
|
|
14
17
|
def value
|
15
18
|
case string
|
16
|
-
when
|
19
|
+
when /\A#{SCIENTIFIC_NOTATION_REGEX}\z/.freeze, /\A#{FLOATING_POINT_REGEX}\z/.freeze
|
17
20
|
Float(string)
|
18
|
-
|
21
|
+
else
|
19
22
|
Integer(string)
|
20
23
|
end
|
21
24
|
end
|
data/lib/keisan/tokens/word.rb
CHANGED
@@ -13,7 +13,7 @@ module Keisan
|
|
13
13
|
|
14
14
|
def [](name)
|
15
15
|
return @hash[name] if @hash.has_key?(name)
|
16
|
-
return @parent[name] if
|
16
|
+
return @parent[name] if !@parent.nil? && @parent.has_name?(name)
|
17
17
|
return default_registry[name] if @use_defaults && default_registry.has_name?(name)
|
18
18
|
raise Keisan::Exceptions::UndefinedVariableError.new name
|
19
19
|
end
|
data/lib/keisan/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keisan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Locke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-25 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|