keisan 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +4 -3
- data/README.md +39 -2
- data/keisan.gemspec +1 -3
- data/lib/keisan.rb +3 -3
- data/lib/keisan/ast.rb +25 -0
- data/lib/keisan/ast/boolean.rb +1 -1
- data/lib/keisan/ast/builder.rb +2 -2
- data/lib/keisan/ast/cell.rb +6 -0
- data/lib/keisan/ast/date.rb +23 -0
- data/lib/keisan/ast/date_time_methods.rb +75 -0
- data/lib/keisan/ast/function.rb +9 -0
- data/lib/keisan/ast/logical_and.rb +20 -3
- data/lib/keisan/ast/logical_equal.rb +6 -5
- data/lib/keisan/ast/logical_greater_than.rb +6 -4
- data/lib/keisan/ast/logical_greater_than_or_equal_to.rb +6 -4
- data/lib/keisan/ast/logical_less_than.rb +6 -4
- data/lib/keisan/ast/logical_less_than_or_equal_to.rb +6 -4
- data/lib/keisan/ast/logical_not_equal.rb +6 -5
- data/lib/keisan/ast/logical_operator.rb +24 -0
- data/lib/keisan/ast/logical_or.rb +18 -1
- data/lib/keisan/ast/number.rb +4 -0
- data/lib/keisan/ast/operator.rb +1 -1
- data/lib/keisan/ast/parent.rb +1 -1
- data/lib/keisan/ast/plus.rb +10 -0
- data/lib/keisan/ast/time.rb +23 -0
- data/lib/keisan/ast/unary_operator.rb +1 -1
- data/lib/keisan/ast/variable.rb +2 -5
- data/lib/keisan/context.rb +6 -6
- data/lib/keisan/exceptions.rb +3 -0
- data/lib/keisan/function.rb +6 -0
- data/lib/keisan/functions/break.rb +11 -0
- data/lib/keisan/functions/continue.rb +11 -0
- data/lib/keisan/functions/default_registry.rb +36 -0
- data/lib/keisan/functions/enumerable_function.rb +10 -2
- data/lib/keisan/functions/filter.rb +6 -0
- data/lib/keisan/functions/loop_control_flow_function.rb +22 -0
- data/lib/keisan/functions/map.rb +6 -0
- data/lib/keisan/functions/reduce.rb +5 -0
- data/lib/keisan/functions/while.rb +7 -1
- data/lib/keisan/parser.rb +5 -5
- data/lib/keisan/parsing/function.rb +1 -1
- data/lib/keisan/parsing/hash.rb +2 -2
- data/lib/keisan/token.rb +1 -1
- data/lib/keisan/util.rb +19 -0
- data/lib/keisan/variables/default_registry.rb +2 -1
- data/lib/keisan/version.rb +1 -1
- metadata +11 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f2d556d5ef3e67015596fee5856e2fe0b27295b227f7d4edf8cb41c037aed98b
|
4
|
+
data.tar.gz: c8c20ac6ba6a9ad27654931238a2dbafcbbbc509b58fd2a74161b7a38b3edaab
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9ccc5172adc0b5ebafabe3cd5846fb455ada6e1351c94deab4da5bfd38617c0e0692258e9384d760d25b88a899b46de1899b1688850bfac8ddcdd432091ddf50
|
7
|
+
data.tar.gz: cb2ca195319a744f3ee9b56ef7df14d8dfcf36dbcafcbe1e675a858f232457d39cb3fe96d7f056a26746630d4a7f1c47a8fe6fc75a880683cd2120428a448ff6
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -9,7 +9,7 @@
|
|
9
9
|
|
10
10
|
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.
|
11
11
|
This allows for safe evaluation of string representations of mathematical/logical expressions.
|
12
|
-
It has support for variables, functions, conditionals, and loops, making it a Turing complete programming language.
|
12
|
+
It has support for variables, functions, conditionals, and loops, making it a [Turing complete](https://github.com/project-eutopia/keisan/blob/master/spec/keisan/turing_machine_spec.rb) programming language.
|
13
13
|
|
14
14
|
## Installation
|
15
15
|
|
@@ -119,6 +119,9 @@ calculator.evaluate("f(0-a)", a: 2)
|
|
119
119
|
#=> -20
|
120
120
|
calculator.evaluate("n") # n only exists in the definition of f(x)
|
121
121
|
#=> Keisan::Exceptions::UndefinedVariableError: n
|
122
|
+
calculator.evaluate("includes(a, element) = a.reduce(false, found, x, found || (x == element))")
|
123
|
+
calculator.evaluate("[3, 9].map(x, [1, 3, 5].includes(x))").value
|
124
|
+
#=> [true, false]
|
122
125
|
```
|
123
126
|
|
124
127
|
This form even supports recursion, but you must explicitly allow it.
|
@@ -248,6 +251,36 @@ calculator.evaluate("range(1, 6).map(x, [x, x**2]).to_h")
|
|
248
251
|
#=> {1 => 1, 2 => 4, 3 => 9, 4 => 16, 5 => 25}
|
249
252
|
```
|
250
253
|
|
254
|
+
##### Date and time objects
|
255
|
+
|
256
|
+
Keisan supports date and time objects like in Ruby.
|
257
|
+
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
|
+
They support methods `year`, `month`, `day`, `weekday`, `strftime`, and `to_time` to convert to a time object.
|
259
|
+
|
260
|
+
```ruby
|
261
|
+
calculator = Keisan::Calculator.new
|
262
|
+
calculator.evaluate("x = 11")
|
263
|
+
calculator.evaluate("(5 + date(2018, x, 2*x)).day")
|
264
|
+
#=> 27
|
265
|
+
calculator.evaluate("today() > date(2018, 11, 1)")
|
266
|
+
#=> true
|
267
|
+
calculator.evaluate("date('1999-12-31').to_time + 10")
|
268
|
+
#=> Time.new(1999, 12, 31, 0, 0, 10)
|
269
|
+
```
|
270
|
+
|
271
|
+
Time objects are created using `time` (either a string to be parsed, or year, month, day, hour, minute, second arguments) or `now`.
|
272
|
+
They support methods `year`, `month`, `day`, `hour`, `minute`, `second`, `weekday`, `strftime`, and `to_date` to convert to a date object.
|
273
|
+
|
274
|
+
```ruby
|
275
|
+
calculator = Keisan::Calculator.new
|
276
|
+
calculator.evaluate("time(2018, 11, 22, 12, 0, 0).to_date <= date(2018, 11, 22)")
|
277
|
+
#=> true
|
278
|
+
calculator.evaluate("time('2000-4-15 12:34:56').minute")
|
279
|
+
#=> 34
|
280
|
+
calculator.evaluate("time('5000-10-10 20:30:40').strftime('%b %d, %Y')")
|
281
|
+
#=> "Oct 10, 5000"
|
282
|
+
```
|
283
|
+
|
251
284
|
##### Functional programming methods
|
252
285
|
|
253
286
|
Keisan also supports the basic functional programming operators `map` (or `collect`), `filter` (or `select`), and `reduce` (or `inject`).
|
@@ -293,12 +326,16 @@ calculator.evaluate("2 + if(1 > 0, 10, 29)")
|
|
293
326
|
```
|
294
327
|
|
295
328
|
For looping, you can use the basic `while` loop, which has an expression that evaluates to a boolean as the first argument, and any expression in the second argument.
|
329
|
+
One can use the keywords `break` and `continue` to control loop flow as well.
|
296
330
|
|
297
331
|
```ruby
|
298
332
|
calculator = Keisan::Calculator.new
|
299
333
|
calculator.evaluate("my_sum(a) = {let i = 0; let total = 0; while(i < a.size, {total += a[i]; i += 1}); total}")
|
300
334
|
calculator.evaluate("my_sum([1,3,5,7,9])")
|
301
335
|
#=> 25
|
336
|
+
calculator.evaluate("has_element(a, x) = {let i=0; let found=false; while(i<a.size, if(a[i] == x, found = true; break); i+=1); found}")
|
337
|
+
calculator.evaluate("[2, 3, 7, 11].has_element(11)")
|
338
|
+
#=> true
|
302
339
|
```
|
303
340
|
|
304
341
|
##### Bitwise operations
|
@@ -368,7 +405,7 @@ calculator.evaluate("log10(1000)")
|
|
368
405
|
#=> 3.0
|
369
406
|
```
|
370
407
|
|
371
|
-
Furthermore, the constants `PI`, `E`, and `
|
408
|
+
Furthermore, the constants `PI`, `E`, `I`, and `INF` are included.
|
372
409
|
|
373
410
|
```ruby
|
374
411
|
calculator = Keisan::Calculator.new
|
data/keisan.gemspec
CHANGED
@@ -19,9 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
end
|
20
20
|
spec.require_paths = ["lib"]
|
21
21
|
|
22
|
-
spec.required_ruby_version = ">= 2.
|
23
|
-
|
24
|
-
spec.add_dependency "activesupport", ">= 4.2.2"
|
22
|
+
spec.required_ruby_version = ">= 2.3.0"
|
25
23
|
|
26
24
|
spec.add_development_dependency "coveralls"
|
27
25
|
spec.add_development_dependency "bundler", "~> 1.14"
|
data/lib/keisan.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
|
-
require "active_support"
|
2
|
-
require "active_support/core_ext"
|
3
|
-
|
4
1
|
require "keisan/version"
|
5
2
|
require "keisan/exceptions"
|
3
|
+
require "keisan/util"
|
6
4
|
|
7
5
|
require "keisan/ast/node"
|
8
6
|
require "keisan/ast/cell"
|
@@ -14,6 +12,8 @@ require "keisan/ast/number"
|
|
14
12
|
require "keisan/ast/string"
|
15
13
|
require "keisan/ast/null"
|
16
14
|
require "keisan/ast/boolean"
|
15
|
+
require "keisan/ast/date"
|
16
|
+
require "keisan/ast/time"
|
17
17
|
|
18
18
|
require "keisan/ast/block"
|
19
19
|
require "keisan/ast/parent"
|
data/lib/keisan/ast.rb
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
require "date"
|
2
|
+
require "time"
|
3
|
+
|
1
4
|
module Keisan
|
2
5
|
module AST
|
3
6
|
def self.parse(expression)
|
@@ -76,6 +79,26 @@ module KeisanHash
|
|
76
79
|
end
|
77
80
|
end
|
78
81
|
|
82
|
+
module KeisanDate
|
83
|
+
def to_node
|
84
|
+
Keisan::AST::Date.new(self)
|
85
|
+
end
|
86
|
+
|
87
|
+
def value(context = nil)
|
88
|
+
self
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
module KeisanTime
|
93
|
+
def to_node
|
94
|
+
Keisan::AST::Time.new(self)
|
95
|
+
end
|
96
|
+
|
97
|
+
def value(context = nil)
|
98
|
+
self
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
79
102
|
class Numeric; prepend KeisanNumeric; end
|
80
103
|
class String; prepend KeisanString; end
|
81
104
|
class TrueClass; prepend KeisanTrueClass; end
|
@@ -83,3 +106,5 @@ class FalseClass; prepend KeisanFalseClass; end
|
|
83
106
|
class NilClass; prepend KeisanNilClass; end
|
84
107
|
class Array; prepend KeisanArray; end
|
85
108
|
class Hash; prepend KeisanHash; end
|
109
|
+
class Date; prepend KeisanDate; end
|
110
|
+
class Time; prepend KeisanTime; end
|
data/lib/keisan/ast/boolean.rb
CHANGED
data/lib/keisan/ast/builder.rb
CHANGED
@@ -12,10 +12,10 @@ module Keisan
|
|
12
12
|
elsif !parser.nil?
|
13
13
|
@components = parser.components
|
14
14
|
else
|
15
|
-
@components = Array
|
15
|
+
@components = Array(components)
|
16
16
|
end
|
17
17
|
|
18
|
-
@lines = @components
|
18
|
+
@lines = Util.array_split(@components) {|component|
|
19
19
|
component.is_a?(Parsing::LineSeparator)
|
20
20
|
}.reject(&:empty?)
|
21
21
|
|
data/lib/keisan/ast/cell.rb
CHANGED
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative "date_time_methods"
|
2
|
+
|
3
|
+
module Keisan
|
4
|
+
module AST
|
5
|
+
class Date < ConstantLiteral
|
6
|
+
include DateTimeMethods
|
7
|
+
|
8
|
+
attr_reader :date
|
9
|
+
|
10
|
+
def initialize(date)
|
11
|
+
@date = date
|
12
|
+
end
|
13
|
+
|
14
|
+
def value(context = nil)
|
15
|
+
date
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
value.to_s
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module Keisan
|
2
|
+
module AST
|
3
|
+
module DateTimeMethods
|
4
|
+
def +(other)
|
5
|
+
other = other.to_node
|
6
|
+
case other
|
7
|
+
when Number
|
8
|
+
self.class.new(value + other.value)
|
9
|
+
else
|
10
|
+
super
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def >(other)
|
15
|
+
other = other.to_node
|
16
|
+
case other
|
17
|
+
when self.class
|
18
|
+
Boolean.new(value.to_time > other.value.to_time)
|
19
|
+
else
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def <(other)
|
25
|
+
other = other.to_node
|
26
|
+
case other
|
27
|
+
when self.class
|
28
|
+
Boolean.new(value.to_time < other.value.to_time)
|
29
|
+
else
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def >=(other)
|
35
|
+
other = other.to_node
|
36
|
+
case other
|
37
|
+
when self.class
|
38
|
+
Boolean.new(value.to_time >= other.value.to_time)
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def <=(other)
|
45
|
+
other = other.to_node
|
46
|
+
case other
|
47
|
+
when self.class
|
48
|
+
Boolean.new(value.to_time <= other.value.to_time)
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def equal(other)
|
55
|
+
other = other.to_node
|
56
|
+
case other
|
57
|
+
when self.class
|
58
|
+
Boolean.new(value.to_time == other.value.to_time)
|
59
|
+
else
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def not_equal(other)
|
65
|
+
other = other.to_node
|
66
|
+
case other
|
67
|
+
when self.class
|
68
|
+
Boolean.new(value.to_time != other.value.to_time)
|
69
|
+
else
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
data/lib/keisan/ast/function.rb
CHANGED
@@ -13,6 +13,15 @@ module Keisan
|
|
13
13
|
function_from_context(context).value(self, context)
|
14
14
|
end
|
15
15
|
|
16
|
+
def unbound_variables(context = nil)
|
17
|
+
context ||= Context.new
|
18
|
+
if context.has_function?(name)
|
19
|
+
function_from_context(context).unbound_variables(children, context)
|
20
|
+
else
|
21
|
+
super
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
16
25
|
def unbound_functions(context = nil)
|
17
26
|
context ||= Context.new
|
18
27
|
|
@@ -5,18 +5,35 @@ module Keisan
|
|
5
5
|
:"&&"
|
6
6
|
end
|
7
7
|
|
8
|
+
def blank_value
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
8
12
|
def evaluate(context = nil)
|
9
|
-
|
13
|
+
short_circuit_do(:evaluate, context)
|
10
14
|
end
|
11
15
|
|
12
|
-
def
|
13
|
-
|
16
|
+
def simplify(context = nil)
|
17
|
+
short_circuit_do(:simplify, context)
|
14
18
|
end
|
15
19
|
|
16
20
|
def value(context = nil)
|
17
21
|
context ||= Context.new
|
18
22
|
children[0].value(context) && children[1].value(context)
|
19
23
|
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def short_circuit_do(method, context)
|
28
|
+
context ||= Context.new
|
29
|
+
lhs = children[0].send(method, context)
|
30
|
+
case lhs
|
31
|
+
when AST::Boolean
|
32
|
+
lhs.false? ? AST::Boolean.new(false) : children[1].send(method, context)
|
33
|
+
else
|
34
|
+
lhs.and(children[1].send(method, context))
|
35
|
+
end
|
36
|
+
end
|
20
37
|
end
|
21
38
|
end
|
22
39
|
end
|
@@ -5,13 +5,14 @@ module Keisan
|
|
5
5
|
:"=="
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
protected
|
9
|
+
|
10
|
+
def value_operator
|
11
|
+
:==
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
-
children[0].value(context) == children[1].value(context)
|
14
|
+
def operator
|
15
|
+
:equal
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
@@ -5,12 +5,14 @@ module Keisan
|
|
5
5
|
:">"
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
protected
|
9
|
+
|
10
|
+
def value_operator
|
11
|
+
:>
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
+
def operator
|
15
|
+
:>
|
14
16
|
end
|
15
17
|
end
|
16
18
|
end
|
@@ -5,12 +5,14 @@ module Keisan
|
|
5
5
|
:">="
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
protected
|
9
|
+
|
10
|
+
def value_operator
|
11
|
+
:>=
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
+
def operator
|
15
|
+
:>=
|
14
16
|
end
|
15
17
|
end
|
16
18
|
end
|
@@ -5,12 +5,14 @@ module Keisan
|
|
5
5
|
:"<"
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
protected
|
9
|
+
|
10
|
+
def value_operator
|
11
|
+
:<
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
+
def operator
|
15
|
+
:<
|
14
16
|
end
|
15
17
|
end
|
16
18
|
end
|
@@ -5,12 +5,14 @@ module Keisan
|
|
5
5
|
:"<="
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
protected
|
9
|
+
|
10
|
+
def value_operator
|
11
|
+
:<=
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
+
def operator
|
15
|
+
:<=
|
14
16
|
end
|
15
17
|
end
|
16
18
|
end
|
@@ -5,13 +5,14 @@ module Keisan
|
|
5
5
|
:"!="
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
8
|
+
protected
|
9
|
+
|
10
|
+
def value_operator
|
11
|
+
:!=
|
10
12
|
end
|
11
13
|
|
12
|
-
def
|
13
|
-
|
14
|
-
children[0].value(context) != children[1].value(context)
|
14
|
+
def operator
|
15
|
+
:not_equal
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
@@ -1,6 +1,30 @@
|
|
1
1
|
module Keisan
|
2
2
|
module AST
|
3
3
|
class LogicalOperator < Operator
|
4
|
+
def evaluate(context = nil)
|
5
|
+
context ||= Context.new
|
6
|
+
children[0].evaluate(context).send(operator, children[1].evaluate(context))
|
7
|
+
end
|
8
|
+
|
9
|
+
def simplify(context = nil)
|
10
|
+
context ||= Context.new
|
11
|
+
children[0].simplify(context).send(operator, children[1].simplify(context))
|
12
|
+
end
|
13
|
+
|
14
|
+
def value(context=nil)
|
15
|
+
context ||= Context.new
|
16
|
+
children[0].value(context).send(value_operator, children[1].value(context))
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def value_operator
|
22
|
+
raise Exceptions::NotImplementedError.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def operator
|
26
|
+
raise Exceptions::NotImplementedError.new
|
27
|
+
end
|
4
28
|
end
|
5
29
|
end
|
6
30
|
end
|
@@ -10,13 +10,30 @@ module Keisan
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def evaluate(context = nil)
|
13
|
-
|
13
|
+
short_circuit_do(:evaluate, context)
|
14
|
+
end
|
15
|
+
|
16
|
+
def simplify(context = nil)
|
17
|
+
short_circuit_do(:simplify, context)
|
14
18
|
end
|
15
19
|
|
16
20
|
def value(context = nil)
|
17
21
|
context ||= Context.new
|
18
22
|
children[0].value(context) || children[1].value(context)
|
19
23
|
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def short_circuit_do(method, context)
|
28
|
+
context ||= Context.new
|
29
|
+
lhs = children[0].send(method, context)
|
30
|
+
case lhs
|
31
|
+
when AST::Boolean
|
32
|
+
lhs.true? ? AST::Boolean.new(true) : children[1].send(method, context)
|
33
|
+
else
|
34
|
+
lhs.or(children[1].send(method, context))
|
35
|
+
end
|
36
|
+
end
|
20
37
|
end
|
21
38
|
end
|
22
39
|
end
|
data/lib/keisan/ast/number.rb
CHANGED
data/lib/keisan/ast/operator.rb
CHANGED
data/lib/keisan/ast/parent.rb
CHANGED
@@ -4,7 +4,7 @@ module Keisan
|
|
4
4
|
attr_reader :children
|
5
5
|
|
6
6
|
def initialize(children = [])
|
7
|
-
children = Array
|
7
|
+
children = Array(children).map do |child|
|
8
8
|
child.is_a?(Cell) ? child : child.to_node
|
9
9
|
end
|
10
10
|
raise Exceptions::InternalError.new unless children.is_a?(Array)
|
data/lib/keisan/ast/plus.rb
CHANGED
@@ -22,6 +22,10 @@ module Keisan
|
|
22
22
|
# Special case of array concatenation
|
23
23
|
elsif children_values.all? {|child| child.is_a?(::Array)}
|
24
24
|
children_values.inject([], &:+)
|
25
|
+
elsif children_values.one? {|child| child.is_a?(::Date)}
|
26
|
+
date_time_plus(children_values, ::Date)
|
27
|
+
elsif children_values.one? {|child| child.is_a?(::Time)}
|
28
|
+
date_time_plus(children_values, ::Time)
|
25
29
|
else
|
26
30
|
children_values.inject(0, &:+)
|
27
31
|
end.to_node.value(context)
|
@@ -77,6 +81,12 @@ module Keisan
|
|
77
81
|
|
78
82
|
private
|
79
83
|
|
84
|
+
def date_time_plus(elements, klass)
|
85
|
+
date_time = elements.select {|child| child.is_a?(klass)}.first
|
86
|
+
others = elements.select {|child| !child.is_a?(klass)}
|
87
|
+
date_time + others.inject(0, &:+)
|
88
|
+
end
|
89
|
+
|
80
90
|
def convert_minus_to_plus!
|
81
91
|
@parsing_operators.each.with_index do |parsing_operator, index|
|
82
92
|
if parsing_operator.is_a?(Parsing::Minus)
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require_relative "date_time_methods"
|
2
|
+
|
3
|
+
module Keisan
|
4
|
+
module AST
|
5
|
+
class Time < ConstantLiteral
|
6
|
+
include DateTimeMethods
|
7
|
+
|
8
|
+
attr_reader :time
|
9
|
+
|
10
|
+
def initialize(time)
|
11
|
+
@time = time
|
12
|
+
end
|
13
|
+
|
14
|
+
def value(context = nil)
|
15
|
+
time
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
value.strftime("%Y-%m-%d %H:%M:%S")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -2,7 +2,7 @@ module Keisan
|
|
2
2
|
module AST
|
3
3
|
class UnaryOperator < Operator
|
4
4
|
def initialize(children = [])
|
5
|
-
children = Array
|
5
|
+
children = Array(children)
|
6
6
|
super(children)
|
7
7
|
if children.count != 1
|
8
8
|
raise Exceptions::ASTError.new("Unary operator takes has a single child")
|
data/lib/keisan/ast/variable.rb
CHANGED
@@ -60,11 +60,8 @@ module Keisan
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def replace(variable, replacement)
|
63
|
-
|
64
|
-
|
65
|
-
else
|
66
|
-
self
|
67
|
-
end
|
63
|
+
to_replace_name = variable.is_a?(::String) ? variable : variable.name
|
64
|
+
name == to_replace_name ? replacement : self
|
68
65
|
end
|
69
66
|
|
70
67
|
def differentiate(variable, context = nil)
|
data/lib/keisan/context.rb
CHANGED
@@ -4,8 +4,8 @@ module Keisan
|
|
4
4
|
|
5
5
|
def initialize(parent: nil, random: nil, allow_recursive: false, shadowed: [])
|
6
6
|
@parent = parent
|
7
|
-
@function_registry = Functions::Registry.new(parent: @parent
|
8
|
-
@variable_registry = Variables::Registry.new(parent: @parent
|
7
|
+
@function_registry = Functions::Registry.new(parent: @parent&.function_registry)
|
8
|
+
@variable_registry = Variables::Registry.new(parent: @parent&.variable_registry, shadowed: shadowed)
|
9
9
|
@random = random
|
10
10
|
@allow_recursive = allow_recursive
|
11
11
|
end
|
@@ -47,7 +47,7 @@ module Keisan
|
|
47
47
|
|
48
48
|
def transient_definitions
|
49
49
|
return {} unless @transient
|
50
|
-
parent_definitions = @parent.
|
50
|
+
parent_definitions = @parent.nil? ? {} : @parent.transient_definitions
|
51
51
|
parent_definitions.merge(
|
52
52
|
@variable_registry.locals
|
53
53
|
).merge(
|
@@ -73,7 +73,7 @@ module Keisan
|
|
73
73
|
|
74
74
|
def register_variable!(name, value, local: false)
|
75
75
|
if !@variable_registry.shadowed.member?(name) && (transient? || !local && @parent&.variable_is_modifiable?(name))
|
76
|
-
@parent.register_variable!(name, value)
|
76
|
+
@parent.register_variable!(name, value, local: local)
|
77
77
|
else
|
78
78
|
@variable_registry.register!(name, value)
|
79
79
|
end
|
@@ -93,14 +93,14 @@ module Keisan
|
|
93
93
|
|
94
94
|
def register_function!(name, function, local: false)
|
95
95
|
if transient? || !local && @parent&.function_is_modifiable?(name)
|
96
|
-
@parent.register_function!(name, function)
|
96
|
+
@parent.register_function!(name, function, local: local)
|
97
97
|
else
|
98
98
|
@function_registry.register!(name.to_s, function)
|
99
99
|
end
|
100
100
|
end
|
101
101
|
|
102
102
|
def random
|
103
|
-
@random || @parent
|
103
|
+
@random || @parent&.random || Random.new
|
104
104
|
end
|
105
105
|
|
106
106
|
protected
|
data/lib/keisan/exceptions.rb
CHANGED
data/lib/keisan/function.rb
CHANGED
@@ -23,6 +23,12 @@ module Keisan
|
|
23
23
|
raise Exceptions::NotImplementedError.new
|
24
24
|
end
|
25
25
|
|
26
|
+
def unbound_variables(children, context)
|
27
|
+
children.inject(Set.new) do |vars, child|
|
28
|
+
vars | child.unbound_variables(context)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
26
32
|
protected
|
27
33
|
|
28
34
|
def validate_arguments!(count)
|
@@ -1,5 +1,7 @@
|
|
1
1
|
require_relative "let"
|
2
2
|
require_relative "puts"
|
3
|
+
require_relative "break"
|
4
|
+
require_relative "continue"
|
3
5
|
|
4
6
|
require_relative "if"
|
5
7
|
require_relative "while"
|
@@ -49,6 +51,8 @@ module Keisan
|
|
49
51
|
def self.register_defaults!(registry)
|
50
52
|
registry.register!(:let, Let.new, force: true)
|
51
53
|
registry.register!(:puts, Puts.new, force: true)
|
54
|
+
registry.register!(:break, Break.new, force: true)
|
55
|
+
registry.register!(:continue, Continue.new, force: true)
|
52
56
|
|
53
57
|
registry.register!(:if, If.new, force: true)
|
54
58
|
registry.register!(:while, While.new, force: true)
|
@@ -65,6 +69,7 @@ module Keisan
|
|
65
69
|
register_math!(registry)
|
66
70
|
register_array_methods!(registry)
|
67
71
|
register_random_methods!(registry)
|
72
|
+
register_date_time_methods!(registry)
|
68
73
|
end
|
69
74
|
|
70
75
|
def self.register_math!(registry)
|
@@ -124,6 +129,37 @@ module Keisan
|
|
124
129
|
registry.register!(:rand, Rand.new, force: true)
|
125
130
|
registry.register!(:sample, Sample.new, force: true)
|
126
131
|
end
|
132
|
+
|
133
|
+
def self.register_date_time_methods!(registry)
|
134
|
+
register_date_time!(registry)
|
135
|
+
|
136
|
+
registry.register!(:today, Proc.new { ::Date.today }, force: true)
|
137
|
+
registry.register!(:day, Proc.new {|d| d.mday }, force: true)
|
138
|
+
registry.register!(:weekday, Proc.new {|d| d.wday }, force: true)
|
139
|
+
registry.register!(:month, Proc.new {|d| d.month }, force: true)
|
140
|
+
registry.register!(:year, Proc.new {|d| d.year }, force: true)
|
141
|
+
|
142
|
+
registry.register!(:now, Proc.new { ::Time.now }, force: true)
|
143
|
+
registry.register!(:hour, Proc.new {|t| t.hour }, force: true)
|
144
|
+
registry.register!(:minute, Proc.new {|t| t.min }, force: true)
|
145
|
+
registry.register!(:second, Proc.new {|t| t.sec }, force: true)
|
146
|
+
registry.register!(:strftime, Proc.new {|*args| args.first.strftime(*args[1..-1]) }, force: true)
|
147
|
+
|
148
|
+
registry.register!(:to_time, Proc.new {|d| d.to_time }, force: true)
|
149
|
+
registry.register!(:to_date, Proc.new {|t| t.to_date }, force: true)
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.register_date_time!(registry)
|
153
|
+
[::Date, ::Time].each do |klass|
|
154
|
+
registry.register!(klass.to_s.downcase.to_sym, Proc.new {|*args|
|
155
|
+
if args.count == 1 && args.first.is_a?(::String)
|
156
|
+
AST.const_get(klass.to_s).new(klass.parse(args.first))
|
157
|
+
else
|
158
|
+
AST.const_get(klass.to_s).new(klass.new(*args))
|
159
|
+
end
|
160
|
+
}, force: true)
|
161
|
+
end
|
162
|
+
end
|
127
163
|
end
|
128
164
|
end
|
129
165
|
end
|
@@ -12,6 +12,10 @@ module Keisan
|
|
12
12
|
evaluate(ast_function, context)
|
13
13
|
end
|
14
14
|
|
15
|
+
def unbound_variables(children, context)
|
16
|
+
super - Set.new(shadowing_variable_names(children).map(&:name))
|
17
|
+
end
|
18
|
+
|
15
19
|
def evaluate(ast_function, context = nil)
|
16
20
|
validate_arguments!(ast_function.children.count)
|
17
21
|
context ||= Context.new
|
@@ -20,9 +24,9 @@ module Keisan
|
|
20
24
|
|
21
25
|
case operand
|
22
26
|
when AST::List
|
23
|
-
evaluate_list(operand, arguments, expression, context)
|
27
|
+
evaluate_list(operand, arguments, expression, context).evaluate(context)
|
24
28
|
when AST::Hash
|
25
|
-
evaluate_hash(operand, arguments, expression, context)
|
29
|
+
evaluate_hash(operand, arguments, expression, context).evaluate(context)
|
26
30
|
else
|
27
31
|
raise Exceptions::InvalidFunctionError.new("Unhandled first argument to #{name}: #{operand}")
|
28
32
|
end
|
@@ -34,6 +38,10 @@ module Keisan
|
|
34
38
|
|
35
39
|
protected
|
36
40
|
|
41
|
+
def shadowing_variable_names(children)
|
42
|
+
raise Exceptions::NotImplementedError.new
|
43
|
+
end
|
44
|
+
|
37
45
|
def verify_arguments!(arguments)
|
38
46
|
unless arguments.all? {|argument| argument.is_a?(AST::Variable)}
|
39
47
|
raise Exceptions::InvalidFunctionError.new("Middle arguments to #{name} must be variables")
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Keisan
|
2
|
+
module Functions
|
3
|
+
class LoopControlFlowFuntion < Function
|
4
|
+
def initialize(name, exception_class)
|
5
|
+
super(name, 0)
|
6
|
+
@exception_class = exception_class
|
7
|
+
end
|
8
|
+
|
9
|
+
def value(ast_function, context = nil)
|
10
|
+
raise @exception_class.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def evaluate(ast_function, context = nil)
|
14
|
+
raise @exception_class.new
|
15
|
+
end
|
16
|
+
|
17
|
+
def simplify(ast_function, context = nil)
|
18
|
+
raise @exception_class.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/keisan/functions/map.rb
CHANGED
@@ -6,12 +6,17 @@ module Keisan
|
|
6
6
|
# Reduces (list, initial, accumulator, variable, expression)
|
7
7
|
# e.g. reduce([1,2,3,4], 0, total, x, total+x)
|
8
8
|
# should give 10
|
9
|
+
# When hash: (hash, initial, accumulator, key, value, expression)
|
9
10
|
def initialize
|
10
11
|
super("reduce")
|
11
12
|
end
|
12
13
|
|
13
14
|
protected
|
14
15
|
|
16
|
+
def shadowing_variable_names(children)
|
17
|
+
children.size == 5 ? children[2..3] : children[2..4]
|
18
|
+
end
|
19
|
+
|
15
20
|
def verify_arguments!(arguments)
|
16
21
|
unless arguments[1..-1].all? {|argument| argument.is_a?(AST::Variable)}
|
17
22
|
raise Exceptions::InvalidFunctionError.new("Middle arguments to #{name} must be variables")
|
@@ -28,7 +28,13 @@ module Keisan
|
|
28
28
|
current = Keisan::AST::Null.new
|
29
29
|
|
30
30
|
while logical_node_evaluates_to_true(logical_node, context)
|
31
|
-
|
31
|
+
begin
|
32
|
+
current = body_node.evaluated(context)
|
33
|
+
rescue Exceptions::BreakError
|
34
|
+
break
|
35
|
+
rescue Exceptions::ContinueError
|
36
|
+
next
|
37
|
+
end
|
32
38
|
end
|
33
39
|
|
34
40
|
current
|
data/lib/keisan/parser.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Keisan
|
2
2
|
class Parser
|
3
|
-
KEYWORDS = %w(let puts).freeze
|
3
|
+
KEYWORDS = %w(let puts break continue).freeze
|
4
4
|
|
5
5
|
attr_reader :tokens, :components
|
6
6
|
|
@@ -39,7 +39,7 @@ module Keisan
|
|
39
39
|
end
|
40
40
|
|
41
41
|
def parse_multi_line!
|
42
|
-
line_parsers = @tokens
|
42
|
+
line_parsers = Util.array_split(@tokens) {|token| token.is_a?(Tokens::LineSeparator)}.map {|tokens| self.class.new(tokens: tokens)}
|
43
43
|
@components = []
|
44
44
|
line_parsers.each.with_index do |line_parser, i|
|
45
45
|
@components += line_parser.components
|
@@ -52,7 +52,7 @@ module Keisan
|
|
52
52
|
def parse_keyword!
|
53
53
|
keyword = tokens.first.string
|
54
54
|
arguments = if tokens[1].is_a?(Tokens::Group)
|
55
|
-
tokens[1].sub_tokens
|
55
|
+
Util.array_split(tokens[1].sub_tokens) {|token| token.is_a?(Tokens::Comma)}.map {|argument_tokens|
|
56
56
|
Parsing::Argument.new(argument_tokens)
|
57
57
|
}
|
58
58
|
else
|
@@ -212,7 +212,7 @@ module Keisan
|
|
212
212
|
@components << Parsing::List.new(arguments_from_group(token))
|
213
213
|
when :curly
|
214
214
|
if token.sub_tokens.any? {|token| token.is_a?(Tokens::Colon)}
|
215
|
-
@components << Parsing::Hash.new(token.sub_tokens
|
215
|
+
@components << Parsing::Hash.new(Util.array_split(token.sub_tokens) {|token| token.is_a?(Tokens::Comma)})
|
216
216
|
else
|
217
217
|
@components << Parsing::CurlyGroup.new(token.sub_tokens)
|
218
218
|
end
|
@@ -280,7 +280,7 @@ module Keisan
|
|
280
280
|
if token.sub_tokens.empty?
|
281
281
|
[]
|
282
282
|
else
|
283
|
-
token.sub_tokens
|
283
|
+
Util.array_split(token.sub_tokens) {|sub_token| sub_token.is_a?(Tokens::Comma)}.map do |sub_tokens|
|
284
284
|
Parsing::Argument.new(sub_tokens)
|
285
285
|
end
|
286
286
|
end
|
data/lib/keisan/parsing/hash.rb
CHANGED
@@ -4,7 +4,7 @@ module Keisan
|
|
4
4
|
attr_reader :key_value_pairs
|
5
5
|
|
6
6
|
def initialize(key_value_pairs)
|
7
|
-
@key_value_pairs = Array
|
7
|
+
@key_value_pairs = Array(key_value_pairs).map {|key_value_pair|
|
8
8
|
validate_and_extract_key_value_pair(key_value_pair)
|
9
9
|
}
|
10
10
|
end
|
@@ -12,7 +12,7 @@ module Keisan
|
|
12
12
|
private
|
13
13
|
|
14
14
|
def validate_and_extract_key_value_pair(key_value_pair)
|
15
|
-
key, value = key_value_pair
|
15
|
+
key, value = Util.array_split(key_value_pair) {|token| token.is_a?(Tokens::Colon)}
|
16
16
|
raise Exceptions::ParseError.new("Invalid hash") unless key.size == 1 && value.size >= 1
|
17
17
|
|
18
18
|
key = key.first
|
data/lib/keisan/token.rb
CHANGED
data/lib/keisan/util.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Keisan
|
2
|
+
class Util
|
3
|
+
def self.underscore(str)
|
4
|
+
str.to_s.gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').downcase
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.array_split(array, &block)
|
8
|
+
array.inject([[]]) do |results, element|
|
9
|
+
if yield(element)
|
10
|
+
results << []
|
11
|
+
else
|
12
|
+
results.last << element
|
13
|
+
end
|
14
|
+
|
15
|
+
results
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/keisan/version.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: keisan
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.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: 2018-
|
11
|
+
date: 2018-11-23 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: activesupport
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - ">="
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: 4.2.2
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - ">="
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: 4.2.2
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: coveralls
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -140,6 +126,8 @@ files:
|
|
140
126
|
- lib/keisan/ast/cell.rb
|
141
127
|
- lib/keisan/ast/cell_assignment.rb
|
142
128
|
- lib/keisan/ast/constant_literal.rb
|
129
|
+
- lib/keisan/ast/date.rb
|
130
|
+
- lib/keisan/ast/date_time_methods.rb
|
143
131
|
- lib/keisan/ast/exponent.rb
|
144
132
|
- lib/keisan/ast/function.rb
|
145
133
|
- lib/keisan/ast/function_assignment.rb
|
@@ -166,6 +154,7 @@ files:
|
|
166
154
|
- lib/keisan/ast/parent.rb
|
167
155
|
- lib/keisan/ast/plus.rb
|
168
156
|
- lib/keisan/ast/string.rb
|
157
|
+
- lib/keisan/ast/time.rb
|
169
158
|
- lib/keisan/ast/times.rb
|
170
159
|
- lib/keisan/ast/unary_bitwise_not.rb
|
171
160
|
- lib/keisan/ast/unary_identity.rb
|
@@ -182,8 +171,10 @@ files:
|
|
182
171
|
- lib/keisan/exceptions.rb
|
183
172
|
- lib/keisan/function.rb
|
184
173
|
- lib/keisan/functions/abs.rb
|
174
|
+
- lib/keisan/functions/break.rb
|
185
175
|
- lib/keisan/functions/cbrt.rb
|
186
176
|
- lib/keisan/functions/cmath_function.rb
|
177
|
+
- lib/keisan/functions/continue.rb
|
187
178
|
- lib/keisan/functions/cos.rb
|
188
179
|
- lib/keisan/functions/cosh.rb
|
189
180
|
- lib/keisan/functions/cot.rb
|
@@ -201,6 +192,7 @@ files:
|
|
201
192
|
- lib/keisan/functions/imag.rb
|
202
193
|
- lib/keisan/functions/let.rb
|
203
194
|
- lib/keisan/functions/log.rb
|
195
|
+
- lib/keisan/functions/loop_control_flow_function.rb
|
204
196
|
- lib/keisan/functions/map.rb
|
205
197
|
- lib/keisan/functions/math_function.rb
|
206
198
|
- lib/keisan/functions/proc_function.rb
|
@@ -292,6 +284,7 @@ files:
|
|
292
284
|
- lib/keisan/tokens/string.rb
|
293
285
|
- lib/keisan/tokens/unknown.rb
|
294
286
|
- lib/keisan/tokens/word.rb
|
287
|
+
- lib/keisan/util.rb
|
295
288
|
- lib/keisan/variables/default_registry.rb
|
296
289
|
- lib/keisan/variables/registry.rb
|
297
290
|
- lib/keisan/version.rb
|
@@ -308,7 +301,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
308
301
|
requirements:
|
309
302
|
- - ">="
|
310
303
|
- !ruby/object:Gem::Version
|
311
|
-
version: 2.
|
304
|
+
version: 2.3.0
|
312
305
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
313
306
|
requirements:
|
314
307
|
- - ">="
|
@@ -316,7 +309,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
316
309
|
version: '0'
|
317
310
|
requirements: []
|
318
311
|
rubyforge_project:
|
319
|
-
rubygems_version: 2.
|
312
|
+
rubygems_version: 2.7.7
|
320
313
|
signing_key:
|
321
314
|
specification_version: 4
|
322
315
|
summary: An equation parser and evaluator
|