keisan 0.8.1 → 0.8.6
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 +22 -0
- data/lib/keisan/ast/block.rb +4 -0
- data/lib/keisan/ast/cell.rb +4 -0
- data/lib/keisan/ast/function_assignment.rb +16 -6
- data/lib/keisan/ast/hash.rb +4 -0
- data/lib/keisan/ast/node.rb +9 -0
- data/lib/keisan/ast/parent.rb +4 -0
- data/lib/keisan/calculator.rb +17 -3
- data/lib/keisan/context.rb +21 -4
- data/lib/keisan/evaluator.rb +16 -4
- data/lib/keisan/functions/default_registry.rb +3 -0
- 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 +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 496551eafa9e9774866f8322a750ec0e0b88c463c9cb0f9174cd3461003d95a5
|
4
|
+
data.tar.gz: 9d509cb07d53c12758bf3cbc1ddb45c4e75809432267cbf3215ab0ce9451404f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a0047d2ffeea473809ce46b8e7a0c5458012a5ee27182384d28f9ce5706eeaf0385a7c6aecf4619b637760ec235d8c55f8815c537665f03af58ed388180d5fe4
|
7
|
+
data.tar.gz: 29d2bf8124706bc44129abec0839bba5809678e71c50ac6b2b6d56e2584f4d7cf3f094005db1e8f6e2606334a637f6df90ab95f1a80ac8e6f9e844f67aaf032d
|
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
|
@@ -256,6 +272,7 @@ calculator.evaluate("range(1, 6).map(x, [x, x**2]).to_h")
|
|
256
272
|
Keisan supports date and time objects like in Ruby.
|
257
273
|
You create a date object using either the method `date` (either a string to be parsed, or year, month, day numerical arguments) or `today`.
|
258
274
|
They support methods `year`, `month`, `day`, `weekday`, `strftime`, and `to_time` to convert to a time object.
|
275
|
+
`epoch_days` computes the number of days since Unix epoch (Jan 1, 1970).
|
259
276
|
|
260
277
|
```ruby
|
261
278
|
calculator = Keisan::Calculator.new
|
@@ -266,10 +283,13 @@ calculator.evaluate("today() > date(2018, 11, 1)")
|
|
266
283
|
#=> true
|
267
284
|
calculator.evaluate("date('1999-12-31').to_time + 10")
|
268
285
|
#=> Time.new(1999, 12, 31, 0, 0, 10)
|
286
|
+
calculator.evaluate("date(1970, 1, 15).epoch_days")
|
287
|
+
#=> 14
|
269
288
|
```
|
270
289
|
|
271
290
|
Time objects are created using `time` (either a string to be parsed, or year, month, day, hour, minute, second arguments) or `now`.
|
272
291
|
They support methods `year`, `month`, `day`, `hour`, `minute`, `second`, `weekday`, `strftime`, and `to_date` to convert to a date object.
|
292
|
+
`epoch_seconds` computes the number of seconds since Unix epoch (00:00:00 on Jan 1, 1970).
|
273
293
|
|
274
294
|
```ruby
|
275
295
|
calculator = Keisan::Calculator.new
|
@@ -279,6 +299,8 @@ calculator.evaluate("time('2000-4-15 12:34:56').minute")
|
|
279
299
|
#=> 34
|
280
300
|
calculator.evaluate("time('5000-10-10 20:30:40').strftime('%b %d, %Y')")
|
281
301
|
#=> "Oct 10, 5000"
|
302
|
+
calculator.evaluate("time(1970, 1, 1, 2, 3, 4).epoch_seconds")
|
303
|
+
#=> 7384
|
282
304
|
```
|
283
305
|
|
284
306
|
##### Functional programming methods
|
data/lib/keisan/ast/block.rb
CHANGED
data/lib/keisan/ast/cell.rb
CHANGED
@@ -29,8 +29,7 @@ module Keisan
|
|
29
29
|
|
30
30
|
def evaluate
|
31
31
|
# Blocks might have local variable/function definitions, so skip check
|
32
|
-
verify_rhs_of_function_assignment_is_valid!
|
33
|
-
|
32
|
+
verify_rhs_of_function_assignment_is_valid!
|
34
33
|
context.register_function!(lhs.name, expression_function, local: local)
|
35
34
|
rhs
|
36
35
|
end
|
@@ -38,15 +37,26 @@ module Keisan
|
|
38
37
|
private
|
39
38
|
|
40
39
|
def verify_rhs_of_function_assignment_is_valid!
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
40
|
+
verify_unbound_functions!
|
41
|
+
verify_unbound_variables!
|
42
|
+
end
|
43
|
+
|
44
|
+
def verify_unbound_functions!
|
45
45
|
# Cannot have undefined functions unless allowed by context
|
46
46
|
unless context.allow_recursive || rhs.unbound_functions(context).empty?
|
47
47
|
raise Exceptions::InvalidExpression.new("Unbound function definitions are not allowed by current context")
|
48
48
|
end
|
49
49
|
end
|
50
|
+
|
51
|
+
def verify_unbound_variables!
|
52
|
+
# We allow unbound variables inside block statements, as they could be temporary
|
53
|
+
# variables assigned locally
|
54
|
+
return if rhs.is_a?(Block)
|
55
|
+
# Only variables that can appear are those that are arguments to the function
|
56
|
+
unless rhs.unbound_variables(context) <= Set.new(argument_names)
|
57
|
+
raise Exceptions::InvalidExpression.new("Unbound variables found in function definition")
|
58
|
+
end
|
59
|
+
end
|
50
60
|
end
|
51
61
|
end
|
52
62
|
end
|
data/lib/keisan/ast/hash.rb
CHANGED
data/lib/keisan/ast/node.rb
CHANGED
data/lib/keisan/ast/parent.rb
CHANGED
data/lib/keisan/calculator.rb
CHANGED
@@ -2,8 +2,14 @@ module Keisan
|
|
2
2
|
class Calculator
|
3
3
|
attr_reader :context
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
# Note, allow_recursive would be more appropriately named:
|
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)
|
8
|
+
@context = context || Context.new(
|
9
|
+
allow_recursive: allow_recursive,
|
10
|
+
allow_blocks: allow_blocks,
|
11
|
+
allow_multiline: allow_multiline
|
12
|
+
)
|
7
13
|
end
|
8
14
|
|
9
15
|
def allow_recursive
|
@@ -14,6 +20,14 @@ module Keisan
|
|
14
20
|
context.allow_recursive!
|
15
21
|
end
|
16
22
|
|
23
|
+
def allow_blocks
|
24
|
+
context.allow_blocks
|
25
|
+
end
|
26
|
+
|
27
|
+
def allow_multiline
|
28
|
+
context.allow_multiline
|
29
|
+
end
|
30
|
+
|
17
31
|
def evaluate(expression, definitions = {})
|
18
32
|
Evaluator.new(self).evaluate(expression, definitions)
|
19
33
|
end
|
@@ -23,7 +37,7 @@ module Keisan
|
|
23
37
|
end
|
24
38
|
|
25
39
|
def ast(expression)
|
26
|
-
Evaluator.new(self).
|
40
|
+
Evaluator.new(self).parse_ast(expression)
|
27
41
|
end
|
28
42
|
|
29
43
|
def define_variable!(name, value)
|
data/lib/keisan/context.rb
CHANGED
@@ -1,13 +1,24 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Context
|
3
|
-
attr_reader :function_registry,
|
4
|
-
|
5
|
-
|
3
|
+
attr_reader :function_registry,
|
4
|
+
:variable_registry,
|
5
|
+
:allow_recursive,
|
6
|
+
:allow_multiline,
|
7
|
+
:allow_blocks
|
8
|
+
|
9
|
+
def initialize(parent: nil,
|
10
|
+
random: nil,
|
11
|
+
allow_recursive: false,
|
12
|
+
allow_multiline: true,
|
13
|
+
allow_blocks: true,
|
14
|
+
shadowed: [])
|
6
15
|
@parent = parent
|
7
16
|
@function_registry = Functions::Registry.new(parent: @parent&.function_registry)
|
8
17
|
@variable_registry = Variables::Registry.new(parent: @parent&.variable_registry, shadowed: shadowed)
|
9
18
|
@random = random
|
10
19
|
@allow_recursive = allow_recursive
|
20
|
+
@allow_multiline = allow_multiline
|
21
|
+
@allow_blocks = allow_blocks
|
11
22
|
end
|
12
23
|
|
13
24
|
def allow_recursive!
|
@@ -110,7 +121,13 @@ module Keisan
|
|
110
121
|
end
|
111
122
|
|
112
123
|
def pure_child(shadowed: [])
|
113
|
-
self.class.new(
|
124
|
+
self.class.new(
|
125
|
+
parent: self,
|
126
|
+
shadowed: shadowed,
|
127
|
+
allow_recursive: allow_recursive,
|
128
|
+
allow_multiline: allow_multiline,
|
129
|
+
allow_blocks: allow_blocks
|
130
|
+
)
|
114
131
|
end
|
115
132
|
end
|
116
133
|
end
|
data/lib/keisan/evaluator.rb
CHANGED
@@ -8,7 +8,7 @@ module Keisan
|
|
8
8
|
|
9
9
|
def evaluate(expression, definitions = {})
|
10
10
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
11
|
-
ast =
|
11
|
+
ast = parse_ast(expression)
|
12
12
|
last_line = last_line(ast)
|
13
13
|
|
14
14
|
evaluation = ast.evaluated(context)
|
@@ -24,16 +24,28 @@ module Keisan
|
|
24
24
|
|
25
25
|
def simplify(expression, definitions = {})
|
26
26
|
context = calculator.context.spawn_child(definitions: definitions, transient: true)
|
27
|
-
ast =
|
27
|
+
ast = parse_ast(expression)
|
28
28
|
ast.simplify(context)
|
29
29
|
end
|
30
30
|
|
31
|
-
def
|
32
|
-
AST.parse(expression)
|
31
|
+
def parse_ast(expression)
|
32
|
+
AST.parse(expression).tap do |ast|
|
33
|
+
disallowed = disallowed_nodes
|
34
|
+
if !disallowed.empty? && ast.contains_a?(disallowed)
|
35
|
+
raise Keisan::Exceptions::InvalidExpression.new("Context does not permit expressions with #{disallowed}")
|
36
|
+
end
|
37
|
+
end
|
33
38
|
end
|
34
39
|
|
35
40
|
private
|
36
41
|
|
42
|
+
def disallowed_nodes
|
43
|
+
disallowed = []
|
44
|
+
disallowed << Keisan::AST::Block unless calculator.allow_blocks
|
45
|
+
disallowed << Keisan::AST::MultiLine unless calculator.allow_multiline
|
46
|
+
disallowed
|
47
|
+
end
|
48
|
+
|
37
49
|
def last_line(ast)
|
38
50
|
ast.is_a?(AST::MultiLine) ? ast.children.last : ast
|
39
51
|
end
|
@@ -147,6 +147,9 @@ module Keisan
|
|
147
147
|
|
148
148
|
registry.register!(:to_time, Proc.new {|d| d.to_time }, force: true)
|
149
149
|
registry.register!(:to_date, Proc.new {|t| t.to_date }, force: true)
|
150
|
+
|
151
|
+
registry.register!(:epoch_seconds, Proc.new {|d| d.to_time - Time.new(1970, 1, 1, 0, 0, 0) }, force: true)
|
152
|
+
registry.register!(:epoch_days, Proc.new {|t| t.to_date - Date.new(1970, 1, 1) }, force: true)
|
150
153
|
end
|
151
154
|
|
152
155
|
def self.register_date_time!(registry)
|
@@ -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.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Locke
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: cmath
|