kalculator 0.6.4 → 1.0.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 +4 -4
- data/lib/kalculator.rb +15 -4
- data/lib/kalculator/built_in_functions.rb +22 -0
- data/lib/kalculator/errors.rb +7 -1
- data/lib/kalculator/evaluator.rb +55 -74
- data/lib/kalculator/formula.rb +8 -2
- data/lib/kalculator/lexer.rb +2 -7
- data/lib/kalculator/parser.rb +45 -38
- data/lib/kalculator/pointer.rb +16 -0
- data/lib/kalculator/transform.rb +9 -4
- data/lib/kalculator/type_sources.rb +36 -0
- data/lib/kalculator/types.rb +86 -0
- data/lib/kalculator/validator.rb +240 -0
- data/lib/kalculator/version.rb +1 -1
- metadata +8 -4
- data/lib/kalculator/nested_lookup.rb +0 -25
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 96f3aedf0934ea881aac16720904825d1861878d7e00d55b8ce81f3f01b4ffdd
|
4
|
+
data.tar.gz: c5ceb6b7b8b9b232b31a81c08f03499c3fdd46b7968fb99f48f240929dfcbb40
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eeb67bb90056d4d8ffc9d3b9d0f07dc0a1441c9da6a3285dca9c6102653e2fb95b0057223ab88d0171dd2030ea3f9cbe43e2986b6ebf8e7aabf7c6acadb645fd
|
7
|
+
data.tar.gz: 581292d72c21d1d8d593a50863ccab63c776570e725bf1444c9b1fa8db63a4e4448a74cff0201932d4191b6e42284ff06b2761fc9e9397646119bf453df27f54
|
data/lib/kalculator.rb
CHANGED
@@ -1,19 +1,30 @@
|
|
1
1
|
require "rltk"
|
2
|
+
require "date"
|
3
|
+
require "kalculator/built_in_functions"
|
2
4
|
require "kalculator/data_sources"
|
3
5
|
require "kalculator/errors"
|
4
6
|
require "kalculator/evaluator"
|
5
7
|
require "kalculator/formula"
|
6
8
|
require "kalculator/lexer"
|
7
|
-
require "kalculator/nested_lookup"
|
8
9
|
require "kalculator/parser"
|
10
|
+
require "kalculator/pointer"
|
9
11
|
require "kalculator/transform"
|
12
|
+
require "kalculator/type_sources"
|
13
|
+
require "kalculator/types"
|
14
|
+
require "kalculator/validator"
|
10
15
|
require "kalculator/version"
|
11
16
|
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
|
12
21
|
class Kalculator
|
13
|
-
def self.evaluate(formula, data_source = {})
|
14
|
-
Kalculator::Formula.new(formula).evaluate(data_source)
|
22
|
+
def self.evaluate(formula, data_source = {}, custom_functions = {})
|
23
|
+
Kalculator::Formula.new(formula).evaluate(data_source, custom_functions)
|
24
|
+
end
|
25
|
+
def self.validate(formula, type_source = Kalculator::TypeSources.new(Hash.new) )
|
26
|
+
Kalculator::Formula.new(formula).validate(type_source)
|
15
27
|
end
|
16
|
-
|
17
28
|
def self.new(*args)
|
18
29
|
Kalculator::Formula.new(*args)
|
19
30
|
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Kalculator
|
2
|
+
BUILT_IN_FUNCTIONS = {
|
3
|
+
["contains", 2] => lambda { |collection, item|
|
4
|
+
collection.include?(item)
|
5
|
+
},
|
6
|
+
["count", 1] => lambda { |list|
|
7
|
+
list.count
|
8
|
+
},
|
9
|
+
["date", 1] => lambda { |str|
|
10
|
+
Date.parse(str)
|
11
|
+
},
|
12
|
+
["max", 2] => lambda { |left, right|
|
13
|
+
[left, right].max
|
14
|
+
},
|
15
|
+
["min", 2] => lambda { |left, right|
|
16
|
+
[left, right].min
|
17
|
+
},
|
18
|
+
["sum", 1] => lambda { |list|
|
19
|
+
list.inject(0){|sum, num| sum + num}
|
20
|
+
},
|
21
|
+
}.freeze
|
22
|
+
end
|
data/lib/kalculator/errors.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
class Kalculator
|
2
|
-
class Error < ::StandardError
|
2
|
+
class Error < ::StandardError ##an error that holds metadata
|
3
|
+
attr_reader :metadata
|
4
|
+
def initialize(metadata)
|
5
|
+
@metadata = metadata
|
6
|
+
end
|
7
|
+
end
|
3
8
|
class TypeError < Error; end
|
9
|
+
class UndefinedFunctionError < Error; end
|
4
10
|
class UndefinedVariableError < Error; end
|
5
11
|
end
|
data/lib/kalculator/evaluator.rb
CHANGED
@@ -1,97 +1,96 @@
|
|
1
|
-
require "date"
|
2
1
|
|
3
2
|
class Kalculator
|
4
3
|
class Evaluator
|
5
|
-
def initialize(data_source)
|
4
|
+
def initialize(data_source, custom_functions = {})
|
5
|
+
|
6
6
|
@data_source = data_source
|
7
|
+
@functions = Kalculator::BUILT_IN_FUNCTIONS.merge(custom_functions)
|
7
8
|
end
|
8
9
|
|
9
10
|
def evaluate(ast)
|
11
|
+
|
10
12
|
send(ast.first, *ast)
|
11
13
|
end
|
12
14
|
|
13
|
-
def
|
15
|
+
def access(_, identifier, object, metadata)
|
16
|
+
a = evaluate(object)
|
17
|
+
if(a.key?(identifier))
|
18
|
+
b =a[identifier]
|
19
|
+
if(b.is_a?(Kalculator::Pointer))
|
20
|
+
b = @data_source[b.p]
|
21
|
+
end
|
22
|
+
if(b.is_a?(Kalculator::AnonymousPointer))
|
23
|
+
b = b.p
|
24
|
+
end
|
25
|
+
return b
|
26
|
+
end
|
27
|
+
raise UndefinedVariableError.new(metadata), "object #{a} doesn't have attribute #{identifier}"
|
28
|
+
end
|
29
|
+
def +(_, left, right, metadata)
|
14
30
|
evaluate(left) + evaluate(right)
|
15
31
|
end
|
16
32
|
|
17
|
-
def -(_, left, right)
|
33
|
+
def -(_, left, right, metadata)
|
18
34
|
evaluate(left) - evaluate(right)
|
19
35
|
end
|
20
36
|
|
21
|
-
def *(_, left, right)
|
37
|
+
def *(_, left, right, metadata)
|
22
38
|
evaluate(left) * evaluate(right)
|
23
39
|
end
|
24
40
|
|
25
|
-
def /(_, left, right)
|
41
|
+
def /(_, left, right, metadata)
|
26
42
|
evaluate(left) / evaluate(right)
|
27
43
|
end
|
28
44
|
|
29
|
-
def >(_, left, right)
|
45
|
+
def >(_, left, right, metadata)
|
30
46
|
evaluate(left) > evaluate(right)
|
31
47
|
end
|
32
48
|
|
33
|
-
def >=(_, left, right)
|
49
|
+
def >=(_, left, right, metadata)
|
34
50
|
evaluate(left) >= evaluate(right)
|
35
51
|
end
|
36
52
|
|
37
|
-
def <(_, left, right)
|
53
|
+
def <(_, left, right, metadata)
|
38
54
|
evaluate(left) < evaluate(right)
|
39
55
|
end
|
40
56
|
|
41
|
-
def <=(_, left, right)
|
57
|
+
def <=(_, left, right, metadata)
|
42
58
|
evaluate(left) <= evaluate(right)
|
43
59
|
end
|
44
60
|
|
45
|
-
def ==(_, left, right)
|
61
|
+
def ==(_, left, right, metadata)
|
46
62
|
evaluate(left) == evaluate(right)
|
47
63
|
end
|
48
64
|
|
49
|
-
def !=(_, left, right)
|
65
|
+
def !=(_, left, right, metadata)
|
50
66
|
evaluate(left) != evaluate(right)
|
51
67
|
end
|
52
68
|
|
53
|
-
def and(_, left, right)
|
69
|
+
def and(_, left, right, metadata)
|
54
70
|
evaluate(left) && evaluate(right)
|
55
71
|
end
|
56
72
|
|
57
|
-
def or(_, left, right)
|
73
|
+
def or(_, left, right, metadata)
|
58
74
|
evaluate(left) || evaluate(right)
|
59
75
|
end
|
60
76
|
|
61
|
-
def boolean(_, boolean)
|
77
|
+
def boolean(_, boolean,_, metadata)
|
62
78
|
boolean
|
63
79
|
end
|
64
80
|
|
65
|
-
def
|
66
|
-
|
67
|
-
item = evaluate(item)
|
68
|
-
if collection.is_a?(Array)
|
69
|
-
collection.include?(item)
|
70
|
-
elsif collection.is_a?(String) && item.is_a?(String)
|
71
|
-
collection.include?(item)
|
72
|
-
else
|
73
|
-
raise TypeError, "contains only works with strings or lists, got #{collection.inspect} and #{item.inspect}"
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
def count(_, collection)
|
78
|
-
collection = evaluate(collection)
|
79
|
-
raise TypeError, "count only works with Enumerable types, got #{collection.inspect}" unless collection.is_a?(Enumerable)
|
80
|
-
collection.count
|
81
|
+
def exists(_, variable, metadata)
|
82
|
+
@data_source.key?(variable)
|
81
83
|
end
|
82
84
|
|
83
|
-
def
|
84
|
-
|
85
|
-
|
86
|
-
|
85
|
+
def fn_call(_, fn_name, expressions, metadata)
|
86
|
+
key = [fn_name, expressions.count]
|
87
|
+
fn = @functions[key]
|
88
|
+
raise UndefinedFunctionError.new(metadata), "no such function #{fn_name}/#{expressions.count}" if fn.nil?
|
89
|
+
args = expressions.map{|expression| evaluate(expression) }
|
90
|
+
return fn.call(*args)
|
87
91
|
end
|
88
92
|
|
89
|
-
def
|
90
|
-
(_variable, name) = variable
|
91
|
-
@data_source.key?(name)
|
92
|
-
end
|
93
|
-
|
94
|
-
def if(_, condition, true_clause, false_clause)
|
93
|
+
def if(_, condition, true_clause, false_clause, metadata)
|
95
94
|
if evaluate(condition)
|
96
95
|
evaluate(true_clause)
|
97
96
|
else
|
@@ -99,59 +98,41 @@ class Kalculator
|
|
99
98
|
end
|
100
99
|
end
|
101
100
|
|
102
|
-
def list(_, expressions)
|
101
|
+
def list(_, expressions, metadata)
|
103
102
|
expressions.map{|expression| evaluate(expression) }
|
104
103
|
end
|
105
104
|
|
106
|
-
def
|
107
|
-
left = evaluate(left)
|
108
|
-
right = evaluate(right)
|
109
|
-
raise TypeError, "max only works with numbers, got #{left.inspect}" unless left.is_a?(Numeric)
|
110
|
-
raise TypeError, "max only works with numbers, got #{right.inspect}" unless right.is_a?(Numeric)
|
111
|
-
[left, right].max
|
112
|
-
end
|
113
|
-
|
114
|
-
def min(_, left, right)
|
115
|
-
left = evaluate(left)
|
116
|
-
right = evaluate(right)
|
117
|
-
raise TypeError, "min only works with numbers, got #{left.inspect}" unless left.is_a?(Numeric)
|
118
|
-
raise TypeError, "min only works with numbers, got #{right.inspect}" unless right.is_a?(Numeric)
|
119
|
-
[left, right].min
|
120
|
-
end
|
121
|
-
|
122
|
-
def not(_, expression)
|
105
|
+
def not(_, expression, metadata)
|
123
106
|
bool = evaluate(expression)
|
124
|
-
raise TypeError, "! only works with booleans, got #{bool.inspect}" unless bool === true || bool === false
|
125
107
|
!bool
|
126
108
|
end
|
127
109
|
|
128
|
-
def null(_, _)
|
110
|
+
def null(_, _,_, metadata)
|
129
111
|
nil
|
130
112
|
end
|
131
113
|
|
132
|
-
def number(_, number)
|
114
|
+
def number(_, number,_, metadata)
|
133
115
|
number
|
134
116
|
end
|
135
117
|
|
136
|
-
def percent(_, percent)
|
118
|
+
def percent(_, percent,_, metadata)
|
137
119
|
percent / 100.0
|
138
120
|
end
|
139
121
|
|
140
|
-
def string(_, string)
|
122
|
+
def string(_, string,_, metadata)
|
141
123
|
string
|
142
124
|
end
|
143
125
|
|
144
|
-
def
|
145
|
-
|
146
|
-
|
147
|
-
|
126
|
+
def variable(_, name, metadata)
|
127
|
+
raise UndefinedVariableError.new(metadata), "undefined variable #{name}" unless @data_source.key?(name)
|
128
|
+
a = @data_source[name]
|
129
|
+
if(a.is_a?(Pointer))
|
130
|
+
a = @data_source[a.p]
|
148
131
|
end
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
raise UndefinedVariableError, "undefined variable #{name}" unless @data_source.key?(name)
|
154
|
-
@data_source[name]
|
132
|
+
if(a.is_a?(Kalculator::AnonymousPointer))
|
133
|
+
a = a.p
|
134
|
+
end
|
135
|
+
return a
|
155
136
|
end
|
156
137
|
end
|
157
138
|
end
|
data/lib/kalculator/formula.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
|
1
2
|
class Kalculator
|
2
3
|
class Formula
|
3
4
|
attr_reader :ast, :string
|
@@ -7,8 +8,13 @@ class Kalculator
|
|
7
8
|
@string = string
|
8
9
|
end
|
9
10
|
|
10
|
-
def
|
11
|
-
Kalculator::
|
11
|
+
def validate( type_source = Kalculator::TypeSources.new(Hash.new))
|
12
|
+
Kalculator::Validator.new(type_source).validate(ast)
|
13
|
+
end
|
14
|
+
|
15
|
+
def evaluate(data_source = {}, custom_functions = {})
|
16
|
+
Kalculator::Evaluator.new(data_source, custom_functions).evaluate(ast)
|
12
17
|
end
|
18
|
+
|
13
19
|
end
|
14
20
|
end
|
data/lib/kalculator/lexer.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
class Kalculator
|
2
2
|
class Lexer < RLTK::Lexer
|
3
3
|
rule(/\s/)
|
4
|
+
rule(/\./) { :PERIOD }
|
4
5
|
rule(/\(/) { :LPAREN }
|
5
6
|
rule(/\)/) { :RPAREN }
|
6
7
|
rule(/\[/) { :LBRACKET }
|
@@ -27,14 +28,8 @@ class Kalculator
|
|
27
28
|
rule(/\-?\d+/) { |t| [:NUMBER, t.to_i] }
|
28
29
|
rule(/\-?\.\d+/) { |t| [:NUMBER, t.to_f] }
|
29
30
|
rule(/\-?\d+\.\d+/) { |t| [:NUMBER, t.to_f] }
|
30
|
-
rule(/contains/) { |t| :CONTAINS }
|
31
|
-
rule(/count/) { |t| :COUNT }
|
32
|
-
rule(/date/) { |t| :DATE }
|
33
31
|
rule(/exists/) { |t| :EXISTS }
|
34
32
|
rule(/if/) { |t| :IF }
|
35
|
-
rule(/max/) { |t| :MAX }
|
36
|
-
rule(/min/) { |t| :MIN }
|
37
|
-
rule(/sum/) { |t| :SUM }
|
38
33
|
rule(/true/) { |t| :TRUE }
|
39
34
|
rule(/false/) { |t| :FALSE }
|
40
35
|
rule(/null/) { |t| :NULL }
|
@@ -42,6 +37,6 @@ class Kalculator
|
|
42
37
|
rule(/FALSE/) { |t| :FALSE }
|
43
38
|
rule(/NULL/) { |t| :NULL }
|
44
39
|
rule(/"[^"]*"/) { |t| [:STRING, t[1..-2]] }
|
45
|
-
rule(/[A-Za-z][A-Za-z0-
|
40
|
+
rule(/[A-Za-z][A-Za-z0-9_]*/) { |t| [:IDENT, t] }
|
46
41
|
end
|
47
42
|
end
|
data/lib/kalculator/parser.rb
CHANGED
@@ -1,59 +1,62 @@
|
|
1
|
+
|
1
2
|
class Kalculator
|
2
3
|
class Parser < RLTK::Parser
|
4
|
+
#code taken from spiff-rb
|
5
|
+
class Environment < Environment
|
6
|
+
def metadata(first_token = nil, last_token = nil)
|
7
|
+
first_token ||= @positions.first
|
8
|
+
last_token ||= @positions.last
|
9
|
+
begins_at = first_token.stream_offset
|
10
|
+
ends_at = last_token.stream_offset + last_token.length - 1
|
11
|
+
{
|
12
|
+
offset: begins_at..ends_at,
|
13
|
+
}
|
14
|
+
end
|
15
|
+
end
|
3
16
|
left :AND, :OR
|
4
|
-
left :GT, :GTE, :LT, :LTE, :EQ
|
17
|
+
left :GT, :GTE, :LT, :LTE, :EQ, :NEQ
|
5
18
|
left :PLUS, :SUB
|
6
19
|
left :MUL, :DIV
|
20
|
+
left :PERIOD
|
7
21
|
#Left 600 '.'.
|
8
22
|
|
9
23
|
production(:expression) do
|
10
|
-
clause('CONTAINS LPAREN expression COMMA expression RPAREN') do |_, _, collection, _, item, _|
|
11
|
-
[:contains, collection, item]
|
12
|
-
end
|
13
24
|
clause('IF LPAREN expression COMMA expression COMMA expression RPAREN') do |_, _, condition, _, true_clause, _, false_clause, _|
|
14
|
-
[:if, condition, true_clause, false_clause]
|
15
|
-
end
|
16
|
-
clause('SUM LPAREN expression RPAREN') do |_, _, e0, _|
|
17
|
-
[:sum, e0]
|
18
|
-
end
|
19
|
-
clause('COUNT LPAREN expression RPAREN') do |_, _, e0, _|
|
20
|
-
[:count, e0]
|
21
|
-
end
|
22
|
-
clause('DATE LPAREN expression RPAREN') do |_, _, e0, _|
|
23
|
-
[:date, e0]
|
25
|
+
[:if, condition, true_clause, false_clause, metadata]
|
24
26
|
end
|
25
27
|
clause('EXISTS LPAREN IDENT RPAREN') do |_, _, n, _|
|
26
|
-
[:exists,
|
28
|
+
[:exists, n, metadata]
|
27
29
|
end
|
28
|
-
clause('MAX LPAREN expression COMMA expression RPAREN') { |_, _, left, _, right, _| [:max, left, right] }
|
29
|
-
clause('MIN LPAREN expression COMMA expression RPAREN') { |_, _, left, _, right, _| [:min, left, right] }
|
30
30
|
clause('LPAREN expression RPAREN') { |_, expression, _| expression }
|
31
|
-
clause('LBRACKET expressions RBRACKET') { |_, expressions, _| [:list, expressions] }
|
31
|
+
clause('LBRACKET expressions RBRACKET') { |_, expressions, _| [:list, expressions, metadata] }
|
32
|
+
clause('IDENT LPAREN expressions RPAREN') { |fn_name, _, expressions, _| [:fn_call, fn_name, expressions, metadata] }
|
33
|
+
clause('expression LBRACKET STRING RBRACKET') { |object, _,ident, _| [:access, ident, object, metadata]}
|
32
34
|
|
33
|
-
clause('NUMBER') { |n| [:number, n] }
|
34
|
-
clause('PERCENT') { |n| [:percent, n] }
|
35
|
-
clause('STRING') { |s| [:string, s] }
|
36
|
-
clause('IDENT') { |n| [:variable, n] }
|
37
|
-
clause('TRUE') { |n| [:boolean, true] }
|
38
|
-
clause('FALSE') { |n| [:boolean, false] }
|
39
|
-
clause('NULL') { |n| [:null, nil] }
|
35
|
+
clause('NUMBER') { |n| [:number, n, Number, metadata] }
|
36
|
+
clause('PERCENT') { |n| [:percent, n, Percent, metadata] }
|
37
|
+
clause('STRING') { |s| [:string, s, String.new, metadata] }
|
38
|
+
clause('IDENT') { |n| [:variable, n, metadata] }
|
39
|
+
clause('TRUE') { |n| [:boolean, true, Bool, metadata] }
|
40
|
+
clause('FALSE') { |n| [:boolean, false, Bool, metadata] }
|
41
|
+
clause('NULL') { |n| [:null, nil, Object, metadata] }
|
40
42
|
|
41
|
-
clause('expression
|
42
|
-
clause('expression
|
43
|
-
clause('expression
|
44
|
-
clause('expression
|
45
|
-
clause('expression
|
46
|
-
clause('expression
|
43
|
+
clause('expression PERIOD IDENT') { |e0, _, i| [:access,i, e0, metadata] }
|
44
|
+
clause('expression GT expression') { |e0, _, e1| [:>, e0, e1, metadata] }
|
45
|
+
clause('expression GTE expression') { |e0, _, e1| [:>=, e0, e1, metadata] }
|
46
|
+
clause('expression LT expression') { |e0, _, e1| [:<, e0, e1, metadata] }
|
47
|
+
clause('expression LTE expression') { |e0, _, e1| [:<=, e0, e1, metadata] }
|
48
|
+
clause('expression EQ expression') { |e0, _, e1| [:==, e0, e1, metadata] }
|
49
|
+
clause('expression NEQ expression') { |e0, _, e1| [:!=, e0, e1, metadata] }
|
47
50
|
|
48
|
-
clause('expression AND expression') { |e0, _, e1| [:and, e0, e1] }
|
49
|
-
clause('expression OR expression') { |e0, _, e1| [:or, e0, e1] }
|
51
|
+
clause('expression AND expression') { |e0, _, e1| [:and, e0, e1, metadata] }
|
52
|
+
clause('expression OR expression') { |e0, _, e1| [:or, e0, e1, metadata] }
|
50
53
|
|
51
|
-
clause('expression PLUS expression') { |e0, _, e1| [:+, e0, e1] }
|
52
|
-
clause('expression SUB expression') { |e0, _, e1| [:-, e0, e1] }
|
53
|
-
clause('expression MUL expression') { |e0, _, e1| [:*, e0, e1] }
|
54
|
-
clause('expression DIV expression') { |e0, _, e1| [:/, e0, e1] }
|
54
|
+
clause('expression PLUS expression') { |e0, _, e1| [:+, e0, e1, metadata] }
|
55
|
+
clause('expression SUB expression') { |e0, _, e1| [:-, e0, e1, metadata] }
|
56
|
+
clause('expression MUL expression') { |e0, _, e1| [:*, e0, e1, metadata] }
|
57
|
+
clause('expression DIV expression') { |e0, _, e1| [:/, e0, e1, metadata] }
|
55
58
|
|
56
|
-
clause('BANG expression') { |_, e0| [:not, e0] }
|
59
|
+
clause('BANG expression') { |_, e0| [:not, e0, metadata] }
|
57
60
|
end
|
58
61
|
|
59
62
|
production(:expressions) do
|
@@ -62,5 +65,9 @@ class Kalculator
|
|
62
65
|
end
|
63
66
|
|
64
67
|
finalize()
|
68
|
+
|
69
|
+
def metadata(num_tokens)
|
70
|
+
{offset: pos(0).stream_offset..pos(num_tokens-1).stream_offset}
|
71
|
+
end
|
65
72
|
end
|
66
73
|
end
|
data/lib/kalculator/transform.rb
CHANGED
@@ -2,13 +2,18 @@ class Kalculator
|
|
2
2
|
module Transform
|
3
3
|
def self.run(node, &block)
|
4
4
|
new_node = yield node
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
if is_terminal?(new_node)
|
6
|
+
new_node
|
7
|
+
elsif new_node.first.is_a?(Symbol)
|
8
|
+
args = new_node[1..-1].map{ |arg| run(arg, &block) }
|
9
|
+
args.unshift(new_node.first)
|
10
|
+
else
|
11
|
+
new_node.map{|node| run(node, &block) }
|
12
|
+
end
|
8
13
|
end
|
9
14
|
|
10
15
|
def self.is_terminal?(node)
|
11
|
-
return false if node.is_a?(Array)
|
16
|
+
return false if node.is_a?(Array)
|
12
17
|
true
|
13
18
|
end
|
14
19
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
##
|
2
|
+
#Author: Benjamin Walter Newhall, 12/17/19, github: bennewhall
|
3
|
+
#Explanation: much like the data sources class, this stores type data in the form {"variable-name"=>String}
|
4
|
+
##
|
5
|
+
|
6
|
+
class Kalculator
|
7
|
+
class TypeSources
|
8
|
+
|
9
|
+
def initialize(*sources)# sources is an array of Hashes
|
10
|
+
@sources = sources
|
11
|
+
end
|
12
|
+
|
13
|
+
def key?(name)
|
14
|
+
ret = false
|
15
|
+
@sources.each do |source|
|
16
|
+
break ret = true if source.key?(name)
|
17
|
+
end
|
18
|
+
ret
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](name)
|
22
|
+
ret = nil
|
23
|
+
@sources.each do |source|
|
24
|
+
break ret = source[name] if source.key?(name)
|
25
|
+
end
|
26
|
+
ret
|
27
|
+
end
|
28
|
+
|
29
|
+
#returns a hash of variable name to type of variable
|
30
|
+
def toHash
|
31
|
+
a = Hash.new
|
32
|
+
@sources.each { |i| a = a.merge(i)}
|
33
|
+
return a
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
##
|
2
|
+
#Author: Benjamin Walter Newhall 12/17/19 github: bennewhall
|
3
|
+
#Explanation: A class that holds the types used for the validator
|
4
|
+
##
|
5
|
+
|
6
|
+
|
7
|
+
class Kalculator
|
8
|
+
class Comparable; end #A Type that can be used in <, >, etc. operations
|
9
|
+
class Number< Comparable; end #A Type that can be used in +,-,etc operations
|
10
|
+
class Percent < Number; end #a type of number
|
11
|
+
class Bool; end
|
12
|
+
class Time; end
|
13
|
+
|
14
|
+
#a generic collection type of type Collection<othertype> where othertype is stored in the type instance variable
|
15
|
+
class Collection
|
16
|
+
attr_reader :type
|
17
|
+
def initialize(type)
|
18
|
+
@type = type
|
19
|
+
end
|
20
|
+
def generic_type?(possible_type) #possibleType has to either be a collection or a type
|
21
|
+
if(possible_type.class <= Kalculator::Collection)
|
22
|
+
return possible_type.type <= @type
|
23
|
+
end
|
24
|
+
if(possible_type.class == Class)
|
25
|
+
return possible_type <= @type
|
26
|
+
end
|
27
|
+
return false
|
28
|
+
end
|
29
|
+
|
30
|
+
def ==(other_type) #othertype has to be a collection or a type
|
31
|
+
if(other_type.class <= self.class)
|
32
|
+
return other_type.type == self.type
|
33
|
+
end
|
34
|
+
return false
|
35
|
+
end
|
36
|
+
|
37
|
+
def <=(other_object) # otherobject has to be a collection, a type, or some other object
|
38
|
+
if(other_object.class <= Kalculator::Collection)
|
39
|
+
return ((self.class<= other_object.class) and (self.type <= other_object.type))
|
40
|
+
elsif(other_object.class == Class)
|
41
|
+
if(other_object == Object)
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
return false
|
45
|
+
elsif(other_object.class != Class)
|
46
|
+
return false
|
47
|
+
end
|
48
|
+
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
def >=(other_object) # otherobject has to be a collection, a type, or some other object
|
53
|
+
if(other_object.class <=Kalculator::Collection)
|
54
|
+
return other_object<= self
|
55
|
+
elsif(other_object.class == Class)
|
56
|
+
return false
|
57
|
+
elsif(other_object.class != Class)
|
58
|
+
return false
|
59
|
+
end
|
60
|
+
|
61
|
+
return false
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
|
67
|
+
class String < Collection #a collection that holds strings
|
68
|
+
def initialize()
|
69
|
+
super(String)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class List < Collection #a collection that holds one type of data
|
74
|
+
def initialize(type)
|
75
|
+
super(type)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
class MappedObject < Collection #a collection that represents an object and holds any type of data
|
80
|
+
attr_reader :hash
|
81
|
+
def initialize(hash)
|
82
|
+
super(Object)
|
83
|
+
@hash = hash
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,240 @@
|
|
1
|
+
##
|
2
|
+
#Author: Benjamin Walter Newhall 12/17/19, github: bennewhall
|
3
|
+
#Explanation: A class that will either return the ending type of an ast, or throw an error with metadata information about error position
|
4
|
+
#Usage: v = Validator.new(typesource) (typesource is a TypeSources object)
|
5
|
+
# v.validate(ast) (ast is the ast returned by parsing a line)(this will either return a type or throw an Error as described in the errors.rb file)
|
6
|
+
##
|
7
|
+
class Kalculator
|
8
|
+
class Validator
|
9
|
+
def initialize(type_source)
|
10
|
+
@type_source = type_source
|
11
|
+
#e stores built in function type data
|
12
|
+
#last index is ALWAYS the return type, and types before that are types of children from left to right
|
13
|
+
e = { "contains" => [Collection.new(Object), Object, Bool],
|
14
|
+
"count" => [List.new(Object), Number],
|
15
|
+
"date" => [String.new, Time],
|
16
|
+
"max"=> [Number,Number,Number],
|
17
|
+
"min" => [Number,Number,Number],
|
18
|
+
"sum" => [List.new(Number), Number] #this only accepts number Lists
|
19
|
+
}
|
20
|
+
@environment = e.merge(type_source.toHash)
|
21
|
+
end
|
22
|
+
|
23
|
+
def validate(ast)
|
24
|
+
send(ast.first, *ast)
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
def access(_,identifier,object, metadata)
|
29
|
+
objectType = validate(object)
|
30
|
+
if((objectType.is_a?(MappedObject)))
|
31
|
+
if(objectType.hash.key?(identifier))
|
32
|
+
attribute =objectType.hash[identifier]
|
33
|
+
if(attribute.is_a?(Pointer))
|
34
|
+
attribute = @environment[attribute.p] #if the attribute is a pointer find an add
|
35
|
+
end
|
36
|
+
if(attribute.is_a?(Kalculator::AnonymousPointer))
|
37
|
+
attribute = attribute.p
|
38
|
+
end
|
39
|
+
return attribute
|
40
|
+
end
|
41
|
+
raise UndefinedVariableError.new(metadata), "object #{objectType} doesn't have type attribute #{identifier}"
|
42
|
+
end
|
43
|
+
raise TypeError.new(metadata), "trying to access something that isn't an object"
|
44
|
+
end
|
45
|
+
|
46
|
+
def +(_, left, right, metadata)
|
47
|
+
leftType = validate(left)
|
48
|
+
if((leftType==validate(right)) and leftType <=Number)
|
49
|
+
return leftType
|
50
|
+
end
|
51
|
+
raise TypeError.new(metadata), "not operating on two of the same Number types"
|
52
|
+
end
|
53
|
+
|
54
|
+
def -(_, left, right, metadata)
|
55
|
+
leftType = validate(left)
|
56
|
+
if((leftType==validate(right)) and leftType <=Number)
|
57
|
+
return leftType
|
58
|
+
end
|
59
|
+
raise TypeError.new(metadata), "not operating on two of the same Number types"
|
60
|
+
end
|
61
|
+
|
62
|
+
def *(_, left, right, metadata)
|
63
|
+
leftType = validate(left)
|
64
|
+
if((leftType==validate(right)) and leftType <=Number)
|
65
|
+
return leftType
|
66
|
+
end
|
67
|
+
raise TypeError.new(metadata), "not operating on two of the same Number types"
|
68
|
+
end
|
69
|
+
|
70
|
+
def /(_, left, right, metadata)
|
71
|
+
leftType = validate(left)
|
72
|
+
if((leftType==validate(right)) and leftType <=Number)
|
73
|
+
return leftType
|
74
|
+
end
|
75
|
+
raise TypeError.new(metadata), "not operating on two of the same Number types"
|
76
|
+
end
|
77
|
+
|
78
|
+
def >(_, left, right, metadata)
|
79
|
+
leftType = validate(left)
|
80
|
+
if((leftType==validate(right)) and leftType<= Comparable)
|
81
|
+
return Bool
|
82
|
+
end
|
83
|
+
raise TypeError.new(metadata), "not comparing two of the same comparable types"
|
84
|
+
end
|
85
|
+
|
86
|
+
def >=(_, left, right, metadata)
|
87
|
+
leftType = validate(left)
|
88
|
+
if((leftType==validate(right)) and leftType<= Comparable)
|
89
|
+
return Bool
|
90
|
+
end
|
91
|
+
raise TypeError.new(metadata), "not comparing two of the same comparable types"
|
92
|
+
end
|
93
|
+
|
94
|
+
def <(_, left, right, metadata)
|
95
|
+
leftType = validate(left)
|
96
|
+
if((leftType==validate(right)) and leftType<= Comparable)
|
97
|
+
return Bool
|
98
|
+
end
|
99
|
+
raise TypeError.new(metadata), "not comparing two of the same types"
|
100
|
+
end
|
101
|
+
|
102
|
+
def <=(_, left, right, metadata)
|
103
|
+
leftType = validate(left)
|
104
|
+
if((leftType==validate(right)) and leftType<= Comparable)
|
105
|
+
return Bool
|
106
|
+
end
|
107
|
+
raise TypeError.new(metadata), "not comparing two of the same comparable types"
|
108
|
+
end
|
109
|
+
|
110
|
+
def ==(_, left, right, metadata)
|
111
|
+
leftType = validate(left)
|
112
|
+
if(leftType==validate(right))
|
113
|
+
return Bool
|
114
|
+
end
|
115
|
+
raise TypeError.new(metadata), "not comparing two of the same comparable types"
|
116
|
+
end
|
117
|
+
|
118
|
+
def !=(_, left, right, metadata)
|
119
|
+
leftType = validate(left)
|
120
|
+
if(leftType==validate(right))
|
121
|
+
return Bool
|
122
|
+
end
|
123
|
+
raise TypeError.new(metadata), "not comparing two of the same comparable types"
|
124
|
+
end
|
125
|
+
|
126
|
+
def and(_, left, right, metadata)
|
127
|
+
if(validate(left)<=Bool and validate(right)<=Bool)
|
128
|
+
return Bool
|
129
|
+
end
|
130
|
+
raise TypeError.new(metadata), "not comparing (AND) two BOOL types"
|
131
|
+
end
|
132
|
+
|
133
|
+
def or(_, left, right, metadata)
|
134
|
+
if(validate(left)<=Bool and validate(right)<=Bool)
|
135
|
+
return Bool
|
136
|
+
end
|
137
|
+
raise TypeError.new(metadata), "not comparing (OR) two BOOL types"
|
138
|
+
end
|
139
|
+
|
140
|
+
def boolean(_, boolean, type, metadata)
|
141
|
+
return Bool
|
142
|
+
end
|
143
|
+
|
144
|
+
def exists(_, variable, metadata)
|
145
|
+
return Bool
|
146
|
+
end
|
147
|
+
|
148
|
+
def fn_call(_, fn_name, expressions, metadata) #compare individually to make sure it is a subclass or class
|
149
|
+
ex =expressions.map{|expression| validate(expression) }
|
150
|
+
raise UndefinedVariableError.new(metadata), "undefined variable #{fn_name}" unless @environment.key?(fn_name)
|
151
|
+
argumentMatch = ex.zip(@environment[fn_name]).all? do |(arg_type, expected_type)|
|
152
|
+
begin
|
153
|
+
arg_type <= expected_type
|
154
|
+
rescue
|
155
|
+
begin
|
156
|
+
expected_type >= arg_type
|
157
|
+
rescue
|
158
|
+
false
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
if(argumentMatch) # make sure each element is related to corresponding element in the funcion params
|
163
|
+
if(fn_name == "max" or fn_name == "min") #add functions here where output type depends on input type and all types must be exact same ie. max(Percent,Percent) => Percent max(Number,Percent)=> Error max(Number,Number)=>Number
|
164
|
+
#check all expressions are same
|
165
|
+
cmptype = ex[0]
|
166
|
+
if( ex.all?{|t| t == cmptype})
|
167
|
+
return cmptype
|
168
|
+
end
|
169
|
+
raise TypeError.new(metadata), "specialized function type error"
|
170
|
+
end
|
171
|
+
#add other specialized functions here
|
172
|
+
if(fn_name == "contains") #generic function in the format List<E>, E => ReturnType
|
173
|
+
if(ex[0].generic_type?(ex[1]))
|
174
|
+
return @environment[fn_name][@environment[fn_name].size - 1]
|
175
|
+
end
|
176
|
+
raise TypeError.new(metadata), "generic function type error"
|
177
|
+
end
|
178
|
+
return @environment[fn_name][@environment[fn_name].size - 1]
|
179
|
+
end
|
180
|
+
raise TypeError.new(metadata), "function type error"
|
181
|
+
end
|
182
|
+
|
183
|
+
def if(_, condition, true_clause, false_clause, metadata)
|
184
|
+
conditionType = validate(condition)
|
185
|
+
if(conditionType <= Object and validate(true_clause)==validate(false_clause))
|
186
|
+
return validate(true_clause)
|
187
|
+
end
|
188
|
+
raise TypeError.new(metadata), "if statement type error"
|
189
|
+
end
|
190
|
+
|
191
|
+
def list(_, expressions, metadata)
|
192
|
+
ex =expressions.map{|expression| validate(expression) }
|
193
|
+
|
194
|
+
cmptype = ex[0]
|
195
|
+
if( ex.all?{|t| t == cmptype})
|
196
|
+
return List.new(cmptype)
|
197
|
+
end
|
198
|
+
raise TypeError.new(metadata), "list type error"
|
199
|
+
|
200
|
+
end
|
201
|
+
|
202
|
+
def not(_, expression, metadata)
|
203
|
+
if(validate(expression) ==Bool)
|
204
|
+
return Bool
|
205
|
+
|
206
|
+
end
|
207
|
+
raise TypeError.new(metadata), "NOT expression type error"
|
208
|
+
end
|
209
|
+
|
210
|
+
def null(_, _, type, metadata)
|
211
|
+
return type
|
212
|
+
end
|
213
|
+
|
214
|
+
def number(_, number, type, metadata)
|
215
|
+
return type
|
216
|
+
end
|
217
|
+
|
218
|
+
def percent(_, percent, type, metadata)
|
219
|
+
return type
|
220
|
+
end
|
221
|
+
|
222
|
+
def string(_, string, type, metadata)
|
223
|
+
return type
|
224
|
+
end
|
225
|
+
|
226
|
+
def variable(_, name, metadata)
|
227
|
+
|
228
|
+
raise UndefinedVariableError.new(metadata), "undefined variable #{name}" unless @environment.key?(name)
|
229
|
+
variable_type = @environment[name]
|
230
|
+
if(variable_type.is_a?(Pointer))
|
231
|
+
variable_type = @environment[variable_type.p] #if the variable you are accessing is a pointer, then return the type of the variable it is pointing to
|
232
|
+
end
|
233
|
+
if(variable_type.is_a?(Kalculator::AnonymousPointer))
|
234
|
+
variable_type = variable_type.p
|
235
|
+
end
|
236
|
+
return variable_type
|
237
|
+
end
|
238
|
+
|
239
|
+
end
|
240
|
+
end
|
data/lib/kalculator/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kalculator
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Ries
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-12-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rltk
|
@@ -89,14 +89,18 @@ extra_rdoc_files: []
|
|
89
89
|
files:
|
90
90
|
- kalculator.gemspec
|
91
91
|
- lib/kalculator.rb
|
92
|
+
- lib/kalculator/built_in_functions.rb
|
92
93
|
- lib/kalculator/data_sources.rb
|
93
94
|
- lib/kalculator/errors.rb
|
94
95
|
- lib/kalculator/evaluator.rb
|
95
96
|
- lib/kalculator/formula.rb
|
96
97
|
- lib/kalculator/lexer.rb
|
97
|
-
- lib/kalculator/nested_lookup.rb
|
98
98
|
- lib/kalculator/parser.rb
|
99
|
+
- lib/kalculator/pointer.rb
|
99
100
|
- lib/kalculator/transform.rb
|
101
|
+
- lib/kalculator/type_sources.rb
|
102
|
+
- lib/kalculator/types.rb
|
103
|
+
- lib/kalculator/validator.rb
|
100
104
|
- lib/kalculator/version.rb
|
101
105
|
homepage: https://github.com/mmmries/kalculator
|
102
106
|
licenses:
|
@@ -118,7 +122,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
118
122
|
version: '0'
|
119
123
|
requirements: []
|
120
124
|
rubyforge_project:
|
121
|
-
rubygems_version: 2.7.
|
125
|
+
rubygems_version: 2.7.9
|
122
126
|
signing_key:
|
123
127
|
specification_version: 4
|
124
128
|
summary: A calculator that can safely and quickly interpret user-input
|
@@ -1,25 +0,0 @@
|
|
1
|
-
class Kalculator
|
2
|
-
class NestedLookup
|
3
|
-
def initialize(data_source)
|
4
|
-
@data_source = data_source
|
5
|
-
end
|
6
|
-
|
7
|
-
def key?(name)
|
8
|
-
names = name.split(".")
|
9
|
-
source = @data_source
|
10
|
-
names.all? do |name|
|
11
|
-
break false unless source.key?(name)
|
12
|
-
source = source[name]
|
13
|
-
true
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
|
-
def [](name)
|
18
|
-
names = name.split(".")
|
19
|
-
names.inject(@data_source) do |source, name|
|
20
|
-
break nil unless source.key?(name)
|
21
|
-
source[name]
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|