sparkql 1.2.5 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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