sparkql 1.2.4 → 1.2.8

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
- YmIyYTljOGIyZmFjZTEyZmY1MzU2ODZjYTg4ZWU1NDI2NWNkOWRkYw==
5
- data.tar.gz: !binary |-
6
- NDkwMjczNDY2N2JlOGRmMzA5MDQ1YmI4Njg3ZTM5MTJlMzBiODVhMQ==
2
+ SHA256:
3
+ metadata.gz: 1d6df2039c1c9c690fe60002b8374ec300e1ffab8496b02e668da3f77999d2e8
4
+ data.tar.gz: a529df747695e8c43a45bec3f166cc8e1b2cfa34a671173a998b4620fe522a67
7
5
  SHA512:
8
- metadata.gz: !binary |-
9
- OTk2NTgwODMyNjExMjBiZThmODdjNzczZmM4M2MzMmY0MWNiNTkxODU5NThk
10
- Y2UwYzY0ZTQ1NGRhNmFjYjI1ODIwYzBiMmMyMDM2NzFmNmNhNTI1OGNkYjNj
11
- NzJhZTRjMDk4MzVmYjJlNDM4YTE2ZWFjYTVjNzlmMGZlNzM4NjQ=
12
- data.tar.gz: !binary |-
13
- NjhmNWU0NmZjNDVkMmUzMDJhMGM2ZTY0ZWVjMTk3ZGUwZGI3MzM2OTQwN2M4
14
- MTUzYTk3N2E1NzI2YWFmMTA1YWIxZWY2MTliZTRiOGNjOTU5NjEwMGFlODkz
15
- YjRmYmVmN2M4NTJkMGUyM2NlNjhmODA0ZGFkYWYzM2Q0OTNlMDU=
6
+ metadata.gz: e4a0fa65b7cb2b41198f239a6c68613939e3f6f0ce4ce20a9ae00b1ea4f7ae377cdd41b3b67f4a2da158344e33fa2d12d0a9cec7cc7c64a85a14e9f35bdffbdc
7
+ data.tar.gz: 746cfed6761f6bdc8ec7dfe91ad45b20d44fd6a67a7fe13e184406a8fab533c1a4122435242447da803160c4c4e3f05a8a8e61ba3a83ca568f2dd4d2b305ed1a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ v1.2.8, 2021-08-11
2
+ -------------------
3
+ * [IMPROVEMENT] all() function
4
+
5
+ v1.2.7, 2021-05-06
6
+ -------------------
7
+ * [IMPROVEMENT] dayofweek(), dayofyear(), and weekdays() functions
8
+
9
+ v1.2.6, 2019-04-01
10
+ -------------------
11
+ * [IMPROVEMENT] hours(), minutes(), and seconds() functions
12
+
13
+ v1.2.5, 2018-12-19
14
+ -------------------
15
+ * [BUGFIX] Correctly handle arithmetic grouping
16
+
1
17
  v1.2.4, 2018-12-13
2
18
  -------------------
3
19
  * [IMPROVEMENT] Support decimal arithmetic
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.4
1
+ 1.2.8
@@ -1,763 +1,859 @@
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
+ # Construct a resolver instance for a function
229
+ # name: function name (String)
230
+ # args: array of literal hashes of the format {:type=><literal_type>, :value=><escaped_literal_value>}.
231
+ # Empty arry for functions that have no arguments.
232
+ def initialize(name, args)
233
+ @name = name
234
+ @args = args
235
+ @errors = []
233
236
  end
234
237
 
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 )
238
+ # Validate the function instance prior to calling it. All validation failures will show up in the
239
+ # errors array.
240
+ def validate
241
+ name = @name.to_sym
242
+ unless support.key?(name)
243
+ @errors << Sparkql::ParserError.new(token: @name,
244
+ message: "Unsupported function call '#{@name}' for expression",
245
+ status: :fatal)
246
+ return
247
+ end
248
+
249
+ required_args = support[name][:args]
250
+ total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:type] }
251
+
252
+ if @args.size < required_args.size || @args.size > total_args.size
253
+ @errors << Sparkql::ParserError.new(token: @name,
254
+ message: "Function call '#{@name}' requires #{required_args.size} arguments",
255
+ status: :fatal)
241
256
  return
242
257
  end
