sparkql 1.2.2 → 1.2.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- OGNlZDVlNWY0ZDgwNDY0YTY1ZWI0ZGY5MzQ1ZmY0NTIxMGY4ZTI2OQ==
5
- data.tar.gz: !binary |-
6
- ZjhiZmI1Mjc4ZDM0YmE5M2Q4YzU5NTMwMmRhODY3MDY5YWE3ODQ5YQ==
2
+ SHA256:
3
+ metadata.gz: 216e474f5077d8b33ede2b8bc61118bed6eab33d530eae71a642306d53257e6a
4
+ data.tar.gz: da6486cedc2ed93ac8db963572afbeccead88643535430442e59d8b941dd0fe6
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- Yzk5NzlkOTMxY2VkZTUyN2JmMmFkYWU0NmM4MWY4ODUyNzQ0NzZkYzM5ZTU5
10
- YTg5NjJhNmFlMDVlNzkxNWM2MDgyYTQwZjI5ZTc1OGZjODI1MzIxMTE2ZGQ5
11
- ZjBmYmQ2ZjdiNzNlODIwOWUzMjI5ODEzMThiMTcyZTJkYWUwOTk=
12
- data.tar.gz: !binary |-
13
- YmUyZjU5ZWI3NWNhOGVhYmU5MDQxZDNhZjhhZWEwNzJmMzNmZjkzNTUxODQ5
14
- YzIwODQwMjgzMThlODc4ZWJiYjdkMzM0NDU4ZjBiZjlhMGMwODQ3YzdkNDRh
15
- MTRiYzZkMmVmMTM0NGNhZWY1ZWJkZDcwYjEzZDIyMDU0ZjY4YmM=
6
+ metadata.gz: 87f6fa0829bb7d99e49bb1675f4d8090ce9271d0dea9ae8da15522b57f86f03f87df21f1187cd4cb6f5812f880da74f690dfae12f99fa52d41957fa09062d82f
7
+ data.tar.gz: 8575083d2bf0a94bb299109617ab2f41641b01636427883ace5733b5500859e3419a6d977f30a79c15602c7b9fe1ec7315c7d4508cab4f1829f19a365c7547f5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ v1.2.7, 2021-05-06
2
+ -------------------
3
+ * [IMPROVEMENT] dayofweek(), dayofyear(), and weekdays() functions
4
+
5
+ v1.2.6, 2019-04-01
6
+ -------------------
7
+ * [IMPROVEMENT] hours(), minutes(), and seconds() functions
8
+
9
+ v1.2.5, 2018-12-19
10
+ -------------------
11
+ * [BUGFIX] Correctly handle arithmetic grouping
12
+
13
+ v1.2.4, 2018-12-13
14
+ -------------------
15
+ * [IMPROVEMENT] Support decimal arithmetic
16
+ * [BUGFIX] Correctly handle type checking with arithmetic
17
+
18
+ v1.2.3, 2018-12-05
19
+ -------------------
20
+ * [IMPROVEMENT] Support Arithmetic Grouping and Negation
21
+
1
22
  v1.2.2, 2018-11-28
2
23
  -------------------
3
24
  * [IMPROVEMENT] Support Arithmetic: Add, Sub, Mul, Div, Mod
data/GRAMMAR.md CHANGED
@@ -46,8 +46,7 @@ and criteria for comparing the value of the field to the value(s) of the
46
46
  condition. The result of evaluating the expression on a resource is a true of
47
47
  false for matching the criteria. We are separating functions and arithmetic
48
48
  based on if we are acting on the field side or the literal side. This is to
49
- allow literal folding on the literal side and to prevent unnecessary checks
50
- to see if a field is in the expression.
49
+ allow literal folding on the literal side.
51
50
 
52
51
 
53
52
  ```
@@ -96,6 +95,8 @@ One or more expressions encased in parenthesis. There are limitations on nesting
96
95
  | field_arithmetic_expression MUL field_arithmetic_expression
97
96
  | field_arithmetic_expression DIV field_arithmetic_expression
98
97
  | field_arithmetic_expression MOD field_arithmetic_expression
98
+ | LPAREN field_arithmetic_expression RPAREN
99
+ | UMINUS field_arithmetic_expression
99
100
  | literals
100
101
  | field_function_expression
101
102
  ;
@@ -117,6 +118,8 @@ on filtering values
117
118
  : arithmetic_condition
118
119
  | literal_list
119
120
  | literal
121
+ | LPAREN condition RPAREN
122
+ | UMINUS condition
120
123
  ;
121
124
  arithmetic_condition
122
125
  : condition ADD condition
@@ -203,8 +206,6 @@ Literals that support multiple values in a list for a condition
203
206
  : INTEGER
204
207
  | DECIMAL
205
208
  | CHARACTER
206
- | LPAREN literals RPAREN
207
- | UMINUS literals
208
209
  ;
209
210
  ```
210
211
 
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
- source "http://rubygems.org"
1
+ source 'http://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in sparkapi_parser.gemspec
4
3
  gemspec
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.2
1
+ 1.2.7
@@ -1,763 +1,855 @@
1
- require 'time'
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
2
4
  require 'geo_ruby'
3
5
  require 'geo_ruby/ewk'
4
6
  require 'sparkql/geo'
5
7
 
