sparkql 1.2.6 → 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
- 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