sparkql 1.2.4 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,15 +1,7 @@
1
1
  ---
2
- !binary "U0hBMQ==":
3
- metadata.gz: !binary |-
4
- 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