6
- # Binding class to all supported function calls in the parser. Current support requires that the
7
- # resolution of function calls to happen on the fly at parsing time at which point a value and
8
- # value type is required, just as literals would be returned to the expression tokenization level.
9
- #
10
- # Name and argument requirements for the function should match the function declaration in
11
- # SUPPORTED_FUNCTIONS which will run validation on the function syntax prior to execution.
12
- class Sparkql::FunctionResolver
13
- SECONDS_IN_DAY = 60 * 60 * 24
14
- STRFTIME_DATE_FORMAT = '%Y-%m-%d'
15
- STRFTIME_TIME_FORMAT = '%H:%M:%S.%N'
16
- VALID_REGEX_FLAGS = ["", "i"]
17
- MIN_DATE_TIME = Time.new(1970, 1, 1, 0, 0, 0, "+00:00").iso8601
18
- MAX_DATE_TIME = Time.new(9999, 12, 31, 23, 59, 59, "+00:00").iso8601
19
- VALID_CAST_TYPES = [:field, :character, :decimal, :integer]
20
-
21
- SUPPORTED_FUNCTIONS = {
22
- :polygon => {
23
- :args => [:character],
24
- :return_type => :shape
25
- },
26
- :rectangle => {
27
- :args => [:character],
28
- :return_type => :shape
29
- },
30
- :radius => {
31
- :args => [:character, [:decimal, :integer]],
32
- :return_type => :shape
33
- },
34
- :regex => {
35
- :args => [:character],
36
- :opt_args => [{
37
- :type => :character,
38
- :default => ''
39
- }],
40
- :return_type => :character
41
- },
42
- :substring => {
43
- :args => [[:field, :character], :integer],
44
- :opt_args => [{
45
- :type => :integer
46
- }],
47
- :resolve_for_type => true,
48
- :return_type => :character
49
- },
50
- :trim => {
51
- :args => [[:field, :character]],
52
- :resolve_for_type => true,
53
- :return_type => :character
54
- },
55
- :tolower => {
56
- :args => [[:field, :character]],
57
- :resolve_for_type => true,
58
- :return_type => :character
59
- },
60
- :toupper => {
61
- :args => [[:field, :character]],
62
- :resolve_for_type => true,
63
- :return_type => :character
64
- },
65
- :length => {
66
- :args => [[:field, :character]],
67
- :resolve_for_type => true,
68
- :return_type => :integer
69
- },
70
- :indexof => {
71
- :args => [[:field, :character], :character],
72
- :return_type => :integer
73
- },
74
- :concat => {
75
- :args => [[:field, :character], :character],
76
- :resolve_for_type => true,
77
- :return_type => :character
78
- },
79
- :cast => {
80
- :args => [[:field, :character, :decimal, :integer, :null], :character],
81
- :resolve_for_type => true,
82
- },
83
- :round => {
84
- :args => [[:field, :decimal]],
85
- :resolve_for_type => true,
86
- :return_type => :integer
87
- },
88
- :ceiling => {
89
- :args => [[:field, :decimal]],
90
- :resolve_for_type => true,
91
- :return_type => :integer
92
- },
93
- :floor => {
94
- :args => [[:field, :decimal]],
95
- :resolve_for_type => true,
96
- :return_type => :integer
97
- },
98
- :startswith => {
99
- :args => [:character],
100
- :return_type => :startswith
101
- },
102
- :endswith => {
103
- :args => [:character],
104
- :return_type => :endswith
105
- },
106
- :contains => {
107
- :args => [:character],
108
- :return_type => :contains
109
- },
110
- :linestring => {
111
- :args => [:character],
112
- :return_type => :shape
113
- },
114
- :days => {
115
- :args => [:integer],
116
- :return_type => :datetime
117
- },
118
- :months => {
119
- :args => [:integer],
120
- :return_type => :datetime
121
- },
122
- :years => {
123
- :args => [:integer],
124
- :return_type => :datetime
125
- },
126
- :now => {
127
- :args => [],
128
- :return_type => :datetime
129
- },
130
- :maxdatetime => {
131
- :args => [],
132
- :return_type => :datetime
133
- },
134
- :mindatetime => {
135
- :args => [],
136
- :return_type => :datetime
137
- },
138
- :date => {
139
- :args => [[:field,:datetime,:date]],
140
- :resolve_for_type => true,
141
- :return_type => :date
142
- },
143
- :time => {
144
- :args => [[:field,:datetime,:date]],
145
- :resolve_for_type => true,
146
- :return_type => :time
147
- },
148
- :year => {
149
- :args => [[:field,:datetime,:date]],
150
- :resolve_for_type => true,
151
- :return_type => :integer
152
- },
153
- :month => {
154
- :args => [[:field,:datetime,:date]],
155
- :resolve_for_type => true,
156
- :return_type => :integer
157
- },
158
- :day => {
159
- :args => [[:field,:datetime,:date]],
160
- :resolve_for_type => true,
161
- :return_type => :integer
162
- },
163
- :hour => {
164
- :args => [[:field,:datetime,:date]],
165
- :resolve_for_type => true,
166
- :return_type => :integer
167
- },
168
- :minute => {
169
- :args => [[:field,:datetime,:date]],
170
- :resolve_for_type => true,
171
- :return_type => :integer
172
- },
173
- :second => {
174
- :args => [[:field,:datetime,:date]],
175
- :resolve_for_type => true,
176
- :return_type => :integer
177
- },
178
- :fractionalseconds => {
179
- :args => [[:field,:datetime,:date]],
180
- :resolve_for_type => true,
181
- :return_type => :decimal
182
- },
183
- :range => {
184
- :args => [:character, :character],
185
- :return_type => :character
186
- },
187
- :wkt => {
188
- :args => [:character],
189
- :return_type => :shape
190
- }
191
- }
192
-
193
- # Construct a resolver instance for a function
194
- # name: function name (String)
195
- # args: array of literal hashes of the format {:type=><literal_type>, :value=><escaped_literal_value>}.
196
- # Empty arry for functions that have no arguments.
197
- def initialize(name, args)
198
- @name = name
199
- @args = args
200
- @errors = []
201
- end
202
-
203
- # Validate the function instance prior to calling it. All validation failures will show up in the
204
- # errors array.
205
- def validate()
206
- name = @name.to_sym
207
- unless support.has_key?(name)
208
- @errors << Sparkql::ParserError.new(:token => @name,
209
- :message => "Unsupported function call '#{@name}' for expression",
210
- :status => :fatal )
211
- return
212
- end
213
-
214
- required_args = support[name][:args]
215
- total_args = required_args + Array(support[name][:opt_args]).collect {|args| args[:type]}
216
-
217
- if @args.size < required_args.size || @args.size > total_args.size
218
- @errors << Sparkql::ParserError.new(:token => @name,
219
- :message => "Function call '#{@name}' requires #{required_args.size} arguments",
220
- :status => :fatal )
221
- return
222
- end
223
-
224
- count = 0
225
- @args.each do |arg|
226
- type = arg[:type] == :function ? arg[:return_type] : arg[:type]
227
- unless Array(total_args[count]).include?(type)
228
- @errors << Sparkql::ParserError.new(:token => @name,
229
- :message => "Function call '#{@name}' has an invalid argument at #{arg[:value]}",
230
- :status => :fatal )
231
- end
232
- count +=1
8
+ module Sparkql
9
+ # Binding class to all supported function calls in the parser. Current support requires that the
10
+ # resolution of function calls to happen on the fly at parsing time at which point a value and
11
+ # value type is required, just as literals would be returned to the expression tokenization level.
12
+ #
13
+ # Name and argument requirements for the function should match the function declaration in
14
+ # SUPPORTED_FUNCTIONS which will run validation on the function syntax prior to execution.
15
+ class FunctionResolver
16
+ SECONDS_IN_MINUTE = 60
17
+ SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60
18
+ SECONDS_IN_DAY = SECONDS_IN_HOUR * 24
19
+ STRFTIME_DATE_FORMAT = '%Y-%m-%d'
20
+ STRFTIME_TIME_FORMAT = '%H:%M:%S.%N'
21
+ VALID_REGEX_FLAGS = ['', 'i'].freeze
22
+ MIN_DATE_TIME = Time.new(1970, 1, 1, 0, 0, 0, '+00:00').iso8601
23
+ MAX_DATE_TIME = Time.new(9999, 12, 31, 23, 59, 59, '+00:00').iso8601
24
+ VALID_CAST_TYPES = %i[field character decimal integer].freeze
25
+
26
+ SUPPORTED_FUNCTIONS = {
27
+ polygon: {
28
+ args: [:character],
29
+ return_type: :shape
30
+ },
31
+ rectangle: {
32
+ args: [:character],
33
+ return_type: :shape
34
+ },
35
+ radius: {
36
+ args: [:character, %i[decimal integer]],
37
+ return_type: :shape
38
+ },
39
+ regex: {
40
+ args: [:character],
41
+ opt_args: [{
42
+ type: :character,
43
+ default: ''
44
+ }],
45
+ return_type: :character
46
+ },
47
+ substring: {
48
+ args: [%i[field character], :integer],
49
+ opt_args: [{
50
+ type: :integer
51
+ }],
52
+ resolve_for_type: true,
53
+ return_type: :character
54
+ },
55
+ trim: {
56
+ args: [%i[field character]],
57
+ resolve_for_type: true,
58
+ return_type: :character
59
+ },
60
+ tolower: {
61
+ args: [%i[field character]],
62
+ resolve_for_type: true,
63
+ return_type: :character
64
+ },
65
+ toupper: {
66
+ args: [%i[field character]],
67
+ resolve_for_type: true,
68
+ return_type: :character
69
+ },
70
+ length: {
71
+ args: [%i[field character]],
72
+ resolve_for_type: true,
73
+ return_type: :integer
74
+ },
75
+ indexof: {
76
+ args: [%i[field character], :character],
77
+ return_type: :integer
78
+ },
79
+ concat: {
80
+ args: [%i[field character], :character],
81
+ resolve_for_type: true,
82
+ return_type: :character
83
+ },
84
+ cast: {
85
+ args: [%i[field character decimal integer null], :character],
86
+ resolve_for_type: true
87
+ },
88
+ round: {
89
+ args: [%i[field decimal]],
90
+ resolve_for_type: true,
91
+ return_type: :integer
92
+ },
93
+ ceiling: {
94
+ args: [%i[field decimal]],
95
+ resolve_for_type: true,
96
+ return_type: :integer
97
+ },
98
+ floor: {
99
+ args: [%i[field decimal]],
100
+ resolve_for_type: true,
101
+ return_type: :integer
102
+ },
103
+ startswith: {
104
+ args: [:character],
105
+ return_type: :startswith
106
+ },
107
+ endswith: {
108
+ args: [:character],
109
+ return_type: :endswith
110
+ },
111
+ contains: {
112
+ args: [:character],
113
+ return_type: :contains
114
+ },
115
+ linestring: {
116
+ args: [:character],
117
+ return_type: :shape
118
+ },
119
+ seconds: {
120
+ args: [:integer],
121
+ return_type: :datetime
122
+ },
123
+ minutes: {
124
+ args: [:integer],
125
+ return_type: :datetime
126
+ },
127
+ hours: {
128
+ args: [:integer],
129
+ return_type: :datetime
130
+ },
131
+ days: {
132
+ args: [:integer],
133
+ return_type: :datetime
134
+ },
135
+ weekdays: {
136
+ args: [:integer],
137
+ return_type: :datetime
138
+ },
139
+ months: {
140
+ args: [:integer],
141
+ return_type: :datetime
142
+ },
143
+ years: {
144
+ args: [:integer],
145
+ return_type: :datetime
146
+ },
147
+ now: {
148
+ args: [],
149
+ return_type: :datetime
150
+ },
151
+ maxdatetime: {
152
+ args: [],
153
+ return_type: :datetime
154
+ },
155
+ mindatetime: {
156
+ args: [],
157
+ return_type: :datetime
158
+ },
159
+ date: {
160
+ args: [%i[field datetime date]],
161
+ resolve_for_type: true,
162
+ return_type: :date
163
+ },
164
+ time: {
165
+ args: [%i[field datetime date]],
166
+ resolve_for_type: true,
167
+ return_type: :time
168
+ },
169
+ year: {
170
+ args: [%i[field datetime date]],
171
+ resolve_for_type: true,
172
+ return_type: :integer
173
+ },
174
+ dayofyear: {
175
+ args: [%i[field datetime date]],
176
+ resolve_for_type: true,
177
+ return_type: :integer
178
+ },
179
+ month: {
180
+ args: [%i[field datetime date]],
181
+ resolve_for_type: true,
182
+ return_type: :integer
183
+ },
184
+ day: {
185
+ args: [%i[field datetime date]],
186
+ resolve_for_type: true,
187
+ return_type: :integer
188
+ },
189
+ dayofweek: {
190
+ args: [%i[field datetime date]],
191
+ resolve_for_type: true,
192
+ return_type: :integer
193
+ },
194
+ hour: {
195
+ args: [%i[field datetime date]],
196
+ resolve_for_type: true,
197
+ return_type: :integer
198
+ },
199
+ minute: {
200
+ args: [%i[field datetime date]],
201
+ resolve_for_type: true,
202
+ return_type: :integer
203
+ },
204
+ second: {
205
+ args: [%i[field datetime date]],
206
+ resolve_for_type: true,
207
+ return_type: :integer
208
+ },
209
+ fractionalseconds: {
210
+ args: [%i[field datetime date]],
211
+ resolve_for_type: true,
212
+ return_type: :decimal
213
+ },
214
+ range: {
215
+ args: %i[character character],
216
+ return_type: :character
217
+ },
218
+ wkt: {
219
+ args: [:character],
220
+ return_type: :shape
221
+ }
222
+ }.freeze
223
+
224
+ # Construct a resolver instance for a function
225
+ # name: function name (String)
226
+ # args: array of literal hashes of the format {:type=><literal_type>, :value=><escaped_literal_value>}.
227
+ # Empty arry for functions that have no arguments.
228
+ def initialize(name, args)
229
+ @name = name
230
+ @args = args
231
+ @errors = []
233
232
  end
