jsonata 2.2.2.1
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 +7 -0
- data/.rspec +1 -0
- data/.rubocop.yml +4 -0
- data/.rubocop_local.yml +52 -0
- data/AGENTS.md +171 -0
- data/CHANGELOG.md +17 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +189 -0
- data/Rakefile +41 -0
- data/lib/jsonata/datetime.rb +1237 -0
- data/lib/jsonata/error.rb +113 -0
- data/lib/jsonata/evaluator.rb +1669 -0
- data/lib/jsonata/functions.rb +1505 -0
- data/lib/jsonata/parser.rb +1206 -0
- data/lib/jsonata/signature.rb +235 -0
- data/lib/jsonata/utils.rb +600 -0
- data/lib/jsonata/version.rb +5 -0
- data/lib/jsonata.rb +47 -0
- metadata +108 -0
|
@@ -0,0 +1,1505 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# (c) Copyright IBM Corp. 2016, 2018 All Rights Reserved
|
|
4
|
+
# Project name: JSONata
|
|
5
|
+
# This project is licensed under the MIT License, see LICENSE
|
|
6
|
+
|
|
7
|
+
# Port of jsonata-js src/functions.js
|
|
8
|
+
#
|
|
9
|
+
# The functions are defined as instance methods, and are invoked with a Jsonata::Focus object as the receiver,
|
|
10
|
+
# which is the equivalent of the `this` object that jsonata-js invokes them with.
|
|
11
|
+
|
|
12
|
+
module Jsonata
|
|
13
|
+
module Functions
|
|
14
|
+
# Sum function
|
|
15
|
+
# @param args - Arguments
|
|
16
|
+
# @returns Total value of arguments
|
|
17
|
+
def sum(args)
|
|
18
|
+
# undefined inputs always return undefined
|
|
19
|
+
return nil if args.nil?
|
|
20
|
+
|
|
21
|
+
total = 0.0
|
|
22
|
+
args.each do |num|
|
|
23
|
+
total += num
|
|
24
|
+
end
|
|
25
|
+
Utils.normalize_number(total)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Count function
|
|
29
|
+
# @param args - Arguments
|
|
30
|
+
# @returns Number of elements in the array
|
|
31
|
+
def count(args)
|
|
32
|
+
# undefined inputs always return undefined
|
|
33
|
+
return 0 if args.nil?
|
|
34
|
+
|
|
35
|
+
args.length
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Max function
|
|
39
|
+
# @param args - Arguments
|
|
40
|
+
# @returns Max element in the array
|
|
41
|
+
def max(args)
|
|
42
|
+
# undefined inputs always return undefined
|
|
43
|
+
return nil if args.nil? || args.empty?
|
|
44
|
+
|
|
45
|
+
args.max
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Min function
|
|
49
|
+
# @param args - Arguments
|
|
50
|
+
# @returns Min element in the array
|
|
51
|
+
def min(args)
|
|
52
|
+
# undefined inputs always return undefined
|
|
53
|
+
return nil if args.nil? || args.empty?
|
|
54
|
+
|
|
55
|
+
args.min
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Average function
|
|
59
|
+
# @param args - Arguments
|
|
60
|
+
# @returns Average element in the array
|
|
61
|
+
def average(args)
|
|
62
|
+
# undefined inputs always return undefined
|
|
63
|
+
return nil if args.nil? || args.empty?
|
|
64
|
+
|
|
65
|
+
total = 0.0
|
|
66
|
+
args.each do |num|
|
|
67
|
+
total += num
|
|
68
|
+
end
|
|
69
|
+
Utils.normalize_number(total / args.length)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Stringify arguments
|
|
73
|
+
# @param arg - Arguments
|
|
74
|
+
# @param prettify - Pretty print the result
|
|
75
|
+
# @returns String from arguments
|
|
76
|
+
def string(arg, prettify = false)
|
|
77
|
+
# undefined inputs always return undefined
|
|
78
|
+
return nil if arg.nil?
|
|
79
|
+
|
|
80
|
+
if arg.kind_of?(String)
|
|
81
|
+
# already a string
|
|
82
|
+
arg
|
|
83
|
+
elsif Utils.function?(arg)
|
|
84
|
+
# functions (built-in and lambda convert to empty string
|
|
85
|
+
''
|
|
86
|
+
elsif arg.kind_of?(Float) && !arg.finite?
|
|
87
|
+
raise Jsonata::Error.new('D3001', :value => arg)
|
|
88
|
+
else
|
|
89
|
+
space = prettify ? 2 : 0
|
|
90
|
+
arg = arg[0] if arg.kind_of?(JArray) && arg.outer_wrapper
|
|
91
|
+
Utils.json_stringify(arg, lambda { |val|
|
|
92
|
+
if !val.nil? && val != NULL && Utils.numeric?(val)
|
|
93
|
+
Utils.integer?(val) ? val : Utils.to_precision(val, 15)
|
|
94
|
+
else
|
|
95
|
+
Utils.function?(val) ? '' : val
|
|
96
|
+
end
|
|
97
|
+
}, space)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Create substring based on character number and length
|
|
102
|
+
# @param str - String to evaluate
|
|
103
|
+
# @param start - Character number to start substring
|
|
104
|
+
# @param length - Number of characters in substring
|
|
105
|
+
# @returns Substring
|
|
106
|
+
def substring(str, start, length)
|
|
107
|
+
# undefined inputs always return undefined
|
|
108
|
+
return nil if str.nil?
|
|
109
|
+
|
|
110
|
+
str_array = Utils.string_to_array(str)
|
|
111
|
+
str_length = str_array.length
|
|
112
|
+
|
|
113
|
+
start = 0 if (str_length + start).negative?
|
|
114
|
+
|
|
115
|
+
unless length.nil?
|
|
116
|
+
return '' if length <= 0
|
|
117
|
+
|
|
118
|
+
end_pos = start >= 0 ? start + length : str_length + start + length
|
|
119
|
+
return Utils.js_slice(str_array, start, end_pos).join
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
Utils.js_slice(str_array, start).join
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Create substring up until a character
|
|
126
|
+
# @param str - String to evaluate
|
|
127
|
+
# @param chars - Character to define substring boundary
|
|
128
|
+
# @returns Substring
|
|
129
|
+
def substring_before(str, chars)
|
|
130
|
+
# undefined inputs always return undefined
|
|
131
|
+
return nil if str.nil?
|
|
132
|
+
|
|
133
|
+
pos = str.index(chars)
|
|
134
|
+
if pos.nil?
|
|
135
|
+
str
|
|
136
|
+
else
|
|
137
|
+
str[0, pos]
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Create substring after a character
|
|
142
|
+
# @param str - String to evaluate
|
|
143
|
+
# @param chars - Character to define substring boundary
|
|
144
|
+
# @returns Substring
|
|
145
|
+
def substring_after(str, chars)
|
|
146
|
+
# undefined inputs always return undefined
|
|
147
|
+
return nil if str.nil?
|
|
148
|
+
|
|
149
|
+
pos = str.index(chars)
|
|
150
|
+
if pos.nil?
|
|
151
|
+
str
|
|
152
|
+
else
|
|
153
|
+
str[(pos + chars.length)..]
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Lowercase a string
|
|
158
|
+
# @param str - String to evaluate
|
|
159
|
+
# @returns Lowercase string
|
|
160
|
+
def lowercase(str)
|
|
161
|
+
# undefined inputs always return undefined
|
|
162
|
+
return nil if str.nil?
|
|
163
|
+
|
|
164
|
+
str.downcase
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Uppercase a string
|
|
168
|
+
# @param str - String to evaluate
|
|
169
|
+
# @returns Uppercase string
|
|
170
|
+
def uppercase(str)
|
|
171
|
+
# undefined inputs always return undefined
|
|
172
|
+
return nil if str.nil?
|
|
173
|
+
|
|
174
|
+
str.upcase
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# length of a string
|
|
178
|
+
# @param str - string
|
|
179
|
+
# @returns The number of characters in the string
|
|
180
|
+
def length(str)
|
|
181
|
+
# undefined inputs always return undefined
|
|
182
|
+
return nil if str.nil?
|
|
183
|
+
|
|
184
|
+
Utils.string_to_array(str).length
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Normalize and trim whitespace within a string
|
|
188
|
+
# @param str - string to be trimmed
|
|
189
|
+
# @returns trimmed string
|
|
190
|
+
def trim(str)
|
|
191
|
+
# undefined inputs always return undefined
|
|
192
|
+
return nil if str.nil?
|
|
193
|
+
|
|
194
|
+
# normalize whitespace
|
|
195
|
+
result = str.gsub(/[ \t\n\r]+/, ' ')
|
|
196
|
+
if result[0] == ' '
|
|
197
|
+
# strip leading space
|
|
198
|
+
result = result[1..]
|
|
199
|
+
end
|
|
200
|
+
if result[-1] == ' '
|
|
201
|
+
# strip trailing space
|
|
202
|
+
result = result[0...-1]
|
|
203
|
+
end
|
|
204
|
+
result
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Pad a string to a minimum width by adding characters to the start or end
|
|
208
|
+
# @param str - string to be padded
|
|
209
|
+
# @param width - the minimum width; +ve pads to the right, -ve pads to the left
|
|
210
|
+
# @param char - the pad character(s); defaults to ' '
|
|
211
|
+
# @returns padded string
|
|
212
|
+
def pad(str, width, char)
|
|
213
|
+
# undefined inputs always return undefined
|
|
214
|
+
return nil if str.nil?
|
|
215
|
+
|
|
216
|
+
char = ' ' if char.nil? || char.empty?
|
|
217
|
+
|
|
218
|
+
width = width.truncate
|
|
219
|
+
pad_length = width.abs - length(str)
|
|
220
|
+
if pad_length.positive?
|
|
221
|
+
padding = char * pad_length
|
|
222
|
+
padding = substring(padding, 0, pad_length) if char.length > 1
|
|
223
|
+
if width.positive?
|
|
224
|
+
str + padding
|
|
225
|
+
else
|
|
226
|
+
padding + str
|
|
227
|
+
end
|
|
228
|
+
else
|
|
229
|
+
str
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Evaluate the matcher function against the str arg
|
|
234
|
+
#
|
|
235
|
+
# @param matcher - matching function (native or lambda)
|
|
236
|
+
# @param str - the string to match against
|
|
237
|
+
# @returns structure that represents the match(es)
|
|
238
|
+
def evaluate_matcher(matcher, str = nil)
|
|
239
|
+
result = call_function(matcher, [str])
|
|
240
|
+
if Utils.truthy?(result) && !(result.kind_of?(Hash) && (result['start'].kind_of?(Numeric) || result['end'] == 'number' ||
|
|
241
|
+
result['groups'].kind_of?(Array) || Utils.function?(result['next'])))
|
|
242
|
+
# the matcher function didn't return the correct structure
|
|
243
|
+
raise Jsonata::Error.new('T1010')
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
result
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Tests if the str contains the token
|
|
250
|
+
# @param str - string to test
|
|
251
|
+
# @param token - substring or regex to find
|
|
252
|
+
# @returns true if str contains token
|
|
253
|
+
def contains(str, token)
|
|
254
|
+
# undefined inputs always return undefined
|
|
255
|
+
return nil if str.nil? || token.nil?
|
|
256
|
+
|
|
257
|
+
if token.kind_of?(String)
|
|
258
|
+
str.include?(token)
|
|
259
|
+
else
|
|
260
|
+
matches = evaluate_matcher(token, str)
|
|
261
|
+
!matches.nil?
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Match a string with a regex returning an array of object containing details of each match
|
|
266
|
+
# @param str - string
|
|
267
|
+
# @param regex - the regex applied to the string
|
|
268
|
+
# @param limit - max number of matches to return
|
|
269
|
+
# @returns The array of match objects
|
|
270
|
+
def match(str, regex, limit)
|
|
271
|
+
# undefined inputs always return undefined
|
|
272
|
+
return nil if str.nil?
|
|
273
|
+
|
|
274
|
+
# limit, if specified, must be a non-negative number
|
|
275
|
+
raise Jsonata::Error.new('D3040', :value => limit, :index => 3) if !limit.nil? && limit.negative?
|
|
276
|
+
|
|
277
|
+
result = create_sequence
|
|
278
|
+
|
|
279
|
+
if limit.nil? || limit.positive?
|
|
280
|
+
count = 0
|
|
281
|
+
matches = evaluate_matcher(regex, str)
|
|
282
|
+
unless matches.nil?
|
|
283
|
+
while !matches.nil? && (limit.nil? || count < limit)
|
|
284
|
+
result.push({
|
|
285
|
+
'match' => matches['match'],
|
|
286
|
+
'index' => matches['start'],
|
|
287
|
+
'groups' => matches['groups']
|
|
288
|
+
})
|
|
289
|
+
matches = evaluate_matcher(matches['next'])
|
|
290
|
+
count += 1
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
result
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Match a string with a regex returning an array of object containing details of each match
|
|
299
|
+
# @param str - string
|
|
300
|
+
# @param pattern - the substring/regex applied to the string
|
|
301
|
+
# @param replacement - text to replace the matched substrings
|
|
302
|
+
# @param limit - max number of matches to return
|
|
303
|
+
# @returns The array of match objects
|
|
304
|
+
def replace(str, pattern, replacement, limit)
|
|
305
|
+
# undefined inputs always return undefined
|
|
306
|
+
return nil if str.nil?
|
|
307
|
+
|
|
308
|
+
# pattern cannot be an empty string
|
|
309
|
+
raise Jsonata::Error.new('D3010', :value => pattern, :index => 2) if pattern == ''
|
|
310
|
+
|
|
311
|
+
# limit, if specified, must be a non-negative number
|
|
312
|
+
raise Jsonata::Error.new('D3011', :value => limit, :index => 4) if !limit.nil? && limit.negative?
|
|
313
|
+
|
|
314
|
+
replacer = if replacement.kind_of?(String)
|
|
315
|
+
lambda do |regex_match|
|
|
316
|
+
substitute = +''
|
|
317
|
+
# scan forward, copying the replacement text into the substitute string
|
|
318
|
+
# and replace any occurrence of $n with the values matched by the regex
|
|
319
|
+
position = 0
|
|
320
|
+
index = replacement.index('$', position)
|
|
321
|
+
while !index.nil? && position < replacement.length
|
|
322
|
+
substitute << replacement[position...index]
|
|
323
|
+
position = index + 1
|
|
324
|
+
dollar_val = replacement[position] || ''
|
|
325
|
+
if dollar_val == '$'
|
|
326
|
+
# literal $
|
|
327
|
+
substitute << '$'
|
|
328
|
+
position += 1
|
|
329
|
+
elsif dollar_val == '0'
|
|
330
|
+
substitute << regex_match['match']
|
|
331
|
+
position += 1
|
|
332
|
+
else
|
|
333
|
+
groups = regex_match['groups']
|
|
334
|
+
max_digits = if groups.empty?
|
|
335
|
+
# no sub-matches; any $ followed by a digit will be replaced by an empty string
|
|
336
|
+
1
|
|
337
|
+
else
|
|
338
|
+
# max number of digits to parse following the $
|
|
339
|
+
(Math.log(groups.length) * 0.4342944819032518).floor + 1
|
|
340
|
+
end
|
|
341
|
+
index = Utils.parse_int(replacement[position, max_digits])
|
|
342
|
+
if max_digits > 1 && !index.nil? && index > groups.length
|
|
343
|
+
index = Utils.parse_int(replacement[position, max_digits - 1])
|
|
344
|
+
end
|
|
345
|
+
if index.nil?
|
|
346
|
+
# not a capture group, treat the $ as literal
|
|
347
|
+
substitute << '$'
|
|
348
|
+
else
|
|
349
|
+
if !groups.empty? && index >= 1
|
|
350
|
+
submatch = groups[index - 1]
|
|
351
|
+
substitute << submatch unless submatch.nil?
|
|
352
|
+
end
|
|
353
|
+
position += index.to_s.length
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
index = replacement.index('$', position)
|
|
357
|
+
end
|
|
358
|
+
substitute << (replacement[position..] || '')
|
|
359
|
+
substitute
|
|
360
|
+
end
|
|
361
|
+
else
|
|
362
|
+
replacement
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
result = +''
|
|
366
|
+
position = 0
|
|
367
|
+
|
|
368
|
+
if limit.nil? || limit.positive?
|
|
369
|
+
count = 0
|
|
370
|
+
if pattern.kind_of?(String)
|
|
371
|
+
index = str.index(pattern, position)
|
|
372
|
+
while !index.nil? && (limit.nil? || count < limit)
|
|
373
|
+
result << str[position...index]
|
|
374
|
+
result << replacement.to_s
|
|
375
|
+
position = index + pattern.length
|
|
376
|
+
count += 1
|
|
377
|
+
index = str.index(pattern, position)
|
|
378
|
+
end
|
|
379
|
+
result << (str[position..] || '')
|
|
380
|
+
else
|
|
381
|
+
matches = evaluate_matcher(pattern, str)
|
|
382
|
+
if matches.nil?
|
|
383
|
+
result = str
|
|
384
|
+
else
|
|
385
|
+
while !matches.nil? && (limit.nil? || count < limit)
|
|
386
|
+
result << (str[position...matches['start']] || '')
|
|
387
|
+
replaced_with = call_function(replacer, [matches])
|
|
388
|
+
# check replacedWith is a string
|
|
389
|
+
unless replaced_with.kind_of?(String)
|
|
390
|
+
# not a string - throw error
|
|
391
|
+
raise Jsonata::Error.new('D3012', :value => replaced_with)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
result << replaced_with
|
|
395
|
+
position = matches['start'] + matches['match'].length
|
|
396
|
+
count += 1
|
|
397
|
+
matches = evaluate_matcher(matches['next'])
|
|
398
|
+
end
|
|
399
|
+
result << (str[position..] || '')
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
else
|
|
403
|
+
result = str
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
result
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
# Base64 encode a string
|
|
410
|
+
# @param str - string
|
|
411
|
+
# @returns Base 64 encoding of the binary data
|
|
412
|
+
def base64encode(str)
|
|
413
|
+
# undefined inputs always return undefined
|
|
414
|
+
return nil if str.nil?
|
|
415
|
+
|
|
416
|
+
# equivalent of Buffer.from(str, 'binary').toString('base64')
|
|
417
|
+
bytes = str.encode('UTF-16LE').unpack('v*').map { |unit| unit & 0xFF }
|
|
418
|
+
[bytes.pack('C*')].pack('m0')
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Base64 decode a string
|
|
422
|
+
# @param str - string
|
|
423
|
+
# @returns Base 64 encoding of the binary data
|
|
424
|
+
def base64decode(str)
|
|
425
|
+
# undefined inputs always return undefined
|
|
426
|
+
return nil if str.nil?
|
|
427
|
+
|
|
428
|
+
# equivalent of Buffer.from(str, 'base64').toString('binary')
|
|
429
|
+
str.unpack1('m').bytes.pack('U*')
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Encode a string into a component for a url
|
|
433
|
+
# @param str - String to encode
|
|
434
|
+
# @returns Encoded string
|
|
435
|
+
def encode_url_component(str)
|
|
436
|
+
# undefined inputs always return undefined
|
|
437
|
+
return nil if str.nil?
|
|
438
|
+
|
|
439
|
+
# Catch URIErrors when URI sequence is malformed
|
|
440
|
+
Utils.encode_uri(str, Utils::URI_UNRESERVED)
|
|
441
|
+
rescue Utils::URIError
|
|
442
|
+
raise Jsonata::Error.new('D3140', :value => str, :function_name => 'encodeUrlComponent')
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Encode a string into a url
|
|
446
|
+
# @param str - String to encode
|
|
447
|
+
# @returns Encoded string
|
|
448
|
+
def encode_url(str)
|
|
449
|
+
# undefined inputs always return undefined
|
|
450
|
+
return nil if str.nil?
|
|
451
|
+
|
|
452
|
+
# Catch URIErrors when URI sequence is malformed
|
|
453
|
+
Utils.encode_uri(str, Utils::URI_UNRESERVED + Utils::URI_RESERVED)
|
|
454
|
+
rescue Utils::URIError
|
|
455
|
+
raise Jsonata::Error.new('D3140', :value => str, :function_name => 'encodeUrl')
|
|
456
|
+
end
|
|
457
|
+
|
|
458
|
+
# Decode a string from a component for a url
|
|
459
|
+
# @param str - String to decode
|
|
460
|
+
# @returns Decoded string
|
|
461
|
+
def decode_url_component(str)
|
|
462
|
+
# undefined inputs always return undefined
|
|
463
|
+
return nil if str.nil?
|
|
464
|
+
|
|
465
|
+
# Catch URIErrors when URI sequence is malformed
|
|
466
|
+
Utils.decode_uri(str, '')
|
|
467
|
+
rescue Utils::URIError
|
|
468
|
+
raise Jsonata::Error.new('D3140', :value => str, :function_name => 'decodeUrlComponent')
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# Decode a string from a url
|
|
472
|
+
# @param str - String to decode
|
|
473
|
+
# @returns Decoded string
|
|
474
|
+
def decode_url(str)
|
|
475
|
+
# undefined inputs always return undefined
|
|
476
|
+
return nil if str.nil?
|
|
477
|
+
|
|
478
|
+
# Catch URIErrors when URI sequence is malformed
|
|
479
|
+
Utils.decode_uri(str, Utils::URI_RESERVED)
|
|
480
|
+
rescue Utils::URIError
|
|
481
|
+
raise Jsonata::Error.new('D3140', :value => str, :function_name => 'decodeUrl')
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
# Split a string into an array of substrings
|
|
485
|
+
# @param str - string
|
|
486
|
+
# @param separator - the token or regex that splits the string
|
|
487
|
+
# @param limit - max number of substrings
|
|
488
|
+
# @returns The array of string
|
|
489
|
+
def split(str, separator, limit)
|
|
490
|
+
# undefined inputs always return undefined
|
|
491
|
+
return nil if str.nil?
|
|
492
|
+
|
|
493
|
+
# limit, if specified, must be a non-negative number
|
|
494
|
+
raise Jsonata::Error.new('D3020', :value => limit, :index => 3) if !limit.nil? && limit.negative?
|
|
495
|
+
|
|
496
|
+
result = []
|
|
497
|
+
|
|
498
|
+
if limit.nil? || limit.positive?
|
|
499
|
+
if separator.kind_of?(String)
|
|
500
|
+
result = Utils.js_split(str, separator, limit)
|
|
501
|
+
else
|
|
502
|
+
count = 0
|
|
503
|
+
matches = evaluate_matcher(separator, str)
|
|
504
|
+
if matches.nil?
|
|
505
|
+
result.push(str)
|
|
506
|
+
else
|
|
507
|
+
start = 0
|
|
508
|
+
while !matches.nil? && (limit.nil? || count < limit)
|
|
509
|
+
result.push(str[start...matches['start']] || '')
|
|
510
|
+
start = matches['end']
|
|
511
|
+
matches = evaluate_matcher(matches['next'])
|
|
512
|
+
count += 1
|
|
513
|
+
end
|
|
514
|
+
result.push(str[start..] || '') if limit.nil? || count < limit
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
result
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# Join an array of strings
|
|
523
|
+
# @param strs - array of string
|
|
524
|
+
# @param separator - the token that splits the string
|
|
525
|
+
# @returns The concatenated string
|
|
526
|
+
def join(strs, separator)
|
|
527
|
+
# undefined inputs always return undefined
|
|
528
|
+
return nil if strs.nil?
|
|
529
|
+
|
|
530
|
+
# if separator is not specified, default to empty string
|
|
531
|
+
separator = '' if separator.nil?
|
|
532
|
+
|
|
533
|
+
strs.join(separator)
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Formats a number into a decimal string representation using XPath 3.1 F&O fn:format-number spec
|
|
537
|
+
# @param value - number to format
|
|
538
|
+
# @param picture - picture string definition
|
|
539
|
+
# @param options - override locale defaults
|
|
540
|
+
# @returns The formatted string
|
|
541
|
+
def format_number(value, picture, options)
|
|
542
|
+
# undefined inputs always return undefined
|
|
543
|
+
return nil if value.nil?
|
|
544
|
+
|
|
545
|
+
defaults = {
|
|
546
|
+
'decimal-separator' => '.',
|
|
547
|
+
'grouping-separator' => ',',
|
|
548
|
+
'exponent-separator' => 'e',
|
|
549
|
+
'infinity' => 'Infinity',
|
|
550
|
+
'minus-sign' => '-',
|
|
551
|
+
'NaN' => 'NaN',
|
|
552
|
+
'percent' => '%',
|
|
553
|
+
'per-mille' => "\u2030",
|
|
554
|
+
'zero-digit' => '0',
|
|
555
|
+
'digit' => '#',
|
|
556
|
+
'pattern-separator' => ';'
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
# if `options` is specified, then its entries override defaults
|
|
560
|
+
properties = defaults
|
|
561
|
+
unless options.nil?
|
|
562
|
+
Utils.keys(options).each do |key|
|
|
563
|
+
properties[key] = options[key]
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
zero_char_code = properties['zero-digit'].ord
|
|
568
|
+
decimal_digit_family = (zero_char_code...(zero_char_code + 10)).map do |ii|
|
|
569
|
+
ii.chr(Encoding::UTF_8)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
active_chars = decimal_digit_family + [properties['decimal-separator'], properties['exponent-separator'],
|
|
573
|
+
properties['grouping-separator'], properties['digit'],
|
|
574
|
+
properties['pattern-separator']]
|
|
575
|
+
|
|
576
|
+
sub_pictures = Utils.js_split(picture, properties['pattern-separator'])
|
|
577
|
+
|
|
578
|
+
raise Jsonata::Error.new('D3080') if sub_pictures.length > 2
|
|
579
|
+
|
|
580
|
+
split_parts = lambda do |subpicture|
|
|
581
|
+
prefix = (lambda do
|
|
582
|
+
subpicture.each_char.with_index do |ch, ii|
|
|
583
|
+
if active_chars.include?(ch) && ch != properties['exponent-separator']
|
|
584
|
+
return subpicture[0, ii]
|
|
585
|
+
end
|
|
586
|
+
end
|
|
587
|
+
''
|
|
588
|
+
end).call
|
|
589
|
+
suffix = (lambda do
|
|
590
|
+
(subpicture.length - 1).downto(0) do |ii|
|
|
591
|
+
ch = subpicture[ii]
|
|
592
|
+
if active_chars.include?(ch) && ch != properties['exponent-separator']
|
|
593
|
+
return subpicture[(ii + 1)..]
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
''
|
|
597
|
+
end).call
|
|
598
|
+
active_part = subpicture[prefix.length...(subpicture.length - suffix.length)]
|
|
599
|
+
exponent_position = subpicture.index(properties['exponent-separator'], prefix.length)
|
|
600
|
+
if exponent_position.nil? || exponent_position > subpicture.length - suffix.length
|
|
601
|
+
mantissa_part = active_part
|
|
602
|
+
exponent_part = nil
|
|
603
|
+
else
|
|
604
|
+
mantissa_part = active_part[0, exponent_position]
|
|
605
|
+
exponent_part = active_part[(exponent_position + 1)..] || ''
|
|
606
|
+
end
|
|
607
|
+
decimal_position = mantissa_part.index(properties['decimal-separator'])
|
|
608
|
+
if decimal_position.nil?
|
|
609
|
+
integer_part = mantissa_part
|
|
610
|
+
fractional_part = suffix
|
|
611
|
+
else
|
|
612
|
+
integer_part = mantissa_part[0, decimal_position]
|
|
613
|
+
fractional_part = mantissa_part[(decimal_position + 1)..]
|
|
614
|
+
end
|
|
615
|
+
{
|
|
616
|
+
:prefix => prefix,
|
|
617
|
+
:suffix => suffix,
|
|
618
|
+
:active_part => active_part,
|
|
619
|
+
:mantissa_part => mantissa_part,
|
|
620
|
+
:exponent_part => exponent_part,
|
|
621
|
+
:integer_part => integer_part,
|
|
622
|
+
:fractional_part => fractional_part,
|
|
623
|
+
:subpicture => subpicture
|
|
624
|
+
}
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
# validate the picture string, F&O 4.7.3
|
|
628
|
+
validate = lambda do |parts|
|
|
629
|
+
error = nil
|
|
630
|
+
subpicture = parts[:subpicture]
|
|
631
|
+
decimal_pos = subpicture.index(properties['decimal-separator'])
|
|
632
|
+
error = 'D3081' if decimal_pos != subpicture.rindex(properties['decimal-separator'])
|
|
633
|
+
error = 'D3082' if subpicture.index(properties['percent']) != subpicture.rindex(properties['percent'])
|
|
634
|
+
error = 'D3083' if subpicture.index(properties['per-mille']) != subpicture.rindex(properties['per-mille'])
|
|
635
|
+
if subpicture.include?(properties['percent']) && subpicture.include?(properties['per-mille'])
|
|
636
|
+
error = 'D3084'
|
|
637
|
+
end
|
|
638
|
+
valid = parts[:mantissa_part].each_char.any? do |ch|
|
|
639
|
+
decimal_digit_family.include?(ch) || ch == properties['digit']
|
|
640
|
+
end
|
|
641
|
+
error = 'D3085' unless valid
|
|
642
|
+
char_types = parts[:active_part].each_char.map do |char|
|
|
643
|
+
active_chars.include?(char) ? 'a' : 'p'
|
|
644
|
+
end.join
|
|
645
|
+
error = 'D3086' if char_types.include?('p')
|
|
646
|
+
if decimal_pos.nil?
|
|
647
|
+
error = 'D3088' if parts[:integer_part][-1] == properties['grouping-separator']
|
|
648
|
+
elsif (decimal_pos.positive? && subpicture[decimal_pos - 1] == properties['grouping-separator']) ||
|
|
649
|
+
subpicture[decimal_pos + 1] == properties['grouping-separator']
|
|
650
|
+
error = 'D3087'
|
|
651
|
+
end
|
|
652
|
+
error = 'D3089' if subpicture.include?(properties['grouping-separator'] + properties['grouping-separator'])
|
|
653
|
+
optional_digit_pos = parts[:integer_part].index(properties['digit'])
|
|
654
|
+
if !optional_digit_pos.nil? && parts[:integer_part][0, optional_digit_pos].each_char.any? { |char| decimal_digit_family.include?(char) }
|
|
655
|
+
error = 'D3090'
|
|
656
|
+
end
|
|
657
|
+
optional_digit_pos = parts[:fractional_part].rindex(properties['digit'])
|
|
658
|
+
if !optional_digit_pos.nil? && parts[:fractional_part][optional_digit_pos..].each_char.any? { |char| decimal_digit_family.include?(char) }
|
|
659
|
+
error = 'D3091'
|
|
660
|
+
end
|
|
661
|
+
exponent_exists = parts[:exponent_part].kind_of?(String)
|
|
662
|
+
if exponent_exists && !parts[:exponent_part].empty? &&
|
|
663
|
+
(subpicture.include?(properties['percent']) || subpicture.include?(properties['per-mille']))
|
|
664
|
+
error = 'D3092'
|
|
665
|
+
end
|
|
666
|
+
if exponent_exists && (parts[:exponent_part].empty? || parts[:exponent_part].each_char.any? { |char| !decimal_digit_family.include?(char) })
|
|
667
|
+
error = 'D3093'
|
|
668
|
+
end
|
|
669
|
+
raise Jsonata::Error.new(error) if error
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# analyse the picture string, F&O 4.7.4
|
|
673
|
+
analyse = lambda do |parts|
|
|
674
|
+
get_grouping_positions = lambda do |part, to_left = false|
|
|
675
|
+
positions = []
|
|
676
|
+
grouping_position = part.index(properties['grouping-separator'])
|
|
677
|
+
until grouping_position.nil?
|
|
678
|
+
chars_to_the_right = (to_left ? part[0, grouping_position] : part[grouping_position..]).each_char.count do |char|
|
|
679
|
+
decimal_digit_family.include?(char) || char == properties['digit']
|
|
680
|
+
end
|
|
681
|
+
positions.push(chars_to_the_right)
|
|
682
|
+
grouping_position = parts[:integer_part].index(properties['grouping-separator'], grouping_position + 1)
|
|
683
|
+
end
|
|
684
|
+
positions
|
|
685
|
+
end
|
|
686
|
+
integer_part_grouping_positions = get_grouping_positions.call(parts[:integer_part])
|
|
687
|
+
regular = lambda do |indexes|
|
|
688
|
+
# are the grouping positions regular? i.e. same interval between each of them
|
|
689
|
+
return 0 if indexes.empty?
|
|
690
|
+
|
|
691
|
+
# find the greatest common divisor of all the positions
|
|
692
|
+
factor = indexes.reduce { |a, b| a.gcd(b) }
|
|
693
|
+
# is every position separated by this divisor? If so, it's regular
|
|
694
|
+
(1..indexes.length).each do |index|
|
|
695
|
+
return 0 unless indexes.include?(index * factor)
|
|
696
|
+
end
|
|
697
|
+
factor
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
regular_grouping = regular.call(integer_part_grouping_positions)
|
|
701
|
+
fractional_part_grouping_positions = get_grouping_positions.call(parts[:fractional_part], true)
|
|
702
|
+
|
|
703
|
+
minimum_integer_part_size = parts[:integer_part].each_char.count { |char| decimal_digit_family.include?(char) }
|
|
704
|
+
scaling_factor = minimum_integer_part_size
|
|
705
|
+
|
|
706
|
+
fractional_part_array = parts[:fractional_part].chars
|
|
707
|
+
minimum_factional_part_size = fractional_part_array.count { |char| decimal_digit_family.include?(char) }
|
|
708
|
+
maximum_factional_part_size = fractional_part_array.count do |char|
|
|
709
|
+
decimal_digit_family.include?(char) || char == properties['digit']
|
|
710
|
+
end
|
|
711
|
+
exponent_present = parts[:exponent_part].kind_of?(String)
|
|
712
|
+
if minimum_integer_part_size.zero? && maximum_factional_part_size.zero?
|
|
713
|
+
if exponent_present
|
|
714
|
+
minimum_factional_part_size = 1
|
|
715
|
+
maximum_factional_part_size = 1
|
|
716
|
+
else
|
|
717
|
+
minimum_integer_part_size = 1
|
|
718
|
+
end
|
|
719
|
+
end
|
|
720
|
+
if exponent_present && minimum_integer_part_size.zero? && parts[:integer_part].include?(properties['digit'])
|
|
721
|
+
minimum_integer_part_size = 1
|
|
722
|
+
end
|
|
723
|
+
minimum_factional_part_size = 1 if minimum_integer_part_size.zero? && minimum_factional_part_size.zero?
|
|
724
|
+
minimum_exponent_size = 0
|
|
725
|
+
if exponent_present
|
|
726
|
+
minimum_exponent_size = parts[:exponent_part].each_char.count { |char| decimal_digit_family.include?(char) }
|
|
727
|
+
end
|
|
728
|
+
|
|
729
|
+
{
|
|
730
|
+
:integer_part_grouping_positions => integer_part_grouping_positions,
|
|
731
|
+
:regular_grouping => regular_grouping,
|
|
732
|
+
:minimum_integer_part_size => minimum_integer_part_size,
|
|
733
|
+
:scaling_factor => scaling_factor,
|
|
734
|
+
:prefix => parts[:prefix],
|
|
735
|
+
:fractional_part_grouping_positions => fractional_part_grouping_positions,
|
|
736
|
+
:minimum_factional_part_size => minimum_factional_part_size,
|
|
737
|
+
:maximum_factional_part_size => maximum_factional_part_size,
|
|
738
|
+
:minimum_exponent_size => minimum_exponent_size,
|
|
739
|
+
:suffix => parts[:suffix],
|
|
740
|
+
:picture => parts[:subpicture]
|
|
741
|
+
}
|
|
742
|
+
end
|
|
743
|
+
|
|
744
|
+
parts = sub_pictures.map(&split_parts)
|
|
745
|
+
parts.each(&validate)
|
|
746
|
+
|
|
747
|
+
variables = parts.map(&analyse)
|
|
748
|
+
|
|
749
|
+
minus_sign = properties['minus-sign']
|
|
750
|
+
zero_digit = properties['zero-digit']
|
|
751
|
+
decimal_separator = properties['decimal-separator']
|
|
752
|
+
grouping_separator = properties['grouping-separator']
|
|
753
|
+
|
|
754
|
+
if variables.length == 1
|
|
755
|
+
variables.push(Marshal.load(Marshal.dump(variables[0])))
|
|
756
|
+
variables[1][:prefix] = minus_sign + variables[1][:prefix]
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# TODO cache the result of the analysis
|
|
760
|
+
|
|
761
|
+
# format the number
|
|
762
|
+
# bullet 1: TODO: NaN - not sure we'd ever get this in JSON
|
|
763
|
+
# bullet 2:
|
|
764
|
+
pic = if value >= 0
|
|
765
|
+
variables[0]
|
|
766
|
+
else
|
|
767
|
+
variables[1]
|
|
768
|
+
end
|
|
769
|
+
# bullet 3:
|
|
770
|
+
adjusted_number = if pic[:picture].include?(properties['percent'])
|
|
771
|
+
value * 100
|
|
772
|
+
elsif pic[:picture].include?(properties['per-mille'])
|
|
773
|
+
value * 1000
|
|
774
|
+
else
|
|
775
|
+
value
|
|
776
|
+
end
|
|
777
|
+
# bullet 4:
|
|
778
|
+
# TODO: infinity - not sure we'd ever get this in JSON
|
|
779
|
+
# bullet 5:
|
|
780
|
+
exponent = nil
|
|
781
|
+
if pic[:minimum_exponent_size].zero?
|
|
782
|
+
mantissa = adjusted_number
|
|
783
|
+
else
|
|
784
|
+
# mantissa * 10^exponent = adjustedNumber
|
|
785
|
+
max_mantissa = 10.0**pic[:scaling_factor]
|
|
786
|
+
min_mantissa = 10.0**(pic[:scaling_factor] - 1)
|
|
787
|
+
mantissa = adjusted_number.to_f
|
|
788
|
+
exponent = 0
|
|
789
|
+
# Compare magnitudes; previously `mantissa < minMantissa` was
|
|
790
|
+
# true for every negative mantissa and `0 * 10 === 0` never
|
|
791
|
+
# crossed the threshold, producing an infinite loop on zero
|
|
792
|
+
# and negative inputs (#785). For zero the desired exponent is
|
|
793
|
+
# simply zero - the XPath F&O Bullet 5 says "if N is zero, set
|
|
794
|
+
# M to zero and E to zero".
|
|
795
|
+
unless mantissa.zero?
|
|
796
|
+
while mantissa.abs < min_mantissa
|
|
797
|
+
mantissa *= 10
|
|
798
|
+
exponent -= 1
|
|
799
|
+
end
|
|
800
|
+
while mantissa.abs > max_mantissa
|
|
801
|
+
mantissa /= 10
|
|
802
|
+
exponent += 1
|
|
803
|
+
end
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
# bullet 6:
|
|
807
|
+
rounded_number = round(mantissa, pic[:maximum_factional_part_size])
|
|
808
|
+
# bullet 7:
|
|
809
|
+
make_string = lambda do |val, dp|
|
|
810
|
+
str = Utils.to_fixed(val.abs, dp)
|
|
811
|
+
if zero_digit != '0'
|
|
812
|
+
str = str.each_char.map do |digit|
|
|
813
|
+
if digit.between?('0', '9')
|
|
814
|
+
decimal_digit_family[digit.ord - 48]
|
|
815
|
+
else
|
|
816
|
+
digit
|
|
817
|
+
end
|
|
818
|
+
end.join
|
|
819
|
+
end
|
|
820
|
+
str
|
|
821
|
+
end
|
|
822
|
+
string_value = make_string.call(rounded_number, pic[:maximum_factional_part_size])
|
|
823
|
+
decimal_pos = string_value.index('.')
|
|
824
|
+
string_value = if decimal_pos.nil?
|
|
825
|
+
string_value + decimal_separator
|
|
826
|
+
else
|
|
827
|
+
string_value.sub('.', decimal_separator)
|
|
828
|
+
end
|
|
829
|
+
string_value = string_value[1..] while string_value[0] == zero_digit
|
|
830
|
+
string_value = string_value[0...-1] while string_value[-1] == zero_digit
|
|
831
|
+
# bullets 8 & 9:
|
|
832
|
+
decimal_pos = string_value.index(decimal_separator)
|
|
833
|
+
pad_left = pic[:minimum_integer_part_size] - decimal_pos
|
|
834
|
+
pad_right = pic[:minimum_factional_part_size] - (string_value.length - decimal_pos - 1)
|
|
835
|
+
string_value = (pad_left.positive? ? zero_digit * pad_left : '') + string_value
|
|
836
|
+
string_value += (pad_right.positive? ? zero_digit * pad_right : '')
|
|
837
|
+
decimal_pos = string_value.index(decimal_separator)
|
|
838
|
+
# bullet 10:
|
|
839
|
+
if pic[:regular_grouping].positive?
|
|
840
|
+
group_count = ((decimal_pos - 1) / pic[:regular_grouping]).floor
|
|
841
|
+
(1..group_count).each do |group|
|
|
842
|
+
pos = decimal_pos - (group * pic[:regular_grouping])
|
|
843
|
+
string_value = [Utils.js_slice(string_value, 0, pos), grouping_separator, Utils.js_slice(string_value, pos)].join
|
|
844
|
+
end
|
|
845
|
+
else
|
|
846
|
+
pic[:integer_part_grouping_positions].each do |pos|
|
|
847
|
+
string_value = [Utils.js_slice(string_value, 0, decimal_pos - pos), grouping_separator,
|
|
848
|
+
Utils.js_slice(string_value, decimal_pos - pos)].join
|
|
849
|
+
decimal_pos += 1
|
|
850
|
+
end
|
|
851
|
+
end
|
|
852
|
+
# bullet 11:
|
|
853
|
+
decimal_pos = string_value.index(decimal_separator)
|
|
854
|
+
pic[:fractional_part_grouping_positions].each do |pos|
|
|
855
|
+
string_value = [Utils.js_slice(string_value, 0, pos + decimal_pos + 1), grouping_separator,
|
|
856
|
+
Utils.js_slice(string_value, pos + decimal_pos + 1)].join
|
|
857
|
+
end
|
|
858
|
+
# bullet 12:
|
|
859
|
+
decimal_pos = string_value.index(decimal_separator)
|
|
860
|
+
if !pic[:picture].include?(decimal_separator) || decimal_pos == string_value.length - 1
|
|
861
|
+
string_value = string_value[0...-1]
|
|
862
|
+
end
|
|
863
|
+
# bullet 13:
|
|
864
|
+
unless exponent.nil?
|
|
865
|
+
string_exponent = make_string.call(exponent, 0)
|
|
866
|
+
pad_left = pic[:minimum_exponent_size] - string_exponent.length
|
|
867
|
+
string_exponent = (zero_digit * pad_left) + string_exponent if pad_left.positive?
|
|
868
|
+
string_value = string_value + properties['exponent-separator'] + (exponent.negative? ? minus_sign : '') + string_exponent
|
|
869
|
+
end
|
|
870
|
+
# bullet 14:
|
|
871
|
+
pic[:prefix] + string_value + pic[:suffix]
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
# Converts a number to a string using a specified number base
|
|
875
|
+
# @param value - the number to convert
|
|
876
|
+
# @param radix - the number base; must be between 2 and 36. Defaults to 10
|
|
877
|
+
# @returns the converted string
|
|
878
|
+
def format_base(value, radix)
|
|
879
|
+
# undefined inputs always return undefined
|
|
880
|
+
return nil if value.nil?
|
|
881
|
+
|
|
882
|
+
value = round(value)
|
|
883
|
+
|
|
884
|
+
radix = if radix.nil?
|
|
885
|
+
10
|
|
886
|
+
else
|
|
887
|
+
round(radix)
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
raise Jsonata::Error.new('D3100', :value => radix) if radix < 2 || radix > 36
|
|
891
|
+
|
|
892
|
+
value.to_i.to_s(radix.to_i)
|
|
893
|
+
end
|
|
894
|
+
|
|
895
|
+
# Cast argument to number
|
|
896
|
+
# @param arg - Argument
|
|
897
|
+
# @returns numeric value of argument
|
|
898
|
+
def number(arg)
|
|
899
|
+
# undefined inputs always return undefined
|
|
900
|
+
return nil if arg.nil?
|
|
901
|
+
|
|
902
|
+
if arg.kind_of?(Numeric)
|
|
903
|
+
# already a number
|
|
904
|
+
arg
|
|
905
|
+
elsif arg.kind_of?(String) && /\A-?[0-9]+(\.[0-9]+)?([Ee][-+]?[0-9]+)?\z/.match?(arg) && Float(arg).finite?
|
|
906
|
+
Utils.normalize_number(Float(arg))
|
|
907
|
+
elsif arg.kind_of?(String) && /\A(0[xX][0-9A-Fa-f]+)|(0[oO][0-7]+)|(0[bB][0-1]+)\z/.match?(arg)
|
|
908
|
+
Utils.js_number(arg)
|
|
909
|
+
elsif arg == true
|
|
910
|
+
# boolean true casts to 1
|
|
911
|
+
1
|
|
912
|
+
elsif arg == false
|
|
913
|
+
# boolean false casts to 0
|
|
914
|
+
0
|
|
915
|
+
else
|
|
916
|
+
raise Jsonata::Error.new('D3030', :value => arg, :index => 1)
|
|
917
|
+
end
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# Absolute value of a number
|
|
921
|
+
# @param arg - Argument
|
|
922
|
+
# @returns absolute value of argument
|
|
923
|
+
def abs(arg)
|
|
924
|
+
# undefined inputs always return undefined
|
|
925
|
+
return nil if arg.nil?
|
|
926
|
+
|
|
927
|
+
arg.abs
|
|
928
|
+
end
|
|
929
|
+
|
|
930
|
+
# Rounds a number down to integer
|
|
931
|
+
# @param arg - Argument
|
|
932
|
+
# @returns rounded integer
|
|
933
|
+
def floor(arg)
|
|
934
|
+
# undefined inputs always return undefined
|
|
935
|
+
return nil if arg.nil?
|
|
936
|
+
|
|
937
|
+
Utils.normalize_number(arg.to_f.floor.to_f)
|
|
938
|
+
end
|
|
939
|
+
|
|
940
|
+
# Rounds a number up to integer
|
|
941
|
+
# @param arg - Argument
|
|
942
|
+
# @returns rounded integer
|
|
943
|
+
def ceil(arg)
|
|
944
|
+
# undefined inputs always return undefined
|
|
945
|
+
return nil if arg.nil?
|
|
946
|
+
|
|
947
|
+
Utils.normalize_number(arg.to_f.ceil.to_f)
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
# Round to half even
|
|
951
|
+
# @param arg - Argument
|
|
952
|
+
# @param precision - number of decimal places
|
|
953
|
+
# @returns rounded integer
|
|
954
|
+
def round(arg, precision = nil)
|
|
955
|
+
# undefined inputs always return undefined
|
|
956
|
+
return nil if arg.nil?
|
|
957
|
+
|
|
958
|
+
if Utils.truthy?(precision)
|
|
959
|
+
# shift the decimal place - this needs to be done in a string since multiplying
|
|
960
|
+
# by a power of ten can introduce floating point precision errors which mess up
|
|
961
|
+
# this rounding algorithm - See 'Decimal rounding' in
|
|
962
|
+
# https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round
|
|
963
|
+
# Shift
|
|
964
|
+
value = Utils.number_to_s(arg).split('e')
|
|
965
|
+
arg = Float("#{value[0]}e#{value[1] ? (value[1].to_i + precision) : precision}")
|
|
966
|
+
end
|
|
967
|
+
|
|
968
|
+
# round up to nearest int
|
|
969
|
+
result = (arg.to_r + Rational(1, 2)).floor
|
|
970
|
+
diff = result - arg.to_f
|
|
971
|
+
if diff.abs == 0.5 && (result % 2).abs == 1 # rubocop:disable Lint/FloatComparison -- exact comparison, as in JS
|
|
972
|
+
# rounded the wrong way - adjust to nearest even number
|
|
973
|
+
result -= 1
|
|
974
|
+
end
|
|
975
|
+
if Utils.truthy?(precision)
|
|
976
|
+
# Shift back
|
|
977
|
+
value = Utils.number_to_s(result).split('e')
|
|
978
|
+
result = Float("#{value[0]}e#{value[1] ? (value[1].to_i - precision) : -precision}")
|
|
979
|
+
end
|
|
980
|
+
# JSON doesn't do -0
|
|
981
|
+
Utils.normalize_number(result.zero? ? 0 : result)
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
# Square root of number
|
|
985
|
+
# @param arg - Argument
|
|
986
|
+
# @returns square root
|
|
987
|
+
def sqrt(arg)
|
|
988
|
+
# undefined inputs always return undefined
|
|
989
|
+
return nil if arg.nil?
|
|
990
|
+
|
|
991
|
+
raise Jsonata::Error.new('D3060', :index => 1, :value => arg) if arg.negative?
|
|
992
|
+
|
|
993
|
+
Utils.normalize_number(Math.sqrt(arg))
|
|
994
|
+
end
|
|
995
|
+
|
|
996
|
+
# Raises number to the power of the second number
|
|
997
|
+
# @param arg - the base
|
|
998
|
+
# @param exp - the exponent
|
|
999
|
+
# @returns rounded integer
|
|
1000
|
+
def power(arg, exp)
|
|
1001
|
+
# undefined inputs always return undefined
|
|
1002
|
+
return nil if arg.nil?
|
|
1003
|
+
|
|
1004
|
+
result = if arg.negative? && !Utils.integer?(exp)
|
|
1005
|
+
Float::NAN
|
|
1006
|
+
else
|
|
1007
|
+
arg.to_f**exp.to_f
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
raise Jsonata::Error.new('D3061', :index => 1, :value => arg, :exp => exp) unless result.finite?
|
|
1011
|
+
|
|
1012
|
+
Utils.normalize_number(result)
|
|
1013
|
+
end
|
|
1014
|
+
|
|
1015
|
+
# Returns a random number 0 <= n < 1
|
|
1016
|
+
# @returns random number
|
|
1017
|
+
def random
|
|
1018
|
+
Kernel.rand
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
# Evaluate an input and return a boolean
|
|
1022
|
+
# @param arg - Arguments
|
|
1023
|
+
# @returns Boolean
|
|
1024
|
+
def boolean(arg)
|
|
1025
|
+
# cast arg to its effective boolean value
|
|
1026
|
+
# boolean: unchanged
|
|
1027
|
+
# string: zero-length -> false; otherwise -> true
|
|
1028
|
+
# number: 0 -> false; otherwise -> true
|
|
1029
|
+
# null -> false
|
|
1030
|
+
# array: empty -> false; length > 1 -> true
|
|
1031
|
+
# object: empty -> false; non-empty -> true
|
|
1032
|
+
# function -> false
|
|
1033
|
+
|
|
1034
|
+
# undefined inputs always return undefined
|
|
1035
|
+
return nil if arg.nil?
|
|
1036
|
+
|
|
1037
|
+
result = false
|
|
1038
|
+
if arg.kind_of?(Array)
|
|
1039
|
+
if arg.length == 1
|
|
1040
|
+
result = boolean(arg[0])
|
|
1041
|
+
elsif arg.length > 1
|
|
1042
|
+
trues = arg.select { |val| boolean(val) }
|
|
1043
|
+
result = !trues.empty?
|
|
1044
|
+
end
|
|
1045
|
+
elsif arg.kind_of?(String)
|
|
1046
|
+
result = true unless arg.empty?
|
|
1047
|
+
elsif Utils.numeric?(arg)
|
|
1048
|
+
result = true unless arg.zero?
|
|
1049
|
+
elsif arg.kind_of?(Hash)
|
|
1050
|
+
result = true unless Utils.keys(arg).empty?
|
|
1051
|
+
elsif arg == true
|
|
1052
|
+
result = true
|
|
1053
|
+
end
|
|
1054
|
+
result
|
|
1055
|
+
end
|
|
1056
|
+
|
|
1057
|
+
# returns the Boolean NOT of the arg
|
|
1058
|
+
# @param arg - argument
|
|
1059
|
+
# @returns NOT arg
|
|
1060
|
+
def not(arg)
|
|
1061
|
+
# undefined inputs always return undefined
|
|
1062
|
+
return nil if arg.nil?
|
|
1063
|
+
|
|
1064
|
+
!boolean(arg)
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
# Helper function to build the arguments to be supplied to the function arg of the
|
|
1068
|
+
# HOFs map, filter, each, sift and single
|
|
1069
|
+
# @param func - the function to be invoked
|
|
1070
|
+
# @param arg1 - the first (required) arg - the value
|
|
1071
|
+
# @param arg2 - the second (optional) arg - the position (index or key)
|
|
1072
|
+
# @param arg3 - the third (optional) arg - the whole structure (array or object)
|
|
1073
|
+
# @returns the argument list
|
|
1074
|
+
def hof_func_args(func, arg1, arg2, arg3)
|
|
1075
|
+
func_args = [arg1] # the first arg (the value) is required
|
|
1076
|
+
# the other two are optional - only supply it if the function can take it
|
|
1077
|
+
length = Utils.get_function_arity(func)
|
|
1078
|
+
func_args.push(arg2) if length >= 2
|
|
1079
|
+
func_args.push(arg3) if length >= 3
|
|
1080
|
+
func_args
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
# Create a map from an array of arguments
|
|
1084
|
+
# @param arr - array to map over
|
|
1085
|
+
# @param func - function to apply
|
|
1086
|
+
# @returns Map array
|
|
1087
|
+
def map(arr, func)
|
|
1088
|
+
# undefined inputs always return undefined
|
|
1089
|
+
return nil if arr.nil?
|
|
1090
|
+
|
|
1091
|
+
result = create_sequence
|
|
1092
|
+
# do the map - iterate over the arrays, and invoke func
|
|
1093
|
+
arr.each_with_index do |item, i|
|
|
1094
|
+
func_args = hof_func_args(func, item, i, arr)
|
|
1095
|
+
# invoke func
|
|
1096
|
+
res = call_function(func, func_args)
|
|
1097
|
+
result.push(res) unless res.nil?
|
|
1098
|
+
end
|
|
1099
|
+
|
|
1100
|
+
result
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
# Create a map from an array of arguments
|
|
1104
|
+
# @param arr - array to filter
|
|
1105
|
+
# @param func - predicate function
|
|
1106
|
+
# @returns Map array
|
|
1107
|
+
def filter(arr, func)
|
|
1108
|
+
# undefined inputs always return undefined
|
|
1109
|
+
return nil if arr.nil?
|
|
1110
|
+
|
|
1111
|
+
result = create_sequence
|
|
1112
|
+
|
|
1113
|
+
arr.each_with_index do |entry, i|
|
|
1114
|
+
func_args = hof_func_args(func, entry, i, arr)
|
|
1115
|
+
# invoke func
|
|
1116
|
+
res = call_function(func, func_args)
|
|
1117
|
+
result.push(entry) if boolean(res)
|
|
1118
|
+
end
|
|
1119
|
+
|
|
1120
|
+
result
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
# Given an array, find the single element matching a specified condition
|
|
1124
|
+
# Throws an exception if the number of matching elements is not exactly one
|
|
1125
|
+
# @param arr - array to filter
|
|
1126
|
+
# @param func - predicate function
|
|
1127
|
+
# @returns Matching element
|
|
1128
|
+
def single(arr, func)
|
|
1129
|
+
# undefined inputs always return undefined
|
|
1130
|
+
return nil if arr.nil?
|
|
1131
|
+
|
|
1132
|
+
has_found_match = false
|
|
1133
|
+
result = nil
|
|
1134
|
+
|
|
1135
|
+
arr.each_with_index do |entry, i|
|
|
1136
|
+
positive_result = true
|
|
1137
|
+
unless func.nil?
|
|
1138
|
+
func_args = hof_func_args(func, entry, i, arr)
|
|
1139
|
+
# invoke func
|
|
1140
|
+
res = call_function(func, func_args)
|
|
1141
|
+
positive_result = boolean(res)
|
|
1142
|
+
end
|
|
1143
|
+
next unless positive_result
|
|
1144
|
+
|
|
1145
|
+
raise Jsonata::Error.new('D3138', :index => i) if has_found_match
|
|
1146
|
+
|
|
1147
|
+
result = entry
|
|
1148
|
+
has_found_match = true
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1151
|
+
raise Jsonata::Error.new('D3139') unless has_found_match
|
|
1152
|
+
|
|
1153
|
+
result
|
|
1154
|
+
end
|
|
1155
|
+
|
|
1156
|
+
# Convolves (zips) each value from a set of arrays
|
|
1157
|
+
# @param args - arrays to zip
|
|
1158
|
+
# @returns Zipped array
|
|
1159
|
+
def zip(*args)
|
|
1160
|
+
# this can take a variable number of arguments
|
|
1161
|
+
result = []
|
|
1162
|
+
# length of the shortest array
|
|
1163
|
+
length = args.map { |arg| arg.kind_of?(Array) ? arg.length : 0 }.min || 0
|
|
1164
|
+
(0...length).each do |i|
|
|
1165
|
+
tuple = args.map { |arg| arg[i] }
|
|
1166
|
+
result.push(tuple)
|
|
1167
|
+
end
|
|
1168
|
+
result
|
|
1169
|
+
end
|
|
1170
|
+
|
|
1171
|
+
# Fold left function
|
|
1172
|
+
# @param sequence - Sequence
|
|
1173
|
+
# @param func - Function
|
|
1174
|
+
# @param init - Initial value
|
|
1175
|
+
# @returns Result
|
|
1176
|
+
def fold_left(sequence, func, init)
|
|
1177
|
+
# undefined inputs always return undefined
|
|
1178
|
+
return nil if sequence.nil?
|
|
1179
|
+
|
|
1180
|
+
arity = Utils.get_function_arity(func)
|
|
1181
|
+
raise Jsonata::Error.new('D3050', :index => 1) if arity < 2
|
|
1182
|
+
|
|
1183
|
+
if init.nil? && !sequence.empty?
|
|
1184
|
+
result = sequence[0]
|
|
1185
|
+
index = 1
|
|
1186
|
+
else
|
|
1187
|
+
result = init
|
|
1188
|
+
index = 0
|
|
1189
|
+
end
|
|
1190
|
+
|
|
1191
|
+
while index < sequence.length
|
|
1192
|
+
args = [result, sequence[index]]
|
|
1193
|
+
args.push(index) if arity >= 3
|
|
1194
|
+
args.push(sequence) if arity >= 4
|
|
1195
|
+
result = call_function(func, args)
|
|
1196
|
+
index += 1
|
|
1197
|
+
end
|
|
1198
|
+
|
|
1199
|
+
result
|
|
1200
|
+
end
|
|
1201
|
+
|
|
1202
|
+
# Return keys for an object
|
|
1203
|
+
# @param arg - Object
|
|
1204
|
+
# @returns Array of keys
|
|
1205
|
+
def keys(arg)
|
|
1206
|
+
result = create_sequence
|
|
1207
|
+
|
|
1208
|
+
if arg.kind_of?(Array)
|
|
1209
|
+
# merge the keys of all of the items in the array
|
|
1210
|
+
merge = {}
|
|
1211
|
+
arg.each do |item|
|
|
1212
|
+
keys(item).each do |key|
|
|
1213
|
+
merge[key] = true
|
|
1214
|
+
end
|
|
1215
|
+
end
|
|
1216
|
+
result = keys(merge)
|
|
1217
|
+
elsif arg.kind_of?(Hash)
|
|
1218
|
+
Utils.keys(arg).each { |key| result.push(key) }
|
|
1219
|
+
end
|
|
1220
|
+
result
|
|
1221
|
+
end
|
|
1222
|
+
|
|
1223
|
+
# Return value from an object for a given key
|
|
1224
|
+
# @param input - Object/Array
|
|
1225
|
+
# @param key - Key in object
|
|
1226
|
+
# @returns Value of key in object
|
|
1227
|
+
def lookup(input, key)
|
|
1228
|
+
# lookup the 'name' item in the input
|
|
1229
|
+
result = nil
|
|
1230
|
+
if input.kind_of?(Array)
|
|
1231
|
+
result = create_sequence
|
|
1232
|
+
input.each do |item|
|
|
1233
|
+
res = lookup(item, key)
|
|
1234
|
+
next if res.nil?
|
|
1235
|
+
|
|
1236
|
+
if res.kind_of?(Array)
|
|
1237
|
+
res.each { |val| result.push(val) }
|
|
1238
|
+
else
|
|
1239
|
+
result.push(res)
|
|
1240
|
+
end
|
|
1241
|
+
end
|
|
1242
|
+
elsif input.kind_of?(Hash) && input.key?(key)
|
|
1243
|
+
result = input[key]
|
|
1244
|
+
end
|
|
1245
|
+
result
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1248
|
+
# Append second argument to first
|
|
1249
|
+
# @param arg1 - First argument
|
|
1250
|
+
# @param arg2 - Second argument
|
|
1251
|
+
# @returns Appended arguments
|
|
1252
|
+
def append(arg1, arg2)
|
|
1253
|
+
# disregard undefined args
|
|
1254
|
+
return arg2 if arg1.nil?
|
|
1255
|
+
return arg1 if arg2.nil?
|
|
1256
|
+
|
|
1257
|
+
# if either argument is not an array, make it so
|
|
1258
|
+
arg1 = create_sequence(arg1) unless arg1.kind_of?(Array)
|
|
1259
|
+
arg2 = [arg2] unless arg2.kind_of?(Array)
|
|
1260
|
+
size = arg1.length + arg2.length
|
|
1261
|
+
raise Jsonata::Error.new('D2015', :value => size) if options && options[:sequence] && size > options[:sequence]
|
|
1262
|
+
|
|
1263
|
+
Array.new(arg1).concat(arg2)
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
# Determines if the argument is undefined
|
|
1267
|
+
# @param arg - argument
|
|
1268
|
+
# @returns False if argument undefined, otherwise true
|
|
1269
|
+
def exists(arg)
|
|
1270
|
+
!arg.nil?
|
|
1271
|
+
end
|
|
1272
|
+
|
|
1273
|
+
# Splits an object into an array of object with one property each
|
|
1274
|
+
# @param arg - the object to split
|
|
1275
|
+
# @returns the array
|
|
1276
|
+
def spread(arg)
|
|
1277
|
+
result = create_sequence
|
|
1278
|
+
|
|
1279
|
+
if arg.kind_of?(Array)
|
|
1280
|
+
# spread all of the items in the array
|
|
1281
|
+
arg.each do |item|
|
|
1282
|
+
result = append(result, spread(item))
|
|
1283
|
+
end
|
|
1284
|
+
elsif arg.kind_of?(Hash)
|
|
1285
|
+
Utils.keys(arg).each do |key|
|
|
1286
|
+
result.push({key => arg[key]})
|
|
1287
|
+
end
|
|
1288
|
+
else
|
|
1289
|
+
result = arg
|
|
1290
|
+
end
|
|
1291
|
+
result
|
|
1292
|
+
end
|
|
1293
|
+
|
|
1294
|
+
# Merges an array of objects into a single object. Duplicate properties are
|
|
1295
|
+
# overridden by entries later in the array
|
|
1296
|
+
# @param arg - the objects to merge
|
|
1297
|
+
# @returns the object
|
|
1298
|
+
def merge(arg)
|
|
1299
|
+
# undefined inputs always return undefined
|
|
1300
|
+
return nil if arg.nil?
|
|
1301
|
+
|
|
1302
|
+
result = {}
|
|
1303
|
+
|
|
1304
|
+
arg.each do |obj|
|
|
1305
|
+
Utils.keys(obj).each do |prop|
|
|
1306
|
+
result[prop] = obj[prop]
|
|
1307
|
+
end
|
|
1308
|
+
end
|
|
1309
|
+
result
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
# Reverses the order of items in an array
|
|
1313
|
+
# @param arr - the array to reverse
|
|
1314
|
+
# @returns the reversed array
|
|
1315
|
+
def reverse(arr)
|
|
1316
|
+
# undefined inputs always return undefined
|
|
1317
|
+
return nil if arr.nil?
|
|
1318
|
+
|
|
1319
|
+
return arr if arr.length <= 1
|
|
1320
|
+
|
|
1321
|
+
Array.new(arr).reverse
|
|
1322
|
+
end
|
|
1323
|
+
|
|
1324
|
+
# @param obj - the input object to iterate over
|
|
1325
|
+
# @param func - the function to apply to each key/value pair
|
|
1326
|
+
# @returns the resultant array
|
|
1327
|
+
def each(obj, func)
|
|
1328
|
+
result = create_sequence
|
|
1329
|
+
|
|
1330
|
+
Utils.keys(obj).each do |key|
|
|
1331
|
+
func_args = hof_func_args(func, obj[key], key, obj)
|
|
1332
|
+
# invoke func
|
|
1333
|
+
val = call_function(func, func_args)
|
|
1334
|
+
result.push(val) unless val.nil?
|
|
1335
|
+
end
|
|
1336
|
+
|
|
1337
|
+
result
|
|
1338
|
+
end
|
|
1339
|
+
|
|
1340
|
+
# @param message - the message to attach to the error
|
|
1341
|
+
# @throws custom error with code 'D3137'
|
|
1342
|
+
def error(message)
|
|
1343
|
+
raise Jsonata::Error.new('D3137', :message => Utils.truthy?(message) ? message : '$error() function evaluated')
|
|
1344
|
+
end
|
|
1345
|
+
|
|
1346
|
+
# @param condition - the condition to evaluate
|
|
1347
|
+
# @param message - the message to attach to the error
|
|
1348
|
+
# @throws custom error with code 'D3137'
|
|
1349
|
+
# @returns undefined
|
|
1350
|
+
def assert(condition, message)
|
|
1351
|
+
unless condition
|
|
1352
|
+
raise Jsonata::Error.new('D3141', :message => Utils.truthy?(message) ? message : '$assert() statement failed')
|
|
1353
|
+
end
|
|
1354
|
+
|
|
1355
|
+
nil
|
|
1356
|
+
end
|
|
1357
|
+
|
|
1358
|
+
# @param value - the input to which the type will be checked
|
|
1359
|
+
# @returns the type of the input
|
|
1360
|
+
def type(value)
|
|
1361
|
+
return nil if value.nil?
|
|
1362
|
+
return 'null' if value == NULL
|
|
1363
|
+
return 'number' if Utils.numeric?(value)
|
|
1364
|
+
return 'string' if value.kind_of?(String)
|
|
1365
|
+
return 'boolean' if [true, false].include?(value)
|
|
1366
|
+
return 'array' if value.kind_of?(Array)
|
|
1367
|
+
return 'function' if Utils.function?(value)
|
|
1368
|
+
|
|
1369
|
+
'object'
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
# Implements the merge sort (stable) with optional comparator function
|
|
1373
|
+
#
|
|
1374
|
+
# @param arr - the array to sort
|
|
1375
|
+
# @param comparator - comparator function
|
|
1376
|
+
# @returns sorted array
|
|
1377
|
+
def sort(arr, comparator)
|
|
1378
|
+
# undefined inputs always return undefined
|
|
1379
|
+
return nil if arr.nil?
|
|
1380
|
+
|
|
1381
|
+
return arr if arr.length <= 1
|
|
1382
|
+
|
|
1383
|
+
if comparator.nil?
|
|
1384
|
+
# inject a default comparator - only works for numeric or string arrays
|
|
1385
|
+
unless Utils.array_of_numbers?(arr) || Utils.array_of_strings?(arr)
|
|
1386
|
+
raise Jsonata::Error.new('D3070', :index => 1)
|
|
1387
|
+
end
|
|
1388
|
+
|
|
1389
|
+
comp = ->(a, b) { a > b }
|
|
1390
|
+
else
|
|
1391
|
+
# for internal usage of functionSort (i.e. order-by syntax)
|
|
1392
|
+
comp = comparator
|
|
1393
|
+
end
|
|
1394
|
+
|
|
1395
|
+
merge = lambda do |l, r|
|
|
1396
|
+
# (merge_iter in jsonata-js; implemented iteratively here to avoid deep recursion)
|
|
1397
|
+
result = []
|
|
1398
|
+
left = 0
|
|
1399
|
+
right = 0
|
|
1400
|
+
while left < l.length && right < r.length
|
|
1401
|
+
if call_function(comp, [l[left], r[right]]) # invoke the comparator function
|
|
1402
|
+
# if it returns true - swap left and right
|
|
1403
|
+
result.push(r[right])
|
|
1404
|
+
right += 1
|
|
1405
|
+
else
|
|
1406
|
+
# otherwise keep the same order
|
|
1407
|
+
result.push(l[left])
|
|
1408
|
+
left += 1
|
|
1409
|
+
end
|
|
1410
|
+
end
|
|
1411
|
+
result.concat(l[left..]) if left < l.length
|
|
1412
|
+
result.concat(r[right..]) if right < r.length
|
|
1413
|
+
result
|
|
1414
|
+
end
|
|
1415
|
+
|
|
1416
|
+
msort = lambda do |array|
|
|
1417
|
+
if !array.kind_of?(Array) || array.length <= 1
|
|
1418
|
+
array
|
|
1419
|
+
else
|
|
1420
|
+
middle = (array.length / 2).floor
|
|
1421
|
+
left = array[0, middle]
|
|
1422
|
+
right = array[middle..]
|
|
1423
|
+
left = msort.call(left)
|
|
1424
|
+
right = msort.call(right)
|
|
1425
|
+
merge.call(left, right)
|
|
1426
|
+
end
|
|
1427
|
+
end
|
|
1428
|
+
|
|
1429
|
+
msort.call(arr)
|
|
1430
|
+
end
|
|
1431
|
+
|
|
1432
|
+
# Randomly shuffles the contents of an array
|
|
1433
|
+
# @param arr - the input array
|
|
1434
|
+
# @returns the shuffled array
|
|
1435
|
+
def shuffle(arr)
|
|
1436
|
+
# undefined inputs always return undefined
|
|
1437
|
+
return nil if arr.nil?
|
|
1438
|
+
|
|
1439
|
+
return arr if arr.length <= 1
|
|
1440
|
+
|
|
1441
|
+
# shuffle using the 'inside-out' variant of the Fisher-Yates algorithm
|
|
1442
|
+
result = Array.new(arr.length)
|
|
1443
|
+
arr.each_with_index do |item, i|
|
|
1444
|
+
j = (Kernel.rand * (i + 1)).floor # random integer such that 0 <= j <= i
|
|
1445
|
+
result[i] = result[j] if i != j
|
|
1446
|
+
result[j] = item
|
|
1447
|
+
end
|
|
1448
|
+
|
|
1449
|
+
result
|
|
1450
|
+
end
|
|
1451
|
+
|
|
1452
|
+
# Returns the values that appear in a sequence, with duplicates eliminated.
|
|
1453
|
+
# @param arr - An array or sequence of values
|
|
1454
|
+
# @returns sequence of distinct values
|
|
1455
|
+
def distinct(arr)
|
|
1456
|
+
# undefined inputs always return undefined
|
|
1457
|
+
return nil if arr.nil?
|
|
1458
|
+
|
|
1459
|
+
return arr if !arr.kind_of?(Array) || arr.length <= 1
|
|
1460
|
+
|
|
1461
|
+
results = Utils.sequence?(arr) ? create_sequence : []
|
|
1462
|
+
|
|
1463
|
+
arr.each do |value|
|
|
1464
|
+
# is this value already in the result sequence?
|
|
1465
|
+
includes = results.any? { |result| Utils.deep_equal?(value, result) }
|
|
1466
|
+
results.push(value) unless includes
|
|
1467
|
+
end
|
|
1468
|
+
results
|
|
1469
|
+
end
|
|
1470
|
+
|
|
1471
|
+
# Applies a predicate function to each key/value pair in an object, and returns an object containing
|
|
1472
|
+
# only the key/value pairs that passed the predicate
|
|
1473
|
+
#
|
|
1474
|
+
# @param arg - the object to be sifted
|
|
1475
|
+
# @param func - the predicate function (lambda or native)
|
|
1476
|
+
# @returns sifted object
|
|
1477
|
+
def sift(arg, func)
|
|
1478
|
+
result = {}
|
|
1479
|
+
|
|
1480
|
+
Utils.keys(arg).each do |item|
|
|
1481
|
+
entry = arg[item]
|
|
1482
|
+
func_args = hof_func_args(func, entry, item, arg)
|
|
1483
|
+
# invoke func
|
|
1484
|
+
res = call_function(func, func_args)
|
|
1485
|
+
result[item] = entry if boolean(res)
|
|
1486
|
+
end
|
|
1487
|
+
|
|
1488
|
+
# empty objects should be changed to undefined
|
|
1489
|
+
result = nil if Utils.keys(result).empty?
|
|
1490
|
+
|
|
1491
|
+
result
|
|
1492
|
+
end
|
|
1493
|
+
|
|
1494
|
+
private
|
|
1495
|
+
|
|
1496
|
+
# Equivalent of `func.apply(this, args)` in jsonata-js
|
|
1497
|
+
def call_function(func, args)
|
|
1498
|
+
if func.kind_of?(Lambda)
|
|
1499
|
+
func.apply(self, args)
|
|
1500
|
+
else
|
|
1501
|
+
Utils.invoke(func, args)
|
|
1502
|
+
end
|
|
1503
|
+
end
|
|
1504
|
+
end
|
|
1505
|
+
end
|