258
+
259
+ count = 0
260
+ @args.each do |arg|
261
+ type = arg[:type] == :function ? arg[:return_type] : arg[:type]
262
+ unless Array(total_args[count]).include?(type)
263
+ @errors << Sparkql::ParserError.new(token: @name,
264
+ message: "Function call '#{@name}' has an invalid argument at #{arg[:value]}",
265
+ status: :fatal)
266
+ end
267
+ count += 1
268
+ end
269
+
270
+ if name == :cast
271
+ type = @args.last[:value]
272
+ unless VALID_CAST_TYPES.include?(type.to_sym)
273
+ @errors << Sparkql::ParserError.new(token: @name,
274
+ message: "Function call '#{@name}' requires a castable type.",
275
+ status: :fatal)
276
+ return
277
+ end
278
+ end
279
+
280
+ substring_index_error?(@args[2][:value]) if name == :substring && !@args[2].nil?
243
281
  end
244
282
 
245
- if name == :substring && !@args[2].nil?
246
- substring_index_error?(@args[2][:value])
283
+ def return_type
284
+ name = @name.to_sym
285
+
286
+ if name == :cast
287
+ @args.last[:value].to_sym
288
+ else
289
+ support[@name.to_sym][:return_type]
290
+ end
247
291
  end
248
- end
249
-
250
- def return_type
251
- name = @name.to_sym
252
292
 
253
- if name == :cast
254
- @args.last[:value].to_sym
255
- else
256
- support[@name.to_sym][:return_type]
293
+ attr_reader :errors
294
+
295
+ def errors?
296
+ @errors.size.positive?
257
297
  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
298
 
277
- field = @args.find do |i|
278
- i[:type] == :field || i.key?(:field)
299
+ def support
300
+ SUPPORTED_FUNCTIONS
279
301
  end
280
302
 
281
- field = field[:type] == :function ? field[:field] : field[:value] unless field.nil?
303
+ # Execute the function
304
+ def call
305
+ real_vals = @args.map { |i| i[:value] }
306
+ name = @name.to_sym
307
+
308
+ field = @args.find do |i|
309
+ i[:type] == :field || i.key?(:field)
310
+ end
311
+
312
+ field = field[:type] == :function ? field[:field] : field[:value] unless field.nil?
313
+
314
+ required_args = support[name][:args]
315
+ total_args = required_args + Array(support[name][:opt_args]).collect { |args| args[:default] }
316
+
317
+ fill_in_optional_args = total_args.drop(real_vals.length)
282
318
 
283
- required_args = support[name][:args]
284
- total_args = required_args + Array(support[name][:opt_args]).collect {|args| args[:default]}
319
+ fill_in_optional_args.each do |default|
320
+ real_vals << default
321
+ end
322
+
323
+ v = if field.nil?
324
+ method = name
325
+ if support[name][:resolve_for_type]
326
+ method_type = @args.first[:type]
327
+ method = "#{method}_#{method_type}"
328
+ end
329
+ send(method, *real_vals)
330
+ else
331
+ {
332
+ type: :function,
333
+ return_type: return_type,
334
+ value: name.to_s
335
+ }
336
+ end
337
+
338
+ return if v.nil?
339
+
340
+ unless v.key?(:function_name)
341
+ v.merge!(function_parameters: real_vals,
342
+ function_name: @name)
343
+ end
285
344
 
286
- fill_in_optional_args = total_args.drop(real_vals.length)
345
+ v.merge!(args: @args,
346
+ field: field)
287
347
 
288
- fill_in_optional_args.each do |default|
289
- real_vals << default
348
+ v
290
349
  end
291
350
 
351
+ protected
352
+
353
+ # Supported function calls
354
+
355
+ def regex(regular_expression, flags)
356
+ unless (flags.chars.to_a - VALID_REGEX_FLAGS).empty?
357
+ @errors << Sparkql::ParserError.new(token: regular_expression,
358
+ message: 'Invalid Regexp',
359
+ status: :fatal)
360
+ return
361
+ end
292
362
 
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}"
363
+ begin
364
+ Regexp.new(regular_expression)
365
+ rescue StandardError
366
+ @errors << Sparkql::ParserError.new(token: regular_expression,
367
+ message: 'Invalid Regexp',
368
+ status: :fatal)
369
+ return
298
370
  end
299
- self.send(method, *real_vals)
300
- else
371
+
372
+ {
373
+ type: :character,
374
+ value: regular_expression
375
+ }
376
+ end
377
+
378
+ def trim_character(arg)
301
379
  {
302
- :type => :function,
303
- :return_type => return_type,
304
- :value => "#{name}",
380
+ type: :character,
381
+ value: arg.strip
305
382
  }
