kalculator 0.6.4 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|