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.
@@ -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