306
383
  end
307
384
 
308
- return if v.nil?
385
+ def substring_character(character, first_index, number_chars)
386
+ second_index = if number_chars.nil?
387
+ -1
388
+ else
389
+ number_chars + first_index - 1
390
+ end
309
391
 
310
- if !v.key?(:function_name)
311
- v.merge!( function_parameters: real_vals,
312
- function_name: @name)
392
+ new_string = character[first_index..second_index].to_s
393
+
394
+ {
395
+ type: :character,
396
+ value: new_string
397
+ }
313
398
  end
314
399
 
315
- v.merge!(args: @args,
316
- field: field)
400
+ def substring_index_error?(second_index)
401
+ if second_index.to_i.negative?
402
+ @errors << Sparkql::ParserError.new(token: second_index,
403
+ message: "Function call 'substring' may not have a negative integer for its second parameter",
404
+ status: :fatal)
405
+ true
406
+ end
407
+ false
408
+ end
317
409
 
318
- v
319
- end
320
-
321
- protected
322
-
323
- # Supported function calls
410
+ def tolower(_args)
411
+ {
412
+ type: :character,
413
+ value: 'tolower'
414
+ }
415
+ end
324
416
 
325
- def regex(regular_expression, flags)
417
+ def tolower_character(string)
418
+ {
419
+ type: :character,
420
+ value: "'#{string.to_s.downcase}'"
421
+ }
422
+ end
326
423
 
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
424
+ def toupper_character(string)
425
+ {
426
+ type: :character,
427
+ value: "'#{string.to_s.upcase}'"
428
+ }
332
429
  end
333
430
 
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
431
+ def length_character(string)
432
+ {
433
+ type: :integer,
434
+ value: string.size.to_s
435
+ }
341
436
  end
342
437
 
343
- {
344
- :type => :character,
345
- :value => regular_expression
346
- }
347
- end
438
+ def startswith(string)
439
+ # Wrap this string in quotes, as we effectively translate
440
+ # City Eq startswith('far')
441
+ # ...to...
442
+ # City Eq '^far'
443
+ #
444
+ # The string passed in will merely be "far", rather than
445
+ # the string literal "'far'".
446
+ string = Regexp.escape(string)
447
+ new_value = "^#{string}"
348
448
 
349
- def trim_character(arg)
350
- {
351
- :type => :character,
352
- :value => arg.strip
353
- }
354
- end
449
+ {
450
+ function_name: 'regex',
451
+ function_parameters: [new_value, ''],
452
+ type: :character,
453
+ value: new_value
454
+ }
455
+ end
355
456
 
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
457
+ def endswith(string)
458
+ # Wrap this string in quotes, as we effectively translate
459
+ # City Eq endswith('far')
460
+ # ...to...
461
+ # City Eq regex('far$')
462
+ #
463
+ # The string passed in will merely be "far", rather than
464
+ # the string literal "'far'".
465
+ string = Regexp.escape(string)
466
+ new_value = "#{string}$"
467
+
468
+ {
469
+ function_name: 'regex',
470
+ function_parameters: [new_value, ''],
471
+ type: :character,
472
+ value: new_value
473
+ }
361
474
  end
362
475
 
363
- new_string = character[first_index..second_index].to_s
476
+ def contains(string)
477
+ # Wrap this string in quotes, as we effectively translate
478
+ # City Eq contains('far')
479
+ # ...to...
480
+ # City Eq regex('far')
481
+ #
482
+ # The string passed in will merely be "far", rather than
483
+ # the string literal "'far'".
484
+ string = Regexp.escape(string)
485
+ new_value = string.to_s
364
486
 
365
- {
366
- :type => :character,
367
- :value => new_string
368
- }
369
- end
487
+ {
488
+ function_name: 'regex',
489
+ function_parameters: [new_value, ''],
490
+ type: :character,
491
+ value: new_value
492
+ }
493
+ end
370
494
 
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
495
+ # Offset the current timestamp by a number of seconds
496
+ def seconds(num)
497
+ t = current_time + num
498
+ {
499
+ type: :datetime,
500
+ value: t.iso8601
501
+ }
377
502
  end
378
- false
379
- end
380
503
 
381
- def tolower(args)
382
- {
383
- :type => :character,
384
- :value => "tolower"
385
- }
386
- end
504
+ # Offset the current timestamp by a number of minutes
505
+ def minutes(num)
506
+ t = current_time + num * SECONDS_IN_MINUTE
507
+ {
508
+ type: :datetime,
509
+ value: t.iso8601
510
+ }
511
+ end
387
512
 