234
233
 
235
- if name == :cast
236
- type = @args.last[:value]
237
- if !VALID_CAST_TYPES.include?(type.to_sym)
238
- @errors << Sparkql::ParserError.new(:token => @name,
239
- :message => "Function call '#{@name}' requires a castable type.",
240
- :status => :fatal )
234
+ # Validate the function instance prior to calling it. All validation failures will show up in the
235
+ # errors array.
236
+ def validate
237
+ name = @name.to_sym
238
+ unless support.key?(name)
239
+ @errors << Sparkql::ParserError.new(token: @name,
240
+ message: "Unsupported function call '#{@name}' for expression",
241
+ status: :fatal)
242
+ return
243
+ end
244
+
245
+ required_args = support[name][:args]
246
+ total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:type] }
247
+
248
+ if @args.size < required_args.size || @args.size > total_args.size
249
+ @errors << Sparkql::ParserError.new(token: @name,
250
+ message: "Function call '#{@name}' requires #{required_args.size} arguments",
251
+ status: :fatal)
241
252
  return
242
253
  end
254
+
255
+ count = 0
256
+ @args.each do |arg|
257
+ type = arg[:type] == :function ? arg[:return_type] : arg[:type]
258
+ unless Array(total_args[count]).include?(type)
259
+ @errors << Sparkql::ParserError.new(token: @name,
260
+ message: "Function call '#{@name}' has an invalid argument at #{arg[:value]}",
261
+ status: :fatal)
262
+ end
263
+ count += 1
264
+ end
265
+
266
+ if name == :cast
267
+ type = @args.last[:value]
268
+ unless VALID_CAST_TYPES.include?(type.to_sym)
269
+ @errors << Sparkql::ParserError.new(token: @name,
270
+ message: "Function call '#{@name}' requires a castable type.",
271
+ status: :fatal)
272
+ return
273
+ end
274
+ end
275
+
276
+ substring_index_error?(@args[2][:value]) if name == :substring && !@args[2].nil?
243
277
  end
