keisan 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/keisan.svg)](https://badge.fury.io/rb/keisan)
|
3
4
|
[![Build Status](https://travis-ci.org/project-eutopia/keisan.png?branch=master)](https://travis-ci.org/project-eutopia/keisan)
|
4
|
-
[![License: MIT](https://img.shields.io/badge/License-MIT-
|
5
|
+
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT)
|
5
6
|
[![Hakiri](https://hakiri.io/github/project-eutopia/keisan/master.svg)](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
|