388
- def tolower_character(string)
389
- {
390
- :type => :character,
391
- :value => "'#{string.to_s.downcase}'"
392
- }
393
- end
513
+ # Offset the current timestamp by a number of hours
514
+ def hours(num)
515
+ t = current_time + num * SECONDS_IN_HOUR
516
+ {
517
+ type: :datetime,
518
+ value: t.iso8601
519
+ }
520
+ end
394
521
 
522
+ # Offset the current timestamp by a number of days
523
+ def days(number_of_days)
524
+ # date calculated as the offset from midnight tommorrow. Zero will provide values for all times
525
+ # today.
526
+ d = current_date + number_of_days
527
+ {
528
+ type: :date,
529
+ value: d.strftime(STRFTIME_DATE_FORMAT)
530
+ }
531
+ end
395
532
 
396
- def toupper_character(string)
397
- {
398
- :type => :character,
399
- :value => "'#{string.to_s.upcase}'"
400
- }
401
- end
533
+ def weekdays(number_of_days)
534
+ today = current_date
535
+ weekend_start = today.saturday? || today.sunday?
536
+ direction = number_of_days.positive? ? 1 : -1
537
+ weeks = (number_of_days / 5.0).to_i
538
+ remaining = number_of_days.abs % 5
539
+
540
+ # Jump ahead the number of weeks represented in the input
541
+ today += weeks * 7
542
+
543
+ # Now iterate on the remaining weekdays
544
+ remaining.times do |i|
545
+ today += direction
546
+ while today.saturday? || today.sunday?
547
+ today += direction
548
+ end
549
+ end
402
550
 
403
- def length_character(string)
404
- {
405
- :type => :integer,
406
- :value => "#{string.size}"
407
- }
408
- end
551
+ # If we end on the weekend, bump accordingly
552
+ while today.saturday? || today.sunday?
553
+ # If we start and end on the weekend, wind things back to the next
554
+ # appropriate weekday.
555
+ if weekend_start && remaining == 0
556
+ today -= direction
557
+ else
558
+ today += direction
559
+ end
560
+ end
409
561
 
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
562
+ {
563
+ type: :date,
564
+ value: today.strftime(STRFTIME_DATE_FORMAT)
565
+ }
566
+ end
428
567
 
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
568
+ # The current timestamp
569
+ def now
570
+ {
571
+ type: :datetime,
572
+ value: current_time.iso8601
573
+ }
574
+ end
447
575
 
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
576
+ def maxdatetime
577
+ {
578
+ type: :datetime,
579
+ value: MAX_DATE_TIME
580
+ }
581
+ end
466
582
 
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
583
+ def mindatetime
584
+ {
585
+ type: :datetime,
586
+ value: MIN_DATE_TIME
587
+ }
588
+ end
485
589
 
486
- def maxdatetime()
487
- {
488
- :type => :datetime,
489
- :value => MAX_DATE_TIME
490
- }
491
- end
590
+ def floor_decimal(arg)
591
+ {
592
+ type: :integer,
593
+ value: arg.floor.to_s
594
+ }
595
+ end
492
596
 
493
- def mindatetime()
494
- {
495
- :type => :datetime,
496
- :value => MIN_DATE_TIME
497
- }
498
- end
597
+ def ceiling_decimal(arg)
598
+ {
599
+ type: :integer,
600
+ value: arg.ceil.to_s
601
+ }
602
+ end
499
603
 
500
- def floor_decimal(arg)
501
- {
502
- :type => :integer,
503
- :value => arg.floor.to_s
504
- }
505
- end
604
+ def round_decimal(arg)
605
+ {
606
+ type: :integer,
607
+ value: arg.round.to_s
608
+ }
609
+ end
506
610
 
507
- def ceiling_decimal(arg)
508
- {
509
- :type => :integer,
510
- :value => arg.ceil.to_s
511
- }
512
- end
611
+ def indexof(arg1, arg2)
612
+ {
613
+ value: 'indexof',
614
+ args: [arg1, arg2]
615
+ }
616
+ end
513
617
 
514
- def round_decimal(arg)
515
- {
516
- :type => :integer,
517
- :value => arg.round.to_s
518
- }
519
- end
618
+ def concat_character(arg1, arg2)
619
+ {
620
+ type: :character,
621
+ value: "'#{arg1}#{arg2}'"
622
+ }
623
+ end
520
624
 