244
278
 
245
- if name == :substring && !@args[2].nil?
246
- substring_index_error?(@args[2][:value])
279
+ def return_type
280
+ name = @name.to_sym
281
+
282
+ if name == :cast
283
+ @args.last[:value].to_sym
284
+ else
285
+ support[@name.to_sym][:return_type]
286
+ end
247
287
  end
248
- end
249
-
250
- def return_type
251
- name = @name.to_sym
252
288
 
253
- if name == :cast
254
- @args.last[:value].to_sym
255
- else
256
- support[@name.to_sym][:return_type]
289
+ attr_reader :errors
290
+
291
+ def errors?
292
+ @errors.size.positive?
257
293
  end
258
- end
259
-
260
- def errors
261
- @errors
262
- end
263
-
264
- def errors?
265
- @errors.size > 0
266
- end
267
-
268
- def support
269
- SUPPORTED_FUNCTIONS
270
- end
271
-
272
- # Execute the function
273
- def call()
274
- real_vals = @args.map { |i| i[:value]}
275
- name = @name.to_sym
276
294
 
277
- field = @args.find do |i|
278
- i[:type] == :field || i.key?(:field)
295
+ def support
296
+ SUPPORTED_FUNCTIONS
279
297
  end
280
298
 
281
- field = field[:type] == :function ? field[:field] : field[:value] unless field.nil?
299
+ # Execute the function
300
+ def call
301
+ real_vals = @args.map { |i| i[:value] }
302
+ name = @name.to_sym
303
+
304
+ field = @args.find do |i|
305
+ i[:type] == :field || i.key?(:field)
306
+ end
307
+
308
+ field = field[:type] == :function ? field[:field] : field[:value] unless field.nil?
309
+
310
+ required_args = support[name][:args]
311
+ total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:default] }
312
+
313
+ fill_in_optional_args = total_args.drop(real_vals.length)
282
314
 
283
- required_args = support[name][:args]
284
- total_args = required_args + Array(support[name][:opt_args]).collect {|args| args[:default]}
315
+ fill_in_optional_args.each do |default|
316
+ real_vals << default
317
+ end
318
+
319
+ v = if field.nil?
320
+ method = name
321
+ if support[name][:resolve_for_type]
322
+ method_type = @args.first[:type]
323
+ method = "#{method}_#{method_type}"
324
+ end
325
+ send(method, *real_vals)
326
+ else
327
+ {
328
+ type: :function,
329
+ return_type: return_type,
330
+ value: name.to_s
331
+ }
332
+ end
333
+
334
+ return if v.nil?
335
+
336
+ unless v.key?(:function_name)
337
+ v.merge!(function_parameters: real_vals,
338
+ function_name: @name)
339
+ end
285
340
 
286
- fill_in_optional_args = total_args.drop(real_vals.length)
341
+ v.merge!(args: @args,
342
+ field: field)
287
343
 
288
- fill_in_optional_args.each do |default|
289
- real_vals << default
344
+ v
290
345
  end
291
346
 
347
+ protected
348
+
349
+ # Supported function calls
350
+
351
+ def regex(regular_expression, flags)
352
+ unless (flags.chars.to_a - VALID_REGEX_FLAGS).empty?
353
+ @errors << Sparkql::ParserError.new(token: regular_expression,
354
+ message: 'Invalid Regexp',
355
+ status: :fatal)
356
+ return
357
+ end
292
358
 
293
- v = if field.nil?
294
- method = name
295
- if support[name][:resolve_for_type]
296
- method_type = @args.first[:type]
297
- method = "#{method}_#{method_type}"
359
+ begin
360
+ Regexp.new(regular_expression)
361
+ rescue StandardError
362
+ @errors << Sparkql::ParserError.new(token: regular_expression,
363
+ message: 'Invalid Regexp',
364
+ status: :fatal)
365
+ return
298
366
  end
299
- self.send(method, *real_vals)
300
- else
367
+
368
+ {
369
+ type: :character,
370
+ value: regular_expression
371
+ }
372
+ end
373
+
374
+ def trim_character(arg)
301
375
  {
302
- :type => :function,
303
- :return_type => return_type,
304
- :value => "#{name}",
376
+ type: :character,
377
+ value: arg.strip
305
378
  }
