keisan 0.7.0 → 0.8.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 +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
|