521
- def indexof(arg1, arg2)
522
- {
523
- :value => "indexof",
524
- :args => [arg1, arg2]
525
- }
526
- end
625
+ def date_datetime(datetime)
626
+ {
627
+ type: :date,
628
+ value: datetime.strftime(STRFTIME_DATE_FORMAT)
629
+ }
630
+ end
527
631
 
528
- def concat_character(arg1, arg2)
529
- {
530
- :type => :character,
531
- :value => "'#{arg1}#{arg2}'"
532
- }
533
- end
632
+ def time_datetime(datetime)
633
+ {
634
+ type: :time,
635
+ value: datetime.strftime(STRFTIME_TIME_FORMAT)
636
+ }
637
+ end
534
638
 
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
639
+ def months(num_months)
640
+ d = current_timestamp >> num_months
641
+ {
642
+ type: :date,
643
+ value: d.strftime(STRFTIME_DATE_FORMAT)
644
+ }
645
+ end
548
646
 
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
647
+ def years(num_years)
648
+ d = current_timestamp >> (num_years * 12)
649
+ {
650
+ type: :date,
651
+ value: d.strftime(STRFTIME_DATE_FORMAT)
652
+ }
653
+ end
556
654
 
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
655
+ def polygon(coords)
656
+ new_coords = parse_coordinates(coords)
657
+ unless new_coords.size > 2
658
+ @errors << Sparkql::ParserError.new(token: coords,
659
+ message: "Function call 'polygon' requires at least three coordinates",
660
+ status: :fatal)
661
+ return
662
+ end
663
+
664
+ # auto close the polygon if it's open
665
+ new_coords << new_coords.first.clone unless new_coords.first == new_coords.last
585
666
 
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
667
+ shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([new_coords])
668
+ {
669
+ type: :shape,
670
+ value: shape
671
+ }
593
672
  end
594
673
 
595
- shape = GeoRuby::SimpleFeatures::LineString.from_coordinates(new_coords)
596
- {
597
- :type => :shape,
598
- :value => shape
599
- }
600
- end
674
+ def linestring(coords)
675
+ new_coords = parse_coordinates(coords)
676
+ unless new_coords.size > 1
677
+ @errors << Sparkql::ParserError.new(token: coords,
678
+ message: "Function call 'linestring' requires at least two coordinates",
679
+ status: :fatal)
680
+ return
681
+ end
601
682
 
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
683
+ shape = GeoRuby::SimpleFeatures::LineString.from_coordinates(new_coords)
684
+ {
685
+ type: :shape,
686
+ value: shape
687
+ }
688
+ end
663
689
 
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
690
+ def wkt(wkt_string)
691
+ shape = GeoRuby::SimpleFeatures::Geometry.from_ewkt(wkt_string)
692
+ {
693
+ type: :shape,
694
+ value: shape
695
+ }
696
+ rescue GeoRuby::SimpleFeatures::EWKTFormatError
697
+ @errors << Sparkql::ParserError.new(token: wkt_string,
698
+ message: "Function call 'wkt' requires a valid wkt string",
699
+ status: :fatal)
700
+ nil
669
701
  end
670
702
 
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
703
+ def rectangle(coords)
704
+ bounding_box = parse_coordinates(coords)
705
+ unless bounding_box.size == 2
706
+ @errors << Sparkql::ParserError.new(token: coords,
707
+ message: "Function call 'rectangle' requires two coordinates for the bounding box",
708
+ status: :fatal)
709
+ return
710
+ end
711
+ poly_coords = [
712
+ bounding_box.first,
713
+ [bounding_box.last.first, bounding_box.first.last],
714
+ bounding_box.last,
715
+ [bounding_box.first.first, bounding_box.last.last],
716
+ bounding_box.first.clone
717
+ ]
718
+ shape = GeoRuby::SimpleFeatures::Polygon.from_coordinates([poly_coords])
719
+ {
720
+ type: :shape,
721
+ value: shape
722
+ }
723
+ end
683
724
 
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
725
+ def radius(coords, length)
726
+ unless length.positive?
727
+ @errors << Sparkql::ParserError.new(token: length,
728
+ message: "Function call 'radius' length must be positive",
729
+ status: :fatal)
730
+ return
731
+ end
700
732
 
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
733
+ # The radius() function is overloaded to allow an identifier
734
+ # to be specified over lat/lon. This identifier should specify a
735
+ # record that, in turn, references a lat/lon. Naturally, this won't be
736
+ # validated here.
737
+ shape_error = false
738
+ shape = if coords?(coords)
739
+ new_coords = parse_coordinates(coords)
740
+ if new_coords.size != 1
741
+ shape_error = true
742
+ else
743
+ GeoRuby::SimpleFeatures::Circle.from_coordinates(new_coords.first, length)
744
+ end
745
+ elsif Sparkql::Geo::RecordRadius.valid_record_id?(coords)
746
+ Sparkql::Geo::RecordRadius.new(coords, length)
747
+ else
748
+ shape_error = true
749
+ end
750
+
751
+ if shape_error
752
+ @errors << Sparkql::ParserError.new(token: coords,
753
+ message: "Function call 'radius' requires one coordinate for the center",
754
+ status: :fatal)
755
+ return
756
+ end
757
+
758
+ {
759
+ type: :shape,
760
+ value: shape
761
+ }
709
762
  end