306
379
  end
307
380
 
308
- return if v.nil?
381
+ def substring_character(character, first_index, number_chars)
382
+ second_index = if number_chars.nil?
383
+ -1
384
+ else
385
+ number_chars + first_index - 1
386
+ end
309
387
 
310
- if !v.key?(:function_name)
311
- v.merge!( function_parameters: real_vals,
312
- function_name: @name)
388
+ new_string = character[first_index..second_index].to_s
389
+
390
+ {
391
+ type: :character,
392
+ value: new_string
393
+ }
313
394
  end
314
395
 
315
- v.merge!(args: @args,
316
- field: field)
396
+ def substring_index_error?(second_index)
397
+ if second_index.to_i.negative?
398
+ @errors << Sparkql::ParserError.new(token: second_index,
399
+ message: "Function call 'substring' may not have a negative integer for its second parameter",
400
+ status: :fatal)
401
+ true
402
+ end
403
+ false
404
+ end
317
405
 
318
- v
319
- end
320
-
321
- protected
322
-
323
- # Supported function calls
406
+ def tolower(_args)
407
+ {
408
+ type: :character,
409
+ value: 'tolower'
410
+ }
411
+ end
324
412
 
325
- def regex(regular_expression, flags)
413
+ def tolower_character(string)
414
+ {
415
+ type: :character,
416
+ value: "'#{string.to_s.downcase}'"
417
+ }
418
+ end
326
419
 
327
- unless (flags.chars.to_a - VALID_REGEX_FLAGS).empty?
328
- @errors << Sparkql::ParserError.new(:token => regular_expression,
329
- :message => "Invalid Regexp",
330
- :status => :fatal)
331
- return
420
+ def toupper_character(string)
421
+ {
422
+ type: :character,
423
+ value: "'#{string.to_s.upcase}'"
424
+ }
332
425
  end
333
426
 
334
- begin
335
- Regexp.new(regular_expression)
336
- rescue
337
- @errors << Sparkql::ParserError.new(:token => regular_expression,
338
- :message => "Invalid Regexp",
339
- :status => :fatal)
340
- return
427
+ def length_character(string)
428
+ {
429
+ type: :integer,
430
+ value: string.size.to_s
431
+ }
341
432
  end
342
433
 
343
- {
344
- :type => :character,
345
- :value => regular_expression
346
- }
347
- end
434
+ def startswith(string)
435
+ # Wrap this string in quotes, as we effectively translate
436
+ # City Eq startswith('far')
437
+ # ...to...
438
+ # City Eq '^far'
439
+ #
440
+ # The string passed in will merely be "far", rather than
441
+ # the string literal "'far'".
442
+ string = Regexp.escape(string)
443
+ new_value = "^#{string}"
348
444
 
349
- def trim_character(arg)
350
- {
351
- :type => :character,
352
- :value => arg.strip
353
- }
354
- end
445
+ {
446
+ function_name: 'regex',
447
+ function_parameters: [new_value, ''],
448
+ type: :character,
449
+ value: new_value
450
+ }
451
+ end
355
452
 
356
- def substring_character(character, first_index, number_chars)
357
- second_index = if number_chars.nil?
358
- -1
359
- else
360
- number_chars + first_index - 1
453
+ def endswith(string)
454
+ # Wrap this string in quotes, as we effectively translate
455
+ # City Eq endswith('far')
456
+ # ...to...
457
+ # City Eq regex('far$')
458
+ #
459
+ # The string passed in will merely be "far", rather than
460
+ # the string literal "'far'".
461
+ string = Regexp.escape(string)
462
+ new_value = "#{string}$"
463
+
464
+ {
465
+ function_name: 'regex',
466
+ function_parameters: [new_value, ''],
467
+ type: :character,
468
+ value: new_value
469
+ }
361
470
  end
362
471
 
363
- new_string = character[first_index..second_index].to_s
472
+ def contains(string)
473
+ # Wrap this string in quotes, as we effectively translate
474
+ # City Eq contains('far')
475
+ # ...to...
476
+ # City Eq regex('far')
477
+ #
478
+ # The string passed in will merely be "far", rather than
479
+ # the string literal "'far'".
480
+ string = Regexp.escape(string)
481
+ new_value = string.to_s
364
482
 
365
- {
366
- :type => :character,
367
- :value => new_string
368
- }
369
- end
483
+ {
484
+ function_name: 'regex',
485
+ function_parameters: [new_value, ''],
486
+ type: :character,
487
+ value: new_value
488
+ }
489
+ end
370
490
 
371
- def substring_index_error?(second_index)
372
- if second_index.to_i < 0
373
- @errors << Sparkql::ParserError.new(:token => second_index,
374
- :message => "Function call 'substring' may not have a negative integer for its second parameter",
375
- :status => :fatal)
376
- true
491
+ # Offset the current timestamp by a number of seconds
492
+ def seconds(num)
493
+ t = current_time + num
494
+ {
495
+ type: :datetime,
496
+ value: t.iso8601
497
+ }
377
498
  end
378
- false
379
- end
380
499
 
381
- def tolower(args)
382
- {
383
- :type => :character,
384
- :value => "tolower"
385
- }
386
- end
500
+ # Offset the current timestamp by a number of minutes
501
+ def minutes(num)
502
+ t = current_time + num * SECONDS_IN_MINUTE
503
+ {
504
+ type: :datetime,
505
+ value: t.iso8601
506
+ }
507
+ end
387
508
 
388
- def tolower_character(string)
389
- {
390
- :type => :character,
391
- :value => "'#{string.to_s.downcase}'"
392
- }
393
- end
509
+ # Offset the current timestamp by a number of hours
510
+ def hours(num)
511
+ t = current_time + num * SECONDS_IN_HOUR
512
+ {
513
+ type: :datetime,
514
+ value: t.iso8601
515
+ }
516
+ end
394
517
 
518
+ # Offset the current timestamp by a number of days
519
+ def days(number_of_days)
520
+ # date calculated as the offset from midnight tommorrow. Zero will provide values for all times
521
+ # today.
522
+ d = current_date + number_of_days
523
+ {
524
+ type: :date,
525
+ value: d.strftime(STRFTIME_DATE_FORMAT)
526
+ }
527
+ end
395
528
 
