sparkql 1.2.5 → 1.3.0

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