sparkql 1.2.6 → 1.2.7

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