710
- end
711
763
 
712
- def cast_null(value, type)
713
- cast(value, type)
714
- end
764
+ def range(start_str, end_str)
765
+ {
766
+ type: :character,
767
+ value: [start_str.to_s, end_str.to_s]
768
+ }
769
+ end
715
770
 
716
- def cast_decimal(value, type)
717
- cast(value, type)
718
- end
771
+ def cast(value, type)
772
+ value = nil if value == 'NULL'
719
773
 
720
- def cast_character(value, type)
721
- cast(value, type)
722
- end
774
+ new_type = type.to_sym
775
+ {
776
+ type: new_type,
777
+ value: cast_literal(value, new_type)
778
+ }
779
+ rescue StandardError
780
+ {
781
+ type: :null,
782
+ value: 'NULL'
783
+ }
784
+ end
723
785
 
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'
786
+ def valid_cast_type?(type)
787
+ if VALID_CAST_TYPES.key?(type.to_sym)
788
+ true
731
789
  else
732
- Integer(Float(value)).to_s
790
+ @errors << Sparkql::ParserError.new(token: coords,
791
+ message: "Function call 'cast' requires a castable type.",
792
+ status: :fatal)
793
+ false
733
794
  end
734
- when :decimal
735
- if value.nil?
736
- '0.0'
737
- else
738
- Float(value).to_s
795
+ end
796
+
797
+ def cast_null(value, type)
798
+ cast(value, type)
799
+ end
800
+
801
+ def cast_decimal(value, type)
802
+ cast(value, type)
803
+ end
804
+
805
+ def cast_character(value, type)
806
+ cast(value, type)
807
+ end
808
+
809
+ def cast_literal(value, type)
810
+ case type
811
+ when :character
812
+ "'#{value}'"
813
+ when :integer
814
+ if value.nil?
815
+ '0'
816
+ else
817
+ Integer(Float(value)).to_s
818
+ end
819
+ when :decimal
820
+ if value.nil?
821
+ '0.0'
822
+ else
823
+ Float(value).to_s
824
+ end
825
+ when :null
826
+ 'NULL'
739
827
  end
740
- when :null
741
- 'NULL'
742
828
  end
743
- end
744
829
 
745
- private
830
+ def current_date
831
+ current_timestamp.to_date
832
+ end
746
833
 
747
- def is_coords?(coord_string)
748
- coord_string.split(" ").size > 1
749
- end
834
+ def current_time
835
+ current_timestamp.to_time
836
+ end
750
837
 
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 }
838
+ def current_timestamp
839
+ @current_timestamp ||= DateTime.now
840
+ end
841
+
842
+ private
843
+
844
+ def coords?(coord_string)
845
+ coord_string.split(' ').size > 1
846
+ end
847
+
848
+ def parse_coordinates(coord_string)
849
+ terms = coord_string.strip.split(',')
850
+ terms.map do |term|
851
+ term.strip.split(/\s+/).reverse.map(&:to_f)
852
+ end
853
+ rescue StandardError
854
+ @errors << Sparkql::ParserError.new(token: coord_string,
855
+ message: 'Unable to parse coordinate string.',
856
+ status: :fatal)
755
857
  end
756
- coords
757
- rescue
758
- @errors << Sparkql::ParserError.new(:token => coord_string,
759
- :message => "Unable to parse coordinate string.",
760
- :status => :fatal )
761
858
  end
762
-
763
859
  end