396
- def toupper_character(string)
397
- {
398
- :type => :character,
399
- :value => "'#{string.to_s.upcase}'"
400
- }
401
- end
529
+ def weekdays(number_of_days)
530
+ today = current_date
531
+ weekend_start = today.saturday? || today.sunday?
532
+ direction = number_of_days.positive? ? 1 : -1
533
+ weeks = (number_of_days / 5.0).to_i
534
+ remaining = number_of_days.abs % 5
535
+
536
+ # Jump ahead the number of weeks represented in the input
537
+ today += weeks * 7
538
+
539
+ # Now iterate on the remaining weekdays
540
+ remaining.times do |i|
541
+ today += direction
542
+ while today.saturday? || today.sunday?
543
+ today += direction
544
+ end
545
+ end
402
546
 
403
- def length_character(string)
404
- {
405
- :type => :integer,
406
- :value => "#{string.size}"
407
- }
408
- end
547
+ # If we end on the weekend, bump accordingly
548
+ while today.saturday? || today.sunday?
549
+ # If we start and end on the weekend, wind things back to the next
550
+ # appropriate weekday.
551
+ if weekend_start && remaining == 0
552
+ today -= direction
553
+ else
554
+ today += direction
555
+ end
556
+ end
409
557
 
410
- def startswith(string)
411
- # Wrap this string in quotes, as we effectively translate
412
- # City Eq startswith('far')
413
- # ...to...
414
- # City Eq '^far'
415
- #
416
- # The string passed in will merely be "far", rather than
417
- # the string literal "'far'".
418
- string = Regexp.escape(string)
419
- new_value = "^#{string}"
420
-
421
- {
422
- :function_name => "regex",
423
- :function_parameters => [new_value, ''],
424
- :type => :character,
425
- :value => new_value
426
- }
427
- end
558
+ {
559
+ type: :date,
560
+ value: today.strftime(STRFTIME_DATE_FORMAT)
561
+ }
562
+ end
428
563
 
429
- def endswith(string)
430
- # Wrap this string in quotes, as we effectively translate
431
- # City Eq endswith('far')
432
- # ...to...
433
- # City Eq regex('far$')
434
- #
435
- # The string passed in will merely be "far", rather than
436
- # the string literal "'far'".
437
- string = Regexp.escape(string)
438
- new_value = "#{string}$"
439
-
440
- {
441
- :function_name => "regex",
442
- :function_parameters => [new_value, ''],
443
- :type => :character,
444
- :value => new_value
445
- }
446
- end
564
+ # The current timestamp
565
+ def now
566
+ {
567
+ type: :datetime,
568
+ value: current_time.iso8601
569
+ }
570
+ end
447
571
 
448
- def contains(string)
449
- # Wrap this string in quotes, as we effectively translate
450
- # City Eq contains('far')
451
- # ...to...
452
- # City Eq regex('far')
453
- #
454
- # The string passed in will merely be "far", rather than
455
- # the string literal "'far'".
456
- string = Regexp.escape(string)
457
- new_value = "#{string}"
458
-
459
- {
460
- :function_name => "regex",
461
- :function_parameters => [new_value, ''],
462
- :type => :character,
463
- :value => new_value
464
- }
465
- end
572
+ def maxdatetime
573
+ {
574
+ type: :datetime,
575
+ value: MAX_DATE_TIME
576
+ }
577
+ end
466
578
 
467
- # Offset the current timestamp by a number of days
468
- def days(num)
469
- # date calculated as the offset from midnight tommorrow. Zero will provide values for all times
470
- # today.
471
- d = Date.today + num
472
- {
473
- :type => :date,
474
- :value => d.strftime(STRFTIME_DATE_FORMAT)
475
- }
476
- end
477
-
478
- # The current timestamp
479
- def now()
480
- {
481
- :type => :datetime,
482
- :value => Time.now.iso8601
483
- }
484
- end
579
+ def mindatetime
580
+ {
581
+ type: :datetime,
582
+ value: MIN_DATE_TIME
583
+ }
584
+ end
485
585
 
486
- def maxdatetime()
487
- {
488
- :type => :datetime,
489
- :value => MAX_DATE_TIME
490
- }
491
- end
586
+ def floor_decimal(arg)
587
+ {
588
+ type: :integer,
589
+ value: arg.floor.to_s
590
+ }
591
+ end
492
592
 
493
- def mindatetime()
494
- {
495
- :type => :datetime,
496
- :value => MIN_DATE_TIME
497
- }
498
- end
593
+ def ceiling_decimal(arg)
594
+ {
595
+ type: :integer,
596
+ value: arg.ceil.to_s
597
+ }
598
+ end
499
599
 
500
- def floor_decimal(arg)
501
- {
502
- :type => :integer,
503
- :value => arg.floor.to_s
504
- }
505
- end
600
+ def round_decimal(arg)
601
+ {
602
+ type: :integer,
603
+ value: arg.round.to_s
604
+ }
605
+ end
506
606
 
507
- def ceiling_decimal(arg)
508
- {
509
- :type => :integer,
510
- :value => arg.ceil.to_s
511
- }
512
- end
607
+ def indexof(arg1, arg2)
608
+ {
609
+ value: 'indexof',
610
+ args: [arg1, arg2]
611
+ }
612
+ end
513
613
 
514
- def round_decimal(arg)
515
- {
516
- :type => :integer,
517
- :value => arg.round.to_s
518
- }
519
- end
614
+ def concat_character(arg1, arg2)
615
+ {
616
+ type: :character,
617
+ value: "'#{arg1}#{arg2}'"
618
+ }
619
+ end
520
620
 
521
- def indexof(arg1, arg2)
522
- {
523
- :value => "indexof",
524
- :args => [arg1, arg2]
525
- }
526
- end
621
+ def date_datetime(datetime)
622
+ {
623
+ type: :date,
624
+ value: datetime.strftime(STRFTIME_DATE_FORMAT)
625
+ }
626
+ end
527
627
 
528
- def concat_character(arg1, arg2)
529
- {
530
- :type => :character,
531
- :value => "'#{arg1}#{arg2}'"
532
- }
533
- end
628
+ def time_datetime(datetime)
629
+ {
630
+ type: :time,
631
+ value: datetime.strftime(STRFTIME_TIME_FORMAT)
632
+ }
633
+ end
534
634
 
535
- def date_datetime(dt)
536
- {
537
- :type => :date,
538
- :value => dt.strftime(STRFTIME_DATE_FORMAT)
539
- }
540
- end
541
-
542
- def time_datetime(dt)
543
- {
544
- :type => :time,
545
- :value => dt.strftime(STRFTIME_TIME_FORMAT)
546
- }
547
- end
635
+ def months(num_months)
636
+ d = current_timestamp >> num_months
637
+ {
638
+ type: :date,
639
+ value: d.strftime(STRFTIME_DATE_FORMAT)
640
+ }
641
+ end
548
642
 
