keisan 0.8.3 → 0.8.8
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/.github/workflows/ruby.yml +28 -0
- data/README.md +16 -0
- data/lib/keisan/ast/block.rb +2 -2
- data/lib/keisan/ast/cell.rb +2 -2
- data/lib/keisan/ast/hash.rb +10 -2
- data/lib/keisan/ast/node.rb +16 -2
- data/lib/keisan/ast/parent.rb +8 -2
- data/lib/keisan/calculator.rb +7 -2
- data/lib/keisan/context.rb +11 -2
- data/lib/keisan/functions/enumerable_function.rb +7 -4
- data/lib/keisan/string_and_group_parser.rb +49 -7
- data/lib/keisan/tokenizer.rb +5 -16
- data/lib/keisan/version.rb +1 -1
- metadata +4 -4
- data/.travis.yml +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d539ead6f33b2151b228dfe6c30a0e3f7d40bb7b1113c90b851d020e04106df5
|
4
|
+
data.tar.gz: c36af3538e6f448c285b9857aa5ea14e3bc90853e5d3f4a74fd726d8b0c35068
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 67557029c43754d862406fc0491594b94dcc4dac5a925e518232accfd75c42582f8e5d11b5f99eec7f44dcb1ea29ea31f087668bf70a68c56fa25fc4ca8f2929
|
7
|
+
data.tar.gz: 2451bd2dbe844af1bc3a4f2ec9d0819aeb4df52f96bd45dbbc5ada1993b2c6003245c8f6191ce3f5dc59de18b106f313e07dedd3aef9ba670525d4bacabfb43a
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: Ruby
|
2
|
+
|
3
|
+
on:
|
4
|
+
push:
|
5
|
+
branches: [ master ]
|
6
|
+
pull_request:
|
7
|
+
branches: [ master ]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
test:
|
11
|
+
|
12
|
+
runs-on: ubuntu-latest
|
13
|
+
|
14
|
+
strategy:
|
15
|
+
matrix:
|
16
|
+
ruby-version: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0]
|
17
|
+
|
18
|
+
steps:
|
19
|
+
- uses: actions/checkout@v2
|
20
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
21
|
+
uses: ruby/setup-ruby@v1
|
22
|
+
with:
|
23
|
+
ruby-version: ${{ matrix.ruby-version }}
|
24
|
+
bundler-cache: true
|
25
|
+
- name: Install dependencies
|
26
|
+
run: bundle install
|
27
|
+
- name: Run tests
|
28
|
+
run: bundle exec rspec
|
data/README.md
CHANGED
@@ -178,6 +178,22 @@ calculator.evaluate("x = 11; {let x = 12}; x")
|
|
178
178
|
#=> 11
|
179
179
|
```
|
180
180
|
|
181
|
+
##### Comments
|
182
|
+
|
183
|
+
When working with multi-line blocks of code, sometimes comments are useful to include.
|
184
|
+
Comments are parts of a string from the `#` character to the end of a line (indicated by a newline character `"\n"`).
|
185
|
+
|
186
|
+
```
|
187
|
+
calculator = Keisan::Calculator.new
|
188
|
+
calculator.evaluate("""
|
189
|
+
# This is a comment
|
190
|
+
x = 'foo'
|
191
|
+
x += '#bar' # Notice that `#` inside strings is not part of the comment
|
192
|
+
x # Should print 'foo#bar'
|
193
|
+
""")
|
194
|
+
#=> "foo#bar"
|
195
|
+
```
|
196
|
+
|
181
197
|
##### Lists
|
182
198
|
|
183
199
|
Just like in Ruby, lists can be defined using square brackets, and indexed using square brackets
|
data/lib/keisan/ast/block.rb
CHANGED
data/lib/keisan/ast/cell.rb
CHANGED
data/lib/keisan/ast/hash.rb
CHANGED
@@ -21,8 +21,16 @@ module Keisan
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def
|
25
|
-
|
24
|
+
def traverse(&block)
|
25
|
+
value = super(&block)
|
26
|
+
return value if value
|
27
|
+
@hash.each do |k, v|
|
28
|
+
value = k.to_node.traverse(&block)
|
29
|
+
return value if value
|
30
|
+
value = v.traverse(&block)
|
31
|
+
return value if value
|
32
|
+
end
|
33
|
+
false
|
26
34
|
end
|
27
35
|
|
28
36
|
def evaluate(context = nil)
|
data/lib/keisan/ast/node.rb
CHANGED
@@ -37,12 +37,26 @@ module Keisan
|
|
37
37
|
value(context)
|
38
38
|
end
|
39
39
|
|
40
|
+
# Takes a block, and does a DFS down the AST, evaluating the received block
|
41
|
+
# at each node, passing in the node as the single argument. If the block
|
42
|
+
# returns a truthy value at any point, the DFS ends and the return value is
|
43
|
+
# percolated up the tree.
|
44
|
+
def traverse(&block)
|
45
|
+
block.call(self)
|
46
|
+
end
|
47
|
+
|
40
48
|
def contains_a?(klass)
|
41
49
|
case klass
|
42
50
|
when Array
|
43
|
-
klass.any?
|
51
|
+
klass.any? do |k|
|
52
|
+
traverse do |node|
|
53
|
+
node.is_a?(k)
|
54
|
+
end
|
55
|
+
end
|
44
56
|
else
|
45
|
-
|
57
|
+
traverse do |node|
|
58
|
+
node.is_a?(klass)
|
59
|
+
end
|
46
60
|
end
|
47
61
|
end
|
48
62
|
|
data/lib/keisan/ast/parent.rb
CHANGED
@@ -25,8 +25,14 @@ module Keisan
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
-
def
|
29
|
-
|
28
|
+
def traverse(&block)
|
29
|
+
value = super(&block)
|
30
|
+
return value if value
|
31
|
+
children.each do |child|
|
32
|
+
value = child.traverse(&block)
|
33
|
+
return value if value
|
34
|
+
end
|
35
|
+
false
|
30
36
|
end
|
31
37
|
|
32
38
|
def freeze
|
data/lib/keisan/calculator.rb
CHANGED
@@ -4,11 +4,12 @@ module Keisan
|
|
4
4
|
|
5
5
|
# Note, allow_recursive would be more appropriately named:
|
6
6
|
# allow_unbound_functions_in_function_definitions, but it is too late for that.
|
7
|
-
def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true)
|
7
|
+
def initialize(context: nil, allow_recursive: false, allow_blocks: true, allow_multiline: true, allow_random: true)
|
8
8
|
@context = context || Context.new(
|
9
9
|
allow_recursive: allow_recursive,
|
10
10
|
allow_blocks: allow_blocks,
|
11
|
-
allow_multiline: allow_multiline
|
11
|
+
allow_multiline: allow_multiline,
|
12
|
+
allow_random: allow_random
|
12
13
|
)
|
13
14
|
end
|
14
15
|
|
@@ -28,6 +29,10 @@ module Keisan
|
|
28
29
|
context.allow_multiline
|
29
30
|
end
|
30
31
|
|
32
|
+
def allow_random
|
33
|
+
context.allow_random
|
34
|
+
end
|
35
|
+
|
31
36
|
def evaluate(expression, definitions = {})
|
32
37
|
Evaluator.new(self).evaluate(expression, definitions)
|
33
38
|
end
|
data/lib/keisan/context.rb
CHANGED
@@ -4,13 +4,15 @@ module Keisan
|
|
4
4
|
:variable_registry,
|
5
5
|
:allow_recursive,
|
6
6
|
:allow_multiline,
|
7
|
-
:allow_blocks
|
7
|
+
:allow_blocks,
|
8
|
+
:allow_random
|
8
9
|
|
9
10
|
def initialize(parent: nil,
|
10
11
|
random: nil,
|
11
12
|
allow_recursive: false,
|
12
13
|
allow_multiline: true,
|
13
14
|
allow_blocks: true,
|
15
|
+
allow_random: true,
|
14
16
|
shadowed: [])
|
15
17
|
@parent = parent
|
16
18
|
@function_registry = Functions::Registry.new(parent: @parent&.function_registry)
|
@@ -19,6 +21,7 @@ module Keisan
|
|
19
21
|
@allow_recursive = allow_recursive
|
20
22
|
@allow_multiline = allow_multiline
|
21
23
|
@allow_blocks = allow_blocks
|
24
|
+
@allow_random = allow_random
|
22
25
|
end
|
23
26
|
|
24
27
|
def allow_recursive!
|
@@ -111,7 +114,13 @@ module Keisan
|
|
111
114
|
end
|
112
115
|
|
113
116
|
def random
|
114
|
-
|
117
|
+
raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with randomness") unless allow_random
|
118
|
+
@random ||= @parent&.random || Random.new
|
119
|
+
end
|
120
|
+
|
121
|
+
def set_random(random)
|
122
|
+
raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with randomness") unless allow_random
|
123
|
+
@random = random
|
115
124
|
end
|
116
125
|
|
117
126
|
protected
|
@@ -21,14 +21,17 @@ module Keisan
|
|
21
21
|
context ||= Context.new
|
22
22
|
|
23
23
|
operand, arguments, expression = operand_arguments_expression_for(ast_function, context)
|
24
|
+
|
25
|
+
# Extract underlying operand for cells
|
26
|
+
real_operand = operand.is_a?(AST::Cell) ? operand.node : operand
|
24
27
|
|
25
|
-
case
|
28
|
+
case real_operand
|
26
29
|
when AST::List
|
27
|
-
evaluate_list(
|
30
|
+
evaluate_list(real_operand, arguments, expression, context).evaluate(context)
|
28
31
|
when AST::Hash
|
29
|
-
evaluate_hash(
|
32
|
+
evaluate_hash(real_operand, arguments, expression, context).evaluate(context)
|
30
33
|
else
|
31
|
-
raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{
|
34
|
+
raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{real_operand}")
|
32
35
|
end
|
33
36
|
end
|
34
37
|
|
@@ -9,24 +9,26 @@ module Keisan
|
|
9
9
|
end
|
10
10
|
|
11
11
|
class StringPortion < Portion
|
12
|
-
attr_reader :string
|
12
|
+
attr_reader :string, :escaped_string
|
13
13
|
|
14
14
|
def initialize(expression, start_index)
|
15
15
|
super(start_index)
|
16
16
|
|
17
17
|
@string = expression[start_index]
|
18
|
+
@escaped_string = expression[start_index]
|
18
19
|
@end_index = start_index + 1
|
19
20
|
|
20
21
|
while @end_index < expression.size
|
21
22
|
if expression[@end_index] == quote_type
|
22
23
|
@string << quote_type
|
24
|
+
@escaped_string << quote_type
|
23
25
|
@end_index += 1
|
24
26
|
# Successfully parsed the string
|
25
27
|
return
|
26
28
|
end
|
27
29
|
|
28
|
-
n, c =
|
29
|
-
@
|
30
|
+
n, c = get_potentially_escaped_next_character(expression, @end_index)
|
31
|
+
@escaped_string << c
|
30
32
|
@end_index += n
|
31
33
|
end
|
32
34
|
|
@@ -44,9 +46,13 @@ module Keisan
|
|
44
46
|
private
|
45
47
|
|
46
48
|
# Returns number of processed input characters, and the output character
|
47
|
-
|
48
|
-
|
49
|
-
|
49
|
+
# If a sequence like '\"' is encountered, the first backslash escapes the
|
50
|
+
# second double-quote, and the two characters will act as a one double-quote
|
51
|
+
# character.
|
52
|
+
def get_potentially_escaped_next_character(expression, index)
|
53
|
+
@string << expression[index]
|
54
|
+
if expression[index] == "\\" && index + 1 < expression.size
|
55
|
+
@string << expression[index + 1]
|
50
56
|
return [2, escaped_character(expression[index + 1])]
|
51
57
|
else
|
52
58
|
return [1, expression[index]]
|
@@ -129,7 +135,7 @@ module Keisan
|
|
129
135
|
|
130
136
|
while index < expression.size
|
131
137
|
case expression[index]
|
132
|
-
when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX
|
138
|
+
when STRING_CHARACTER_REGEX, OPEN_GROUP_REGEX, CLOSED_GROUP_REGEX, COMMENT_CHARACTER_REGEX
|
133
139
|
break
|
134
140
|
else
|
135
141
|
index += 1
|
@@ -149,9 +155,40 @@ module Keisan
|
|
149
155
|
end
|
150
156
|
end
|
151
157
|
|
158
|
+
class CommentPortion < Portion
|
159
|
+
attr_reader :string
|
160
|
+
|
161
|
+
def initialize(expression, start_index)
|
162
|
+
super(start_index)
|
163
|
+
|
164
|
+
if expression[start_index] != '#'
|
165
|
+
raise Keisan::Exceptions::TokenizingError.new("Comment should start with '#'")
|
166
|
+
else
|
167
|
+
index = start_index + 1
|
168
|
+
end
|
169
|
+
|
170
|
+
while index < expression.size
|
171
|
+
break if expression[index] == "\n"
|
172
|
+
index += 1
|
173
|
+
end
|
174
|
+
|
175
|
+
@end_index = index
|
176
|
+
@string = expression[start_index...end_index]
|
177
|
+
end
|
178
|
+
|
179
|
+
def size
|
180
|
+
string.size
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_s
|
184
|
+
string
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
152
188
|
# An ordered array of "portions", which
|
153
189
|
attr_reader :portions, :size
|
154
190
|
|
191
|
+
COMMENT_CHARACTER_REGEX = /[#]/
|
155
192
|
STRING_CHARACTER_REGEX = /["']/
|
156
193
|
OPEN_GROUP_REGEX = /[\(\{\[]/
|
157
194
|
CLOSED_GROUP_REGEX = /[\)\}\]]/
|
@@ -176,6 +213,11 @@ module Keisan
|
|
176
213
|
when CLOSED_GROUP_REGEX
|
177
214
|
raise Keisan::Exceptions::TokenizingError.new("Tokenizing error, unexpected closing brace #{expression[start_index]}")
|
178
215
|
|
216
|
+
when COMMENT_CHARACTER_REGEX
|
217
|
+
portion = CommentPortion.new(expression, index)
|
218
|
+
index += portion.size
|
219
|
+
@portions << portion
|
220
|
+
|
179
221
|
else
|
180
222
|
portion = OtherPortion.new(expression, index)
|
181
223
|
index += portion.size
|
data/lib/keisan/tokenizer.rb
CHANGED
@@ -24,14 +24,16 @@ module Keisan
|
|
24
24
|
attr_reader :expression, :tokens
|
25
25
|
|
26
26
|
def initialize(expression)
|
27
|
-
@expression =
|
27
|
+
@expression = expression
|
28
28
|
|
29
|
-
portions = StringAndGroupParser.new(
|
29
|
+
portions = StringAndGroupParser.new(expression).portions.reject do |portion|
|
30
|
+
portion.is_a? StringAndGroupParser::CommentPortion
|
31
|
+
end
|
30
32
|
|
31
33
|
@tokens = portions.inject([]) do |tokens, portion|
|
32
34
|
case portion
|
33
35
|
when StringAndGroupParser::StringPortion
|
34
|
-
tokens << Tokens::String.new(portion.
|
36
|
+
tokens << Tokens::String.new(portion.escaped_string)
|
35
37
|
when StringAndGroupParser::GroupPortion
|
36
38
|
tokens << Tokens::Group.new(portion.to_s)
|
37
39
|
when StringAndGroupParser::OtherPortion
|
@@ -43,21 +45,8 @@ module Keisan
|
|
43
45
|
end
|
44
46
|
end
|
45
47
|
|
46
|
-
def self.normalize_expression(expression)
|
47
|
-
expression = normalize_line_delimiters(expression)
|
48
|
-
expression = remove_comments(expression)
|
49
|
-
end
|
50
|
-
|
51
48
|
private
|
52
49
|
|
53
|
-
def self.normalize_line_delimiters(expression)
|
54
|
-
expression.gsub(/\n/, ";")
|
55
|
-
end
|
56
|
-
|
57
|
-
def self.remove_comments(expression)
|
58
|
-
expression.gsub(/#[^;]*/, "")
|
59
|
-
end
|
60
|
-
|
61
50
|
def tokenize!(scan)
|
62
51
|
scan.map do |scan_result|
|
63
52
|
i = scan_result.find_index {|token| !token.nil?}
|
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.8.
|
4
|
+
version: 0.8.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Locke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmath
|
@@ -115,9 +115,9 @@ executables: []
|
|
115
115
|
extensions: []
|
116
116
|
extra_rdoc_files: []
|
117
117
|
files:
|
118
|
+
- ".github/workflows/ruby.yml"
|
118
119
|
- ".gitignore"
|
119
120
|
- ".rspec"
|
120
|
-
- ".travis.yml"
|
121
121
|
- Gemfile
|
122
122
|
- MIT-LICENSE
|
123
123
|
- README.md
|
@@ -328,7 +328,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
328
328
|
- !ruby/object:Gem::Version
|
329
329
|
version: '0'
|
330
330
|
requirements: []
|
331
|
-
rubygems_version: 3.
|
331
|
+
rubygems_version: 3.2.15
|
332
332
|
signing_key:
|
333
333
|
specification_version: 4
|
334
334
|
summary: An equation parser and evaluator
|