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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5b8d9f63d9aa766d2a26f0b17d63f2ed90dccb7ea1cc98013f2e5703445a2fc9
4
- data.tar.gz: 54bdd33465e18514a700e1f2a49d24b399ff19556a375c68fda95d868f05145a
3
+ metadata.gz: 96f3aedf0934ea881aac16720904825d1861878d7e00d55b8ce81f3f01b4ffdd
4
+ data.tar.gz: c5ceb6b7b8b9b232b31a81c08f03499c3fdd46b7968fb99f48f240929dfcbb40
5
5
  SHA512:
6
- metadata.gz: 2162cfd1c00584151fc1148dc8d0dc3d2e779bf634a623ea9d816270b7d5ceff241421307905b5eb5b72bf7bc979251c1be7232867d2499ea195fc549f466971
7
- data.tar.gz: cf4a5db28cb6280c2b7761c6a6ace135103341f987a63b7b6d4bc1ef3476213346e4992590c686f88938b2c4da40bebcb71720247c7c0220e615099741342fec
6
+ metadata.gz: eeb67bb90056d4d8ffc9d3b9d0f07dc0a1441c9da6a3285dca9c6102653e2fb95b0057223ab88d0171dd2030ea3f9cbe43e2986b6ebf8e7aabf7c6acadb645fd
7
+ data.tar.gz: 581292d72c21d1d8d593a50863ccab63c776570e725bf1444c9b1fa8db63a4e4448a74cff0201932d4191b6e42284ff06b2761fc9e9397646119bf453df27f54
@@ -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
@@ -1,5 +1,11 @@
1
1
  class Kalculator
2
- class Error < ::StandardError; end
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
@@ -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 +(_, left, right)
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 contains(_, collection, item)
66
- collection = evaluate(collection)
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 date(_, expression)
84
- value = evaluate(expression)
85
- raise TypeError, "date only works with Strings, got #{value.inspect}" unless value.is_a?(String)
86
- Date.parse(value)
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 exists(_, variable)
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 max(_, left, right)
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 sum(_, array)
145
- array = evaluate(array)
146
- unless array.is_a?(Array) && array.all?{|n| n.is_a?(Numeric)}
147
- raise TypeError, "sum only works with lists of numbers, got #{array.inspect}"
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
- array.inject(0){|sum, num| sum + num}
150
- end
151
-
152
- def variable(_, name)
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
@@ -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 evaluate(data_source = {})
11
- Kalculator::Evaluator.new(data_source).evaluate(ast)
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
@@ -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-9\._]*/) { |t| [:IDENT, t] }
40
+ rule(/[A-Za-z][A-Za-z0-9_]*/) { |t| [:IDENT, t] }
46
41
  end
47
42
  end
@@ -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, [:variable, n]]
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 GT expression') { |e0, _, e1| [:>, e0, e1] }
42
- clause('expression GTE expression') { |e0, _, e1| [:>=, e0, e1] }
43
- clause('expression LT expression') { |e0, _, e1| [:<, e0, e1] }
44
- clause('expression LTE expression') { |e0, _, e1| [:<=, e0, e1] }
45
- clause('expression EQ expression') { |e0, _, e1| [:==, e0, e1] }
46
- clause('expression NEQ expression') { |e0, _, e1| [:!=, e0, e1] }
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
@@ -0,0 +1,16 @@
1
+ class Kalculator
2
+ class Pointer
3
+ attr_reader :p
4
+ def initialize( p)
5
+ @p = p
6
+ end
7
+ end
8
+
9
+ class AnonymousPointer
10
+ attr_reader :p
11
+
12
+ def initialize(p)
13
+ @p = p
14
+ end
15
+ end
16
+ end
@@ -2,13 +2,18 @@ class Kalculator
2
2
  module Transform
3
3
  def self.run(node, &block)
4
4
  new_node = yield node
5
- return new_node if is_terminal?(new_node)
6
- args = new_node[1..-1].map{ |arg| run(arg, &block) }
7
- args.unshift(new_node.first)
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) && node.first.is_a?(Symbol)
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
@@ -1,3 +1,3 @@
1
1
  class Kalculator
2
- VERSION = "0.6.4"
2
+ VERSION = "1.0.0"
3
3
  end
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.6.4
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: 2018-11-16 00:00:00.000000000 Z
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.6
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