549
- def months num_months
550
- d = DateTime.now >> num_months
551
- {
552
- :type => :date,
553
- :value => d.strftime(STRFTIME_DATE_FORMAT)
554
- }
555
- end
643
+ def years(num_years)
644
+ d = current_timestamp >> (num_years * 12)
645
+ {
646
+ type: :date,
647
+ value: d.strftime(STRFTIME_DATE_FORMAT)
648
+ }
649
+ end
556
650
 
557
- def years num_years
558
- d = DateTime.now >> (num_years * 12)
559
- {
560
- :type => :date,
561
- :value => d.strftime(STRFTIME_DATE_FORMAT)
562
- }
563
- end
564
-
565
- def polygon(coords)
566
- new_coords = parse_coordinates(coords)
567
- unless new_coords.size > 2
568
- @errors << Sparkql::ParserError.new(:token => coords,
569
- :message => "Function call 'polygon' requires at least three coordinates",
570
- :status => :fatal )
571
- return
572
- end
573
-
574
- # auto close the polygon if it's open
575
- unless new_coords.first == new_coords.last
576
- new_coords << new_coords.first.clone
577
- end
578
-
579
- shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([new_coords])
580
- {
581
- :type => :shape,
582
- :value => shape
583
- }
584
- end
651
+ def polygon(coords)
652
+ new_coords = parse_coordinates(coords)
653
+ unless new_coords.size > 2
654
+ @errors << Sparkql::ParserError.new(token: coords,
655
+ message: "Function call 'polygon' requires at least three coordinates",
656
+ status: :fatal)
657
+ return
658
+ end
659
+
660
+ # auto close the polygon if it's open
661
+ new_coords << new_coords.first.clone unless new_coords.first == new_coords.last
585
662
 
586
- def linestring(coords)
587
- new_coords = parse_coordinates(coords)
588
- unless new_coords.size > 1
589
- @errors << Sparkql::ParserError.new(:token => coords,
590
- :message => "Function call 'linestring' requires at least two coordinates",
591
- :status => :fatal )
592
- return
663
+ shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([new_coords])
664
+ {
665
+ type: :shape,
666
+ value: shape
667
+ }
593
668
  end
594
669
 
595
- shape = GeoRuby::SimpleFeatures::LineString.from_coordinates(new_coords)
596
- {
597
- :type => :shape,
598
- :value => shape
599
- }
600
- end
670
+ def linestring(coords)
671
+ new_coords = parse_coordinates(coords)
672
+ unless new_coords.size > 1
673
+ @errors << Sparkql::ParserError.new(token: coords,
674
+ message: "Function call 'linestring' requires at least two coordinates",
675
+ status: :fatal)
676
+ return
677
+ end
601
678
 
602
- def wkt(wkt_string)
603
- shape = GeoRuby::SimpleFeatures::Geometry.from_ewkt(wkt_string)
604
- {
605
- :type => :shape,
606
- :value => shape
607
- }
608
- rescue GeoRuby::SimpleFeatures::EWKTFormatError
609
- @errors << Sparkql::ParserError.new(:token => wkt_string,
610
- :message => "Function call 'wkt' requires a valid wkt string",
611
- :status => :fatal )
612
- return
613
- end
614
-
615
- def rectangle(coords)
616
- bounding_box = parse_coordinates(coords)
617
- unless bounding_box.size == 2
618
- @errors << Sparkql::ParserError.new(:token => coords,
619
- :message => "Function call 'rectangle' requires two coordinates for the bounding box",
620
- :status => :fatal )
621
- return
622
- end
623
- poly_coords = [
624
- bounding_box.first,
625
- [bounding_box.last.first, bounding_box.first.last],
626
- bounding_box.last,
627
- [bounding_box.first.first, bounding_box.last.last],
628
- bounding_box.first.clone,
629
- ]
630
- shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([poly_coords])
631
- {
632
- :type => :shape,
633
- :value => shape
634
- }
635
- end
636
-
637
- def radius(coords, length)
638
-
639
- unless length > 0
640
- @errors << Sparkql::ParserError.new(:token => length,
641
- :message => "Function call 'radius' length must be positive",
642
- :status => :fatal )
643
- return
644
- end
645
-
646
- # The radius() function is overloaded to allow an identifier
647
- # to be specified over lat/lon. This identifier should specify a
648
- # record that, in turn, references a lat/lon. Naturally, this won't be
649
- # validated here.
650
- shape_error = false
651
- shape = if is_coords?(coords)
652
- new_coords = parse_coordinates(coords)
653
- if new_coords.size != 1
654
- shape_error = true
655
- else
656
- GeoRuby::SimpleFeatures::Circle.from_coordinates(new_coords.first, length);
657
- end
658
- elsif Sparkql::Geo::RecordRadius.valid_record_id?(coords)
659
- Sparkql::Geo::RecordRadius.new(coords, length)
660
- else
661
- shape_error = true
662
- end
679
+ shape = GeoRuby::SimpleFeatures::LineString.from_coordinates(new_coords)
680
+ {
681
+ type: :shape,
682
+ value: shape
683
+ }
684
+ end
663
685
 
664
- if shape_error
665
- @errors << Sparkql::ParserError.new(:token => coords,
666
- :message => "Function call 'radius' requires one coordinate for the center",
667
- :status => :fatal )
668
- return
686
+ def wkt(wkt_string)
687
+ shape = GeoRuby::SimpleFeatures::Geometry.from_ewkt(wkt_string)
688
+ {
689
+ type: :shape,
690
+ value: shape
691
+ }
692
+ rescue GeoRuby::SimpleFeatures::EWKTFormatError
693
+ @errors << Sparkql::ParserError.new(token: wkt_string,
694
+ message: "Function call 'wkt' requires a valid wkt string",
695
+ status: :fatal)
696
+ nil
669
697
  end
670
698
 
671
- {
672
- :type => :shape,
673
- :value => shape
674
- }
675
- end
676
-
677
- def range(start_str, end_str)
678
- {
679
- :type => :character,
680
- :value => [start_str.to_s, end_str.to_s]
681
- }
682
- end
699
+ def rectangle(coords)
700
+ bounding_box = parse_coordinates(coords)
701
+ unless bounding_box.size == 2
702
+ @errors << Sparkql::ParserError.new(token: coords,
703
+ message: "Function call 'rectangle' requires two coordinates for the bounding box",
704
+ status: :fatal)
705
+ return
706
+ end
707
+ poly_coords = [
708
+ bounding_box.first,
709
+ [bounding_box.last.first, bounding_box.first.last],
710
+ bounding_box.last,
711
+ [bounding_box.first.first, bounding_box.last.last],
712
+ bounding_box.first.clone
713
+ ]
714
+ shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([poly_coords])
715
+ {
716
+ type: :shape,
717
+ value: shape
718
+ }
719
+ end
683
720
 
684
- def cast(value, type)
685
- if value == 'NULL'
686
- value = nil
687
- end
688
-
689
- new_type = type.to_sym
690
- {
691
- type: new_type,
692
- value: cast_literal(value, new_type)
693
- }
694
- rescue
695
- {
696
- type: :null,
697
- value: 'NULL'
698
- }
699
- end
721
+ def radius(coords, length)
722
+ unless length.positive?
723
+ @errors << Sparkql::ParserError.new(token: length,
724
+ message: "Function call 'radius' length must be positive",
725
+ status: :fatal)
726
+ return
727
+ end
700
728
 
701
- def valid_cast_type?(type)
702
- if VALID_CAST_TYPES.key?(type.to_sym)
703
- true
704
- else
705
- @errors << Sparkql::ParserError.new(:token => coords,
706
- :message => "Function call 'cast' requires a castable type.",
707
- :status => :fatal )
708
- false
729
+ # The radius() function is overloaded to allow an identifier
730
+ # to be specified over lat/lon. This identifier should specify a
731
+ # record that, in turn, references a lat/lon. Naturally, this won't be
732
+ # validated here.
733
+ shape_error = false
734
+ shape = if coords?(coords)
735
+ new_coords = parse_coordinates(coords)
736
+ if new_coords.size != 1
737
+ shape_error = true
738
+ else
739
+ GeoRuby::SimpleFeatures::Circle.from_coordinates(new_coords.first, length)
740
+ end
741
+ elsif Sparkql::Geo::RecordRadius.valid_record_id?(coords)
742
+ Sparkql::Geo::RecordRadius.new(coords, length)
743
+ else
744
+ shape_error = true
745
+ end
746
+
747
+ if shape_error
748
+ @errors << Sparkql::ParserError.new(token: coords,
749
+ message: "Function call 'radius' requires one coordinate for the center",
750
+ status: :fatal)
751
+ return
752
+ end
753
+
754
+ {
755
+ type: :shape,
756
+ value: shape
757
+ }
709
758
  end
710
- end
711
759
 
712
- def cast_null(value, type)
713
- cast(value, type)
714
- end
760
+ def range(start_str, end_str)
761
+ {
762
+ type: :character,
763
+ value: [start_str.to_s, end_str.to_s]
764
+ }
765
+ end
715
766
 
716
- def cast_decimal(value, type)
717
- cast(value, type)
718
- end
767
+ def cast(value, type)
768
+ value = nil if value == 'NULL'
719
769
 
720
- def cast_character(value, type)
721
- cast(value, type)
722
- end
770
+ new_type = type.to_sym
771
+ {
772
+ type: new_type,
773
+ value: cast_literal(value, new_type)
774
+ }
775
+ rescue StandardError
776
+ {
777
+ type: :null,
778
+ value: 'NULL'
779
+ }
780
+ end
723
781
 
724
- def cast_literal(value, type)
725
- case type
726
- when :character
727
- "'#{value.to_s}'"
728
- when :integer
729
- if value.nil?
730
- '0'
782
+ def valid_cast_type?(type)
783
+ if VALID_CAST_TYPES.key?(type.to_sym)
784
+ true
731
785
  else
732
- Integer(Float(value)).to_s
786
+ @errors << Sparkql::ParserError.new(token: coords,
787
+ message: "Function call 'cast' requires a castable type.",
788
+ status: :fatal)
789
+ false
733
790
  end
734
- when :decimal
735
- if value.nil?
736
- '0.0'
737
- else
738
- Float(value).to_s
791
+ end
792
+
793
+ def cast_null(value, type)
794
+ cast(value, type)
795
+ end
796
+
797
+ def cast_decimal(value, type)
798
+ cast(value, type)
799
+ end
800
+
801
+ def cast_character(value, type)
802
+ cast(value, type)
803
+ end
804
+
805
+ def cast_literal(value, type)
806
+ case type
807
+ when :character
808
+ "'#{value}'"
809
+ when :integer
810
+ if value.nil?
811
+ '0'
812
+ else
813
+ Integer(Float(value)).to_s
814
+ end
815
+ when :decimal
816
+ if value.nil?
817
+ '0.0'
818
+ else
819
+ Float(value).to_s
820
+ end
821
+ when :null
822
+ 'NULL'
739
823
  end
740
- when :null
741
- 'NULL'
742
824
  end
743
- end
744
825
 
745
- private
826
+ def current_date
827
+ current_timestamp.to_date
828
+ end
746
829
 
747
- def is_coords?(coord_string)
748
- coord_string.split(" ").size > 1
749
- end
830
+ def current_time
831
+ current_timestamp.to_time
832
+ end
750
833
 
751
- def parse_coordinates coord_string
752
- terms = coord_string.strip.split(',')
753
- coords = terms.map do |term|
754
- term.strip.split(/\s+/).reverse.map { |i| i.to_f }
834
+ def current_timestamp
835
+ @current_timestamp ||= DateTime.now
836
+ end
837
+
838
+ private
839
+
840
+ def coords?(coord_string)
841
+ coord_string.split(' ').size > 1
842
+ end
843
+
844
+ def parse_coordinates(coord_string)
845
+ terms = coord_string.strip.split(',')
846
+ terms.map do |term|
847
+ term.strip.split(/\s+/).reverse.map(&:to_f)
848
+ end
849
+ rescue StandardError
850
+ @errors << Sparkql::ParserError.new(token: coord_string,
851
+ message: 'Unable to parse coordinate string.',
852
+ status: :fatal)
755
853
  end
756
- coords
757
- rescue
758
- @errors << Sparkql::ParserError.new(:token => coord_string,
759
- :message => "Unable to parse coordinate string.",
760
- :status => :fatal )
761
854
  end
762
-
763
855
  end