jsonata-python 0.5.2__py3-none-any.whl

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.
jsonata/functions.py ADDED
@@ -0,0 +1,2234 @@
1
+ #
2
+ # Copyright Robert Yokota
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License")
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+ # Derived from the following code:
17
+ #
18
+ # Project name: jsonata-java
19
+ # Copyright Dashjoin GmbH. https://dashjoin.com
20
+ # Licensed under the Apache License, Version 2.0 (the "License")
21
+ #
22
+ # Project name: elementpath
23
+ # Copyright (c), 2018-2021, SISSA (Scuola Internazionale Superiore di Studi Avanzati)
24
+ # This project is licensed under the MIT License, see LICENSE
25
+
26
+ import base64
27
+ import datetime
28
+ import decimal
29
+ import functools
30
+ import inspect
31
+ import json
32
+ import math
33
+ import random
34
+ import re
35
+ import sys
36
+ import unicodedata
37
+ import urllib.parse
38
+ from dataclasses import dataclass
39
+ from typing import Any, AnyStr, Mapping, NoReturn, Optional, Sequence, Callable, Type, Union
40
+
41
+ from jsonata import datetimeutils, jexception, parser, utils
42
+
43
+
44
+ class Functions:
45
+
46
+ #
47
+ # Sum function
48
+ # @param {Object} args - Arguments
49
+ # @returns {number} Total value of arguments
50
+ #
51
+ @staticmethod
52
+ def sum(args: Optional[Sequence[float]]) -> Optional[float]:
53
+ # undefined inputs always return undefined
54
+ if args is None:
55
+ return None
56
+
57
+ return sum(args)
58
+
59
+ #
60
+ # Count function
61
+ # @param {Object} args - Arguments
62
+ # @returns {number} Number of elements in the array
63
+ #
64
+ @staticmethod
65
+ def count(args: Optional[Sequence[Any]]) -> float:
66
+ # undefined inputs always return undefined
67
+ if args is None:
68
+ return 0
69
+
70
+ return len(args)
71
+
72
+ #
73
+ # Max function
74
+ # @param {Object} args - Arguments
75
+ # @returns {number} Max element in the array
76
+ #
77
+ @staticmethod
78
+ def max(args: Optional[Sequence[float]]) -> Optional[float]:
79
+ # undefined inputs always return undefined
80
+ if args is None or not args:
81
+ return None
82
+
83
+ return max(args)
84
+
85
+ #
86
+ # Min function
87
+ # @param {Object} args - Arguments
88
+ # @returns {number} Min element in the array
89
+ #
90
+ @staticmethod
91
+ def min(args: Optional[Sequence[float]]) -> Optional[float]:
92
+ # undefined inputs always return undefined
93
+ if args is None or not args:
94
+ return None
95
+
96
+ return min(args)
97
+
98
+ #
99
+ # Average function
100
+ # @param {Object} args - Arguments
101
+ # @returns {number} Average element in the array
102
+ #
103
+ @staticmethod
104
+ def average(args: Optional[Sequence[float]]) -> Optional[float]:
105
+ # undefined inputs always return undefined
106
+ if args is None or not args:
107
+ return None
108
+
109
+ return sum(args) / len(args)
110
+
111
+ #
112
+ # Stringify arguments
113
+ # @param {Object} arg - Arguments
114
+ # @param {boolean} [prettify] - Pretty print the result
115
+ # @returns {String} String from arguments
116
+ #
117
+ @staticmethod
118
+ def string(arg: Optional[Any], prettify: Optional[bool]) -> Optional[str]:
119
+
120
+ if isinstance(arg, utils.Utils.JList):
121
+ if arg.outer_wrapper:
122
+ arg = arg[0]
123
+
124
+ if arg is None:
125
+ return None
126
+
127
+ # see https://docs.jsonata.org/string-functions#string: Strings are unchanged
128
+ if isinstance(arg, str):
129
+ return str(arg)
130
+
131
+ return Functions._string(arg, bool(prettify))
132
+
133
+ @staticmethod
134
+ def _string(arg: Any, prettify: bool) -> str:
135
+ from jsonata import jsonata
136
+
137
+ if isinstance(arg, (jsonata.Jsonata.JFunction, parser.Parser.Symbol)):
138
+ return ""
139
+
140
+ if prettify:
141
+ return json.dumps(arg, cls=Functions.Encoder, indent=" ")
142
+ else:
143
+ return json.dumps(arg, cls=Functions.Encoder, separators=(',', ':'))
144
+
145
+ class Encoder(json.JSONEncoder):
146
+ def encode(self, arg):
147
+ if not isinstance(arg, bool) and isinstance(arg, (int, float)):
148
+ d = decimal.Decimal(arg)
149
+ res = Functions.remove_exponent(d, decimal.Context(prec=15))
150
+ return str(res).lower()
151
+
152
+ return super().encode(arg)
153
+
154
+ def default(self, arg):
155
+ from jsonata import jsonata
156
+
157
+ if arg is utils.Utils.NULL_VALUE:
158
+ return None
159
+
160
+ if isinstance(arg, (jsonata.Jsonata.JFunction, parser.Parser.Symbol)):
161
+ return ""
162
+
163
+ return super().default(arg)
164
+
165
+ @staticmethod
166
+ def remove_exponent(d: decimal.Decimal, ctx: decimal.Context) -> decimal.Decimal:
167
+ # Adapted from https://docs.python.org/3/library/decimal.html#decimal-faq
168
+ if d == d.to_integral():
169
+ try:
170
+ return d.quantize(decimal.Decimal(1), context=ctx)
171
+ except decimal.InvalidOperation:
172
+ pass
173
+ return d.normalize(ctx)
174
+
175
+ #
176
+ # Validate input data types.
177
+ # This will make sure that all input data can be processed.
178
+ #
179
+ # @param arg
180
+ # @return
181
+ #
182
+ @staticmethod
183
+ def validate_input(arg: Optional[Any]) -> None:
184
+ from jsonata import jsonata
185
+
186
+ if arg is None or arg is utils.Utils.NULL_VALUE:
187
+ return
188
+
189
+ if isinstance(arg, (jsonata.Jsonata.JFunction, parser.Parser.Symbol)):
190
+ return
191
+
192
+ if isinstance(arg, bool):
193
+ return
194
+
195
+ if isinstance(arg, (int, float)):
196
+ return
197
+
198
+ if isinstance(arg, str):
199
+ return
200
+
201
+ if isinstance(arg, dict):
202
+ for k, v in arg.items():
203
+ Functions.validate_input(k)
204
+ Functions.validate_input(v)
205
+ return
206
+
207
+ if isinstance(arg, list):
208
+ for v in arg:
209
+ Functions.validate_input(v)
210
+ return
211
+
212
+ # Throw error for unknown types
213
+ raise ValueError(
214
+ "Only JSON types (values, Map, List) are allowed as input. Unsupported type: " + str(type(arg)))
215
+
216
+ #
217
+ # Create substring based on character number and length
218
+ # @param {String} str - String to evaluate
219
+ # @param {Integer} start - Character number to start substring
220
+ # @param {Integer} [length] - Number of characters in substring
221
+ # @returns {string|*} Substring
222
+ #
223
+ @staticmethod
224
+ def substring(string: Optional[str], start: Optional[float], length: Optional[float]) -> Optional[str]:
225
+ # undefined inputs always return undefined
226
+ if string is None:
227
+ return None
228
+
229
+ if string is utils.Utils.NULL_VALUE:
230
+ raise jexception.JException("T0410", -1)
231
+
232
+ start = int(start) if start is not None else None
233
+ length = int(length) if length is not None else None
234
+
235
+ # not used: var strArray = stringToArray(string)
236
+ str_length = len(string)
237
+
238
+ if str_length + start < 0:
239
+ start = 0
240
+
241
+ if length is not None:
242
+ if length <= 0:
243
+ return ""
244
+ return Functions.substr(string, start, length)
245
+
246
+ return Functions.substr(string, start, str_length)
247
+
248
+ #
249
+ # Source = Jsonata4Java JSONataUtils.substr
250
+ # @param str
251
+ # @param start Location at which to begin extracting characters. If a negative
252
+ # number is given, it is treated as strLength - start where
253
+ # strLength is the length of the string. For example,
254
+ # str.substr(-3) is treated as str.substr(str.length - 3)
255
+ # @param length The number of characters to extract. If this argument is null,
256
+ # all the characters from start to the end of the string are
257
+ # extracted.
258
+ # @return A new string containing the extracted section of the given string. If
259
+ # length is 0 or a negative number, an empty string is returned.
260
+ #
261
+ @staticmethod
262
+ def substr(string: str, start: int, length: int) -> str:
263
+
264
+ # below has to convert start and length for emojis and unicode
265
+ orig_len = len(string)
266
+
267
+ str_data = string
268
+ str_len = len(str_data)
269
+ if start >= str_len:
270
+ return ""
271
+ # If start is negative, substr() uses it as a character index from the
272
+ # end of the string; the index of the last character is -1.
273
+ start = start if start >= 0 else (0 if (str_len + start) < 0 else str_len + start)
274
+ if start < 0:
275
+ start = 0 # If start is negative and abs(start) is larger than the length of the
276
+ # string, substr() uses 0 as the start index.
277
+ # If length is omitted, substr() extracts characters to the end of the
278
+ # string.
279
+ if length is None:
280
+ length = len(str_data)
281
+ elif length < 0:
282
+ # If length is 0 or negative, substr() returns an empty string.
283
+ return ""
284
+ elif length > len(str_data):
285
+ length = len(str_data)
286
+
287
+ if start >= 0:
288
+ # If start is positive and is greater than or equal to the length of
289
+ # the string, substr() returns an empty string.
290
+ if start >= orig_len:
291
+ return ""
292
+
293
+ # collect length characters (unless it reaches the end of the string
294
+ # first, in which case it will return fewer)
295
+ end = start + length
296
+ if end > orig_len:
297
+ end = orig_len
298
+
299
+ return str_data[start:end]
300
+
301
+ #
302
+ # Create substring up until a character
303
+ # @param {String} str - String to evaluate
304
+ # @param {String} chars - Character to define substring boundary
305
+ # @returns {*} Substring
306
+ #
307
+ @staticmethod
308
+ def substring_before(string: Optional[str], chars: Optional[str]) -> Optional[str]:
309
+ # undefined inputs always return undefined
310
+ if string is None:
311
+ return None
312
+
313
+ if string is utils.Utils.NULL_VALUE:
314
+ raise jexception.JException("T0410", -1)
315
+
316
+ if chars is None:
317
+ return string
318
+
319
+ pos = string.find(chars)
320
+ if pos > -1:
321
+ return string[0:pos]
322
+ else:
323
+ return string
324
+
325
+ #
326
+ # Create substring after a character
327
+ # @param {String} str - String to evaluate
328
+ # @param {String} chars - Character to define substring boundary
329
+ # @returns {*} Substring
330
+ #
331
+ @staticmethod
332
+ def substring_after(string: Optional[str], chars: Optional[str]) -> Optional[str]:
333
+ # undefined inputs always return undefined
334
+ if string is None:
335
+ return None
336
+
337
+ if string is utils.Utils.NULL_VALUE:
338
+ raise jexception.JException("T0410", -1)
339
+
340
+ pos = string.find(chars)
341
+ if pos > -1:
342
+ return string[pos + len(chars):]
343
+ else:
344
+ return string
345
+
346
+ #
347
+ # Lowercase a string
348
+ # @param {String} str - String to evaluate
349
+ # @returns {string} Lowercase string
350
+ #
351
+ @staticmethod
352
+ def lowercase(string: Optional[str]) -> Optional[str]:
353
+ # undefined inputs always return undefined
354
+ if string is None:
355
+ return None
356
+
357
+ if string is utils.Utils.NULL_VALUE:
358
+ raise jexception.JException("T0410", -1)
359
+
360
+ return string.casefold()
361
+
362
+ #
363
+ # Uppercase a string
364
+ # @param {String} str - String to evaluate
365
+ # @returns {string} Uppercase string
366
+ #
367
+ @staticmethod
368
+ def uppercase(string: Optional[str]) -> Optional[str]:
369
+ # undefined inputs always return undefined
370
+ if string is None:
371
+ return None
372
+
373
+ if string is utils.Utils.NULL_VALUE:
374
+ raise jexception.JException("T0410", -1)
375
+
376
+ return string.upper()
377
+
378
+ #
379
+ # length of a string
380
+ # @param {String} str - string
381
+ # @returns {Number} The number of characters in the string
382
+ #
383
+ @staticmethod
384
+ def length(string: Optional[str]) -> Optional[int]:
385
+ # undefined inputs always return undefined
386
+ if string is None:
387
+ return None
388
+
389
+ if string is utils.Utils.NULL_VALUE:
390
+ raise jexception.JException("T0410", -1)
391
+
392
+ return len(string)
393
+
394
+ #
395
+ # Normalize and trim whitespace within a string
396
+ # @param {string} str - string to be trimmed
397
+ # @returns {string} - trimmed string
398
+ #
399
+ @staticmethod
400
+ def trim(string: Optional[str]) -> Optional[str]:
401
+ # undefined inputs always return undefined
402
+ if string is None:
403
+ return None
404
+
405
+ if string is utils.Utils.NULL_VALUE:
406
+ raise jexception.JException("T0410", -1)
407
+
408
+ if not string:
409
+ return ""
410
+
411
+ # normalize whitespace
412
+ result = re.sub("[ \t\n\r]+", " ", string)
413
+ if result[0] == ' ':
414
+ # strip leading space
415
+ result = result[1:]
416
+
417
+ if result == "":
418
+ return ""
419
+
420
+ if result[len(result) - 1] == ' ':
421
+ # strip trailing space
422
+ result = result[0:len(result) - 1]
423
+ return result
424
+
425
+ #
426
+ # Pad a string to a minimum width by adding characters to the start or end
427
+ # @param {string} str - string to be padded
428
+ # @param {number} width - the minimum width; +ve pads to the right, -ve pads to the left
429
+ # @param {string} [char] - the pad character(s); defaults to ' '
430
+ # @returns {string} - padded string
431
+ #
432
+ @staticmethod
433
+ def pad(string: Optional[str], width: Optional[int], char: Optional[str]) -> Optional[str]:
434
+ # undefined inputs always return undefined
435
+ if string is None:
436
+ return None
437
+
438
+ if string is utils.Utils.NULL_VALUE:
439
+ raise jexception.JException("T0410", -1)
440
+
441
+ if char is None or not char:
442
+ char = " "
443
+
444
+ if width < 0:
445
+ result = Functions.left_pad(string, -width, char)
446
+ else:
447
+ result = Functions.right_pad(string, width, char)
448
+ return result
449
+
450
+ # Source: Jsonata4Java PadFunction
451
+ @staticmethod
452
+ def left_pad(string: Optional[str], size: Optional[int], pad_str: Optional[str]) -> Optional[str]:
453
+ if string is None:
454
+ return None
455
+
456
+ if string is utils.Utils.NULL_VALUE:
457
+ raise jexception.JException("T0410", -1)
458
+
459
+ if pad_str is None:
460
+ pad_str = " "
461
+
462
+ str_data = string
463
+ str_len = len(str_data)
464
+
465
+ if not pad_str:
466
+ pad_str = " "
467
+ pads = size - str_len
468
+ if pads <= 0:
469
+ return string
470
+ padding = ""
471
+ i = 0
472
+ while i < pads + 1:
473
+ padding += pad_str
474
+ i += 1
475
+ return Functions.substr(padding, 0, pads) + string
476
+
477
+ # Source: Jsonata4Java PadFunction
478
+ @staticmethod
479
+ def right_pad(string: Optional[str], size: Optional[int], pad_str: Optional[str]) -> Optional[str]:
480
+ if string is None:
481
+ return None
482
+
483
+ if string is utils.Utils.NULL_VALUE:
484
+ raise jexception.JException("T0410", -1)
485
+
486
+ if pad_str is None:
487
+ pad_str = " "
488
+
489
+ str_data = string
490
+ str_len = len(str_data)
491
+
492
+ if not pad_str:
493
+ pad_str = " "
494
+ pads = size - str_len
495
+ if pads <= 0:
496
+ return string
497
+ padding = ""
498
+ i = 0
499
+ while i < pads + 1:
500
+ padding += pad_str
501
+ i += 1
502
+ return string + Functions.substr(padding, 0, pads)
503
+
504
+ @dataclass
505
+ class RegexpMatch:
506
+ match: str
507
+ index: int
508
+ groups: Sequence[AnyStr]
509
+
510
+ #
511
+ # Evaluate the matcher function against the str arg
512
+ #
513
+ # @param {*} matcher - matching function (native or lambda)
514
+ # @param {string} str - the string to match against
515
+ # @returns {object} - structure that represents the match(es)
516
+ #
517
+ @staticmethod
518
+ def evaluate_matcher(matcher: re.Pattern, string: Optional[str]) -> list[RegexpMatch]:
519
+ res = []
520
+ matches = matcher.finditer(string)
521
+ for m in matches:
522
+ groups = []
523
+ # Collect the groups
524
+ g = 1
525
+ while g <= len(m.groups()):
526
+ groups.append(m.group(g))
527
+ g += 1
528
+
529
+ rm = Functions.RegexpMatch(m.group(), m.start(), groups)
530
+ rm.groups = groups
531
+ res.append(rm)
532
+ return res
533
+
534
+ #
535
+ # Tests if the str contains the token
536
+ # @param {String} str - string to test
537
+ # @param {String} token - substring or regex to find
538
+ # @returns {Boolean} - true if str contains token
539
+ #
540
+ @staticmethod
541
+ def contains(string: Optional[str], token: Union[None, str, re.Pattern]) -> Optional[bool]:
542
+ # undefined inputs always return undefined
543
+ if string is None:
544
+ return None
545
+
546
+ if string is utils.Utils.NULL_VALUE:
547
+ return None
548
+
549
+ result = False
550
+
551
+ if isinstance(token, str):
552
+ result = (string.find(str(token)) != - 1)
553
+ elif isinstance(token, re.Pattern):
554
+ matches = Functions.evaluate_matcher(token, string)
555
+ # if (dbg) System.out.println("match = "+matches)
556
+ # result = (typeof matches !== 'undefined')
557
+ # throw new Error("regexp not impl"); //result = false
558
+ result = bool(matches)
559
+ else:
560
+ raise RuntimeError("unknown type to match: " + str(token))
561
+
562
+ return result
563
+
564
+ #
565
+ # Match a string with a regex returning an array of object containing details of each match
566
+ # @param {String} str - string
567
+ # @param {String} regex - the regex applied to the string
568
+ # @param {Integer} [limit] - max number of matches to return
569
+ # @returns {Array} The array of match objects
570
+ #
571
+ @staticmethod
572
+ def match_(string: Optional[str], regex: Optional[re.Pattern], limit: Optional[int]) -> Optional[list[dict[str, Any]]]:
573
+ # undefined inputs always return undefined
574
+ if string is None:
575
+ return None
576
+
577
+ if string is utils.Utils.NULL_VALUE:
578
+ raise jexception.JException("T0410", -1)
579
+
580
+ # limit, if specified, must be a non-negative number
581
+ if limit is not None and limit < 0:
582
+ raise jexception.JException("D3040", -1, limit)
583
+
584
+ result = utils.Utils.create_sequence()
585
+ matches = Functions.evaluate_matcher(regex, string)
586
+ max = sys.maxsize
587
+ if limit is not None:
588
+ max = limit
589
+
590
+ for i, rm in enumerate(matches):
591
+ m = {"match": rm.match, "index": rm.index, "groups": rm.groups}
592
+ # Convert to JSON map:
593
+ result.append(m)
594
+ if i >= max:
595
+ break
596
+ return result
597
+
598
+ #
599
+ # Join an array of strings
600
+ # @param {Array} strs - array of string
601
+ # @param {String} [separator] - the token that splits the string
602
+ # @returns {String} The concatenated string
603
+ #
604
+ @staticmethod
605
+ def join(strs: Optional[Sequence[str]], separator: Optional[str]) -> Optional[str]:
606
+ # undefined inputs always return undefined
607
+ if strs is None:
608
+ return None
609
+
610
+ # if separator is not specified, default to empty string
611
+ if separator is None:
612
+ separator = ""
613
+
614
+ return separator.join(strs)
615
+
616
+ @staticmethod
617
+ def safe_replacement(in_: str) -> str:
618
+ result = in_
619
+
620
+ # Replace "$<num>" with "\<num>" for Python regex
621
+ result = re.sub(r"\$(\d+)", r"\\g<\g<1>>", result)
622
+
623
+ # Replace "$$" with "$"
624
+ result = re.sub("\\$\\$", "$", result)
625
+
626
+ return result
627
+
628
+ #
629
+ # Safe replaceAll
630
+ #
631
+ # In Java, non-existing groups cause an exception.
632
+ # Ignore these non-existing groups (replace with "")
633
+ #
634
+ # @param s
635
+ # @param pattern
636
+ # @param replacement
637
+ # @return
638
+ #
639
+ @staticmethod
640
+ def safe_replace_all(s: str, pattern: re.Pattern, replacement: Optional[Any]) -> Optional[str]:
641
+
642
+ if not (isinstance(replacement, str)):
643
+ return Functions.safe_replace_all_fn(s, pattern, replacement)
644
+
645
+ replacement = str(replacement)
646
+
647
+ replacement = Functions.safe_replacement(replacement)
648
+ r = None
649
+ for i in range(0, 10):
650
+ try:
651
+ r = re.sub(pattern, replacement, s)
652
+ break
653
+ except Exception as e:
654
+ msg = str(e)
655
+
656
+ # Message we understand needs to be:
657
+ # invalid group reference <g> at position <p>
658
+ m = re.match(r"invalid group reference (\d+) at position (\d+)", msg)
659
+
660
+ if m is None:
661
+ raise e
662
+
663
+ g = m.group(1)
664
+ suffix = g[-1]
665
+ prefix = g[:-1]
666
+ # Try capturing a smaller numbered group, e.g. "\g<1>2" instead of "\g<12>"
667
+ replace = "" if not prefix else r"\g<" + prefix + ">" + suffix
668
+
669
+ # Adjust replacement to remove the non-existing group
670
+ replacement = replacement.replace(r"\g<" + g + ">", replace)
671
+ return r
672
+
673
+ #
674
+ # Converts Java MatchResult to the Jsonata object format
675
+ # @param mr
676
+ # @return
677
+ #
678
+ @staticmethod
679
+ def to_jsonata_match(mr: re.Match[str]) -> dict[str, list[str]]:
680
+ obj = {"match": mr.group()}
681
+
682
+ groups = []
683
+ i = 0
684
+ while i <= len(mr.groups()):
685
+ groups.append(mr.group(i))
686
+ i += 1
687
+
688
+ obj["groups"] = groups
689
+
690
+ return obj
691
+
692
+ #
693
+ # Regexp Replace with replacer function
694
+ # @param s
695
+ # @param pattern
696
+ # @param fn
697
+ # @return
698
+ #
699
+ @staticmethod
700
+ def safe_replace_all_fn(s: str, pattern: re.Pattern, fn: Optional[Any]) -> str:
701
+ def replace_fn(t):
702
+ res = Functions.func_apply(fn, [Functions.to_jsonata_match(t)])
703
+ if isinstance(res, str):
704
+ return res
705
+ else:
706
+ raise jexception.JException("D3012", -1)
707
+
708
+ r = re.sub(pattern, replace_fn, s)
709
+ return r
710
+
711
+ #
712
+ # Safe replaceFirst
713
+ #
714
+ # @param s
715
+ # @param pattern
716
+ # @param replacement
717
+ # @return
718
+ #
719
+ @staticmethod
720
+ def safe_replace_first(s: str, pattern: re.Pattern, replacement: str) -> Optional[str]:
721
+ replacement = Functions.safe_replacement(replacement)
722
+ r = None
723
+ for i in range(0, 10):
724
+ try:
725
+ r = re.sub(pattern, replacement, s, 1)
726
+ break
727
+ except Exception as e:
728
+ msg = str(e)
729
+
730
+ # Message we understand needs to be:
731
+ # invalid group reference <g> at position <p>
732
+ m = re.match(r"invalid group reference (\d+) at position (\d+)", msg)
733
+
734
+ if m is None:
735
+ raise e
736
+
737
+ g = m.group(1)
738
+ suffix = g[-1]
739
+ prefix = g[:-1]
740
+ # Try capturing a smaller numbered group, e.g. "\g<1>2" instead of "\g<12>"
741
+ replace = "" if not prefix else r"\g<" + prefix + ">" + suffix
742
+
743
+ # Adjust replacement to remove the non-existing group
744
+ replacement = replacement.replace(r"\g<" + g + ">", replace)
745
+ return r
746
+
747
+ @staticmethod
748
+ def replace(string: Optional[str], pattern: Union[str, re.Pattern], replacement: Optional[Any], limit: Optional[int]) -> Optional[str]:
749
+ if string is None:
750
+ return None
751
+
752
+ if string is utils.Utils.NULL_VALUE:
753
+ raise jexception.JException("T0410", -1)
754
+
755
+ if isinstance(pattern, str):
756
+ if not pattern:
757
+ raise jexception.JException("Second argument of replace function cannot be an empty string", 0)
758
+
759
+ if limit is not None and limit < 0:
760
+ raise jexception.JException("Fourth argument of replace function must evaluate to a positive number", 0)
761
+
762
+ def string_replacer(match):
763
+ result = ''
764
+ position = 0
765
+ repl = str(replacement)
766
+ while position < len(repl):
767
+ index = repl.find('$', position)
768
+ if index == -1:
769
+ result += repl[position:]
770
+ break
771
+ result += repl[position:index]
772
+ position = index + 1
773
+ if position < len(repl):
774
+ dollar_val = repl[position]
775
+ if dollar_val == '$':
776
+ result += '$'
777
+ position += 1
778
+ elif dollar_val == '0':
779
+ result += match.group(0)
780
+ position += 1
781
+ else:
782
+ max_digits = len(str(len(match.groups())))
783
+ group_num = repl[position:position+max_digits]
784
+ if group_num.isdigit():
785
+ group_index = int(group_num)
786
+ if 0 < group_index <= len(match.groups()):
787
+ result += match.group(group_index) or ''
788
+ position += len(group_num)
789
+ else:
790
+ result += '$'
791
+ else:
792
+ result += '$'
793
+ else:
794
+ result += '$'
795
+ return result
796
+
797
+ if callable(replacement):
798
+ replacer = lambda m: replacement(m.groupdict())
799
+ elif isinstance(replacement, str):
800
+ replacer = string_replacer
801
+ else:
802
+ replacer = lambda m: str(replacement)
803
+
804
+ if isinstance(pattern, str):
805
+ # Use string methods for literal string patterns
806
+ result = ''
807
+ position = 0
808
+ count = 0
809
+ while True:
810
+ if limit is not None and count >= limit:
811
+ result += string[position:]
812
+ break
813
+ index = string.find(pattern, position)
814
+ if index == -1:
815
+ result += string[position:]
816
+ break
817
+ result += string[position:index]
818
+ match = re.match(re.escape(pattern), string[index:])
819
+ result += replacer(match)
820
+ position = index + len(pattern)
821
+ count += 1
822
+ return result
823
+ else:
824
+ # Use regex for pattern objects
825
+ if limit is None:
826
+ return Functions.safe_replace_all(string, pattern, replacement)
827
+ else:
828
+ count = 0
829
+ result = string
830
+ while count < limit:
831
+ result = Functions.safe_replace_first(result, pattern, str(replacement))
832
+ count += 1
833
+ return result
834
+
835
+ #
836
+ # Base64 encode a string
837
+ # @param {String} str - string
838
+ # @returns {String} Base 64 encoding of the binary data
839
+ #
840
+ @staticmethod
841
+ def base64encode(string: Optional[str]) -> Optional[str]:
842
+ # undefined inputs always return undefined
843
+ if string is None:
844
+ return None
845
+
846
+ if string is utils.Utils.NULL_VALUE:
847
+ raise jexception.JException("T0410", -1)
848
+
849
+ try:
850
+ return base64.b64encode(string.encode("utf-8")).decode("utf-8")
851
+ except Exception as e:
852
+ return None
853
+
854
+ #
855
+ # Base64 decode a string
856
+ # @param {String} str - string
857
+ # @returns {String} Base 64 encoding of the binary data
858
+ #
859
+ @staticmethod
860
+ def base64decode(string: Optional[str]) -> Optional[str]:
861
+ # undefined inputs always return undefined
862
+ if string is None:
863
+ return None
864
+
865
+ if string is utils.Utils.NULL_VALUE:
866
+ raise jexception.JException("T0410", -1)
867
+
868
+ try:
869
+ return base64.b64decode(string.encode("utf-8")).decode("utf-8")
870
+ except Exception as e:
871
+ return None
872
+
873
+ #
874
+ # Encode a string into a component for a url
875
+ # @param {String} str - String to encode
876
+ # @returns {string} Encoded string
877
+ #
878
+ @staticmethod
879
+ def encode_url_component(string: Optional[str]) -> Optional[str]:
880
+ # undefined inputs always return undefined
881
+ if string is None:
882
+ return None
883
+
884
+ if string is utils.Utils.NULL_VALUE:
885
+ raise jexception.JException("T0410", -1)
886
+
887
+ # See https://stackoverflow.com/questions/946170/equivalent-javascript-functions-for-pythons-urllib-parse-quote-and-urllib-par
888
+ return urllib.parse.quote(string, safe="~()*!.'")
889
+
890
+ #
891
+ # Encode a string into a url
892
+ # @param {String} str - String to encode
893
+ # @returns {string} Encoded string
894
+ #
895
+ @staticmethod
896
+ def encode_url(string: Optional[str]) -> Optional[str]:
897
+ # undefined inputs always return undefined
898
+ if string is None:
899
+ return None
900
+
901
+ if string is utils.Utils.NULL_VALUE:
902
+ raise jexception.JException("T0410", -1)
903
+
904
+ # See https://stackoverflow.com/questions/946170/equivalent-javascript-functions-for-pythons-urllib-parse-quote-and-urllib-par
905
+ return urllib.parse.quote(string, safe="~@#$&()*!+=:;,.?/'")
906
+
907
+ #
908
+ # Decode a string from a component for a url
909
+ # @param {String} str - String to decode
910
+ # @returns {string} Decoded string
911
+ #
912
+ @staticmethod
913
+ def decode_url_component(string: Optional[str]) -> Optional[str]:
914
+ # undefined inputs always return undefined
915
+ if string is None:
916
+ return None
917
+
918
+ if string is utils.Utils.NULL_VALUE:
919
+ raise jexception.JException("T0410", -1)
920
+
921
+ # See https://stackoverflow.com/questions/946170/equivalent-javascript-functions-for-pythons-urllib-parse-quote-and-urllib-par
922
+ return urllib.parse.unquote(string, errors="strict")
923
+
924
+ #
925
+ # Decode a string from a url
926
+ # @param {String} str - String to decode
927
+ # @returns {string} Decoded string
928
+ #
929
+ @staticmethod
930
+ def decode_url(string: Optional[str]) -> Optional[str]:
931
+ # undefined inputs always return undefined
932
+ if string is None:
933
+ return None
934
+
935
+ if string is utils.Utils.NULL_VALUE:
936
+ raise jexception.JException("T0410", -1)
937
+
938
+ # See https://stackoverflow.com/questions/946170/equivalent-javascript-functions-for-pythons-urllib-parse-quote-and-urllib-par
939
+ return urllib.parse.unquote(string, errors="strict")
940
+
941
+ @staticmethod
942
+ def split(string: Optional[str], pattern: Union[str, Optional[re.Pattern]], limit: Optional[float]) -> Optional[list[str]]:
943
+ if string is None:
944
+ return None
945
+
946
+ if string is utils.Utils.NULL_VALUE:
947
+ raise jexception.JException("T0410", -1)
948
+
949
+ if limit is not None and int(limit) < 0:
950
+ raise jexception.JException("D3020", -1, string)
951
+
952
+ result = []
953
+ if limit is not None and int(limit) == 0:
954
+ return result
955
+
956
+ if isinstance(pattern, str):
957
+ sep = str(pattern)
958
+ if not sep:
959
+ # $split("str", ""): Split string into characters
960
+ lim = int(limit) if limit is not None else sys.maxsize
961
+ i = 0
962
+ while i < len(string) and i < lim:
963
+ result.append(string[i])
964
+ i += 1
965
+ else:
966
+ # Quote separator string + preserve trailing empty strings (-1)
967
+ result = string.split(sep, -1)
968
+ else:
969
+ result = pattern.split(string)
970
+ if limit is not None and int(limit) < len(result):
971
+ result = result[0:int(limit)]
972
+ return result
973
+
974
+ EXPONENT_PIC = re.compile(r'\d[eE]\d')
975
+
976
+ #
977
+ # Formats a number into a decimal string representation using XPath 3.1 F&O fn:format-number spec
978
+ # @param {number} value - number to format
979
+ # @param {String} picture - picture string definition
980
+ # @param {Object} [options] - override locale defaults
981
+ # @returns {String} The formatted string
982
+ #
983
+ # Adapted from https://github.com/sissaschool/elementpath
984
+ @staticmethod
985
+ def format_number(value: Optional[float], picture: Optional[str], decimal_format: Optional[Mapping[str, str]]) -> Optional[str]:
986
+ if decimal_format is None:
987
+ decimal_format = {}
988
+ pattern_separator = decimal_format.get('pattern-separator', ';')
989
+ sub_pictures = picture.split(pattern_separator)
990
+ if len(sub_pictures) > 2:
991
+ raise jexception.JException('D3080', -1)
992
+
993
+ decimal_separator = decimal_format.get('decimal-separator', '.')
994
+ if any(p.count(decimal_separator) > 1 for p in sub_pictures):
995
+ raise jexception.JException('D3081', -1)
996
+
997
+ percent_sign = decimal_format.get('percent', '%')
998
+ if any(p.count(percent_sign) > 1 for p in sub_pictures):
999
+ raise jexception.JException('D3082', -1)
1000
+
1001
+ per_mille_sign = decimal_format.get('per-mille', '‰')
1002
+ if any(p.count(per_mille_sign) > 1 for p in sub_pictures):
1003
+ raise jexception.JException('D3083', -1)
1004
+ if any(p.count(percent_sign) + p.count(per_mille_sign) > 1 for p in sub_pictures):
1005
+ raise jexception.JException('D3084')
1006
+
1007
+ zero_digit = decimal_format.get('zero-digit', '0')
1008
+ optional_digit = decimal_format.get('digit', '#')
1009
+ digits_family = ''.join(chr(cp + ord(zero_digit)) for cp in range(10))
1010
+ if any(optional_digit not in p and all(x not in p for x in digits_family)
1011
+ for p in sub_pictures):
1012
+ raise jexception.JException('D3085', -1)
1013
+
1014
+ grouping_separator = decimal_format.get('grouping-separator', ',')
1015
+ adjacent_pattern = re.compile(r'[\\%s\\%s]{2}' % (grouping_separator, decimal_separator))
1016
+ if any(adjacent_pattern.search(p) for p in sub_pictures):
1017
+ raise jexception.JException('D3087', -1)
1018
+
1019
+ if any(x.endswith(grouping_separator)
1020
+ for s in sub_pictures for x in s.split(decimal_separator)):
1021
+ raise jexception.JException('D3088', -1)
1022
+
1023
+ active_characters = digits_family + ''.join([
1024
+ decimal_separator, grouping_separator, pattern_separator, optional_digit
1025
+ ])
1026
+
1027
+ exponent_pattern = None
1028
+
1029
+ # Check optional exponent spec correctness in each sub-picture
1030
+ exponent_separator = decimal_format.get('exponent-separator', 'e')
1031
+ pattern = re.compile(r'(?<=[{0}]){1}[{0}]'.format(
1032
+ re.escape(active_characters), exponent_separator
1033
+ ))
1034
+ for p in sub_pictures:
1035
+ for match in pattern.finditer(p):
1036
+ if percent_sign in p or per_mille_sign in p:
1037
+ raise jexception.JException('D3092', -1)
1038
+ elif any(c not in digits_family for c in p[match.span()[1] - 1:]):
1039
+ # detailed check to consider suffix
1040
+ has_suffix = False
1041
+ for ch in p[match.span()[1] - 1:]:
1042
+ if ch in digits_family:
1043
+ if has_suffix:
1044
+ raise jexception.JException('D3093', -1)
1045
+ elif ch in active_characters:
1046
+ raise jexception.JException('D3086', -1)
1047
+ else:
1048
+ has_suffix = True
1049
+
1050
+ exponent_pattern = pattern
1051
+
1052
+ if value is None:
1053
+ return None
1054
+ elif math.isnan(value):
1055
+ return decimal_format.get('NaN', 'NaN')
1056
+ elif isinstance(value, float):
1057
+ value = decimal.Decimal.from_float(value)
1058
+ elif not isinstance(value, decimal.Decimal):
1059
+ value = decimal.Decimal(value)
1060
+
1061
+ minus_sign = decimal_format.get('minus-sign', '-')
1062
+
1063
+ prefix = ''
1064
+ if value >= 0:
1065
+ subpic = sub_pictures[0]
1066
+ else:
1067
+ subpic = sub_pictures[-1]
1068
+ if len(sub_pictures) == 1:
1069
+ prefix = minus_sign
1070
+
1071
+ for k, ch in enumerate(subpic):
1072
+ if ch in active_characters:
1073
+ prefix += subpic[:k]
1074
+ subpic = subpic[k:]
1075
+ break
1076
+ else:
1077
+ prefix += subpic
1078
+ subpic = ''
1079
+
1080
+ if not subpic:
1081
+ suffix = ''
1082
+ elif subpic.endswith(percent_sign):
1083
+ suffix = percent_sign
1084
+ subpic = subpic[:-len(percent_sign)]
1085
+
1086
+ if value.as_tuple().exponent < 0:
1087
+ value *= 100
1088
+ else:
1089
+ value = decimal.Decimal(int(value) * 100)
1090
+
1091
+ elif subpic.endswith(per_mille_sign):
1092
+ suffix = per_mille_sign
1093
+ subpic = subpic[:-len(per_mille_sign)]
1094
+
1095
+ if value.as_tuple().exponent < 0:
1096
+ value *= 1000
1097
+ else:
1098
+ value = decimal.Decimal(int(value) * 1000)
1099
+
1100
+ else:
1101
+ for k, ch in enumerate(reversed(subpic)):
1102
+ if ch in active_characters:
1103
+ idx = len(subpic) - k
1104
+ suffix = subpic[idx:]
1105
+ subpic = subpic[:idx]
1106
+ break
1107
+ else:
1108
+ suffix = subpic
1109
+ subpic = ''
1110
+
1111
+ exp_fmt = None
1112
+ if exponent_pattern is not None:
1113
+ exp_match = exponent_pattern.search(subpic)
1114
+ if exp_match is not None:
1115
+ exp_fmt = subpic[exp_match.span()[0] + 1:]
1116
+ subpic = subpic[:exp_match.span()[0]]
1117
+
1118
+ fmt_tokens = subpic.split(decimal_separator)
1119
+ if all(not fmt for fmt in fmt_tokens):
1120
+ raise jexception.JException('both integer and fractional parts are empty', -1)
1121
+
1122
+ if math.isinf(value):
1123
+ return prefix + decimal_format.get('infinity', '∞') + suffix
1124
+
1125
+ # Calculate the exponent value if it's in the sub-picture
1126
+ exp_value = 0
1127
+ if exp_fmt and value:
1128
+ num_digits = 0
1129
+ for ch in fmt_tokens[0]:
1130
+ if ch in digits_family:
1131
+ num_digits += 1
1132
+
1133
+ if abs(value) > 1:
1134
+ v = abs(value)
1135
+ while v > 10 ** num_digits:
1136
+ exp_value += 1
1137
+ v /= 10
1138
+
1139
+ # modify empty fractional part to store a digit
1140
+ if not num_digits:
1141
+ if len(fmt_tokens) == 1:
1142
+ fmt_tokens.append(zero_digit)
1143
+ elif not fmt_tokens[-1]:
1144
+ fmt_tokens[-1] = zero_digit
1145
+
1146
+ elif len(fmt_tokens) > 1 and fmt_tokens[-1] and value >= 0:
1147
+ v = abs(value) * 10
1148
+ while v < 10 ** num_digits:
1149
+ exp_value -= 1
1150
+ v *= 10
1151
+ else:
1152
+ v = abs(value) * 10
1153
+ while v < 10:
1154
+ exp_value -= 1
1155
+ v *= 10
1156
+
1157
+ if exp_value:
1158
+ value = value * decimal.Decimal(10) ** -exp_value
1159
+
1160
+ # round the value by fractional part
1161
+ if len(fmt_tokens) == 1 or not fmt_tokens[-1]:
1162
+ exp = decimal.Decimal('1')
1163
+ else:
1164
+ k = -1
1165
+ for ch in fmt_tokens[-1]:
1166
+ if ch in digits_family or ch == optional_digit:
1167
+ k += 1
1168
+ exp = decimal.Decimal('.' + '0' * k + '1')
1169
+
1170
+ try:
1171
+ if value > 0:
1172
+ value = value.quantize(exp, rounding='ROUND_HALF_UP')
1173
+ else:
1174
+ value = value.quantize(exp, rounding='ROUND_HALF_DOWN')
1175
+ except decimal.InvalidOperation:
1176
+ pass # number too large, don't round ...
1177
+
1178
+ chunks = Functions.decimal_to_string(value).lstrip('-').split('.')
1179
+ kwargs = {
1180
+ 'digits_family': digits_family,
1181
+ 'optional_digit': optional_digit,
1182
+ 'grouping_separator': grouping_separator,
1183
+ }
1184
+ result = Functions.format_digits(chunks[0], fmt_tokens[0], **kwargs)
1185
+
1186
+ if len(fmt_tokens) > 1 and fmt_tokens[0]:
1187
+ has_decimal_digit = False
1188
+ for ch in fmt_tokens[0]:
1189
+ if ch in digits_family:
1190
+ has_decimal_digit = True
1191
+ elif ch == optional_digit and has_decimal_digit:
1192
+ raise jexception.JException('D3090', -1)
1193
+
1194
+ if len(fmt_tokens) > 1 and fmt_tokens[-1]:
1195
+ has_optional_digit = False
1196
+ for ch in fmt_tokens[-1]:
1197
+ if ch == optional_digit:
1198
+ has_optional_digit = True
1199
+ elif ch in digits_family and has_optional_digit:
1200
+ raise jexception.JException('D3091', -1)
1201
+
1202
+ if len(chunks) == 1:
1203
+ chunks.append(zero_digit)
1204
+
1205
+ decimal_part = Functions.format_digits(chunks[1], fmt_tokens[-1], **kwargs)
1206
+
1207
+ for ch in reversed(fmt_tokens[-1]):
1208
+ if ch == optional_digit:
1209
+ if decimal_part and decimal_part[-1] == zero_digit:
1210
+ decimal_part = decimal_part[:-1]
1211
+ else:
1212
+ if not decimal_part:
1213
+ decimal_part = zero_digit
1214
+ break
1215
+
1216
+ if decimal_part:
1217
+ result += decimal_separator + decimal_part
1218
+
1219
+ if not fmt_tokens[0] and result.startswith(zero_digit):
1220
+ result = result.lstrip(zero_digit)
1221
+
1222
+ if exp_fmt:
1223
+ exp_digits = Functions.format_digits(str(abs(exp_value)), exp_fmt, **kwargs)
1224
+ if exp_value >= 0:
1225
+ result += f'{exponent_separator}{exp_digits}'
1226
+ else:
1227
+ result += f'{exponent_separator}-{exp_digits}'
1228
+
1229
+ return prefix + result + suffix
1230
+
1231
+ @staticmethod
1232
+ def decimal_to_string(value: decimal.Decimal) -> str:
1233
+ """
1234
+ Convert a Decimal value to a string representation
1235
+ that not includes exponent and with its decimals.
1236
+ """
1237
+ sign, digits, exponent = value.as_tuple()
1238
+
1239
+ if not exponent:
1240
+ result = ''.join(str(x) for x in digits)
1241
+ elif exponent > 0:
1242
+ result = ''.join(str(x) for x in digits) + '0' * exponent
1243
+ else:
1244
+ result = ''.join(str(x) for x in digits[:exponent])
1245
+ if not result:
1246
+ result = '0'
1247
+ result += '.'
1248
+ if len(digits) >= -exponent:
1249
+ result += ''.join(str(x) for x in digits[exponent:])
1250
+ else:
1251
+ result += '0' * (-exponent - len(digits))
1252
+ result += ''.join(str(x) for x in digits)
1253
+
1254
+ return '-' + result if sign else result
1255
+
1256
+ @staticmethod
1257
+ def format_digits(digits: str,
1258
+ fmt: str,
1259
+ digits_family: str = '0123456789',
1260
+ optional_digit: str = '#',
1261
+ grouping_separator: Optional[str] = None) -> str:
1262
+ result = []
1263
+ iter_num_digits = reversed(digits)
1264
+ num_digit = next(iter_num_digits)
1265
+
1266
+ for fmt_char in reversed(fmt):
1267
+ if fmt_char in digits_family or fmt_char == optional_digit:
1268
+ if num_digit:
1269
+ result.append(digits_family[ord(num_digit) - 48])
1270
+ num_digit = next(iter_num_digits, '')
1271
+ elif fmt_char != optional_digit:
1272
+ result.append(digits_family[0])
1273
+ elif not result or not result[-1] in digits_family and grouping_separator \
1274
+ and result[-1] != grouping_separator:
1275
+ raise jexception.JException("invalid grouping in picture argument", -1)
1276
+ else:
1277
+ result.append(fmt_char)
1278
+
1279
+ if num_digit:
1280
+ separator = ''
1281
+ sep = {x for x in fmt if x not in digits_family and x != optional_digit}
1282
+ if len(sep) != 1:
1283
+ repeat = None
1284
+ else:
1285
+ separator = sep.pop()
1286
+ chunks = fmt.split(separator)
1287
+
1288
+ if len(chunks[0]) > len(chunks[-1]):
1289
+ repeat = None
1290
+ elif all(len(item) == len(chunks[-1]) for item in chunks[1:-1]):
1291
+ repeat = len(chunks[-1]) + 1
1292
+ else:
1293
+ repeat = None
1294
+
1295
+ if repeat is None:
1296
+ while num_digit:
1297
+ result.append(digits_family[ord(num_digit) - 48])
1298
+ num_digit = next(iter_num_digits, '')
1299
+ else:
1300
+ while num_digit:
1301
+ if ((len(result) + 1) % repeat) == 0:
1302
+ result.append(separator)
1303
+ result.append(digits_family[ord(num_digit) - 48])
1304
+ num_digit = next(iter_num_digits, '')
1305
+
1306
+ if grouping_separator:
1307
+ return ''.join(reversed(result)).lstrip(grouping_separator)
1308
+ while result and \
1309
+ unicodedata.category(result[-1]) not in ('Nd', 'Nl', 'No', 'Lu', 'Ll', 'Lt', 'Lm', 'Lo'):
1310
+ result.pop()
1311
+ return ''.join(reversed(result))
1312
+
1313
+ #
1314
+ # Converts a number to a string using a specified number base
1315
+ # @param {number} value - the number to convert
1316
+ # @param {number} [radix] - the number base; must be between 2 and 36. Defaults to 10
1317
+ # @returns {string} - the converted string
1318
+ #
1319
+ @staticmethod
1320
+ def format_base(value: Optional[float], radix: Optional[float]) -> Optional[str]:
1321
+ # undefined inputs always return undefined
1322
+ if value is None:
1323
+ return None
1324
+
1325
+ value = Functions.round(value, 0)
1326
+
1327
+ if radix is None:
1328
+ radix = 10
1329
+ else:
1330
+ radix = int(radix)
1331
+
1332
+ if radix < 2 or radix > 36:
1333
+ raise jexception.JException("D3100", radix)
1334
+
1335
+ result = Functions.base_repr(int(value), radix).lower()
1336
+
1337
+ return result
1338
+
1339
+ @staticmethod
1340
+ def base_repr(number: int, base: int = 2, padding: int = 0) -> str:
1341
+ """
1342
+ Return a string representation of a number in the given base system.
1343
+
1344
+ Parameters
1345
+ ----------
1346
+ number : int
1347
+ The value to convert. Positive and negative values are handled.
1348
+ base : int, optional
1349
+ Convert `number` to the `base` number system. The valid range is 2-36,
1350
+ the default value is 2.
1351
+ padding : int, optional
1352
+ Number of zeros padded on the left. Default is 0 (no padding).
1353
+
1354
+ Returns
1355
+ -------
1356
+ out : str
1357
+ String representation of `number` in `base` system.
1358
+
1359
+ """
1360
+ digits = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
1361
+ if base > len(digits):
1362
+ raise ValueError("Bases greater than 36 not handled in base_repr.")
1363
+ elif base < 2:
1364
+ raise ValueError("Bases less than 2 not handled in base_repr.")
1365
+
1366
+ num = abs(number)
1367
+ res = []
1368
+ while num:
1369
+ res.append(digits[num % base])
1370
+ num //= base
1371
+ if padding:
1372
+ res.append('0' * padding)
1373
+ if number < 0:
1374
+ res.append('-')
1375
+ return ''.join(reversed(res or '0'))
1376
+
1377
+ #
1378
+ # Cast argument to number
1379
+ # @param {Object} arg - Argument
1380
+ # @throws NumberFormatException
1381
+ # @returns {Number} numeric value of argument
1382
+ #
1383
+ @staticmethod
1384
+ def number(arg: Optional[Any]) -> Optional[float]:
1385
+ result = None
1386
+
1387
+ # undefined inputs always return undefined
1388
+ if arg is None:
1389
+ return None
1390
+
1391
+ if arg is utils.Utils.NULL_VALUE:
1392
+ raise jexception.JException("T0410", -1)
1393
+
1394
+ if isinstance(arg, bool):
1395
+ result = 1 if (bool(arg)) else 0
1396
+ elif isinstance(arg, (int, float)):
1397
+ result = arg
1398
+ elif isinstance(arg, str):
1399
+ s = str(arg)
1400
+ if s.startswith("0x"):
1401
+ result = int(s[2:], 16)
1402
+ elif s.startswith("0B"):
1403
+ result = int(s[2:], 2)
1404
+ elif s.startswith("0O"):
1405
+ result = int(s[2:], 8)
1406
+ else:
1407
+ result = float(str(arg))
1408
+ return result
1409
+
1410
+ #
1411
+ # Absolute value of a number
1412
+ # @param {Number} arg - Argument
1413
+ # @returns {Number} absolute value of argument
1414
+ #
1415
+ @staticmethod
1416
+ def abs(arg: Optional[float]) -> Optional[float]:
1417
+
1418
+ # undefined inputs always return undefined
1419
+ if arg is None:
1420
+ return None
1421
+
1422
+ return abs(float(arg)) if isinstance(arg, float) else abs(int(arg))
1423
+
1424
+ #
1425
+ # Rounds a number down to integer
1426
+ # @param {Number} arg - Argument
1427
+ # @returns {Number} rounded integer
1428
+ #
1429
+ @staticmethod
1430
+ def floor(arg: Optional[float]) -> Optional[float]:
1431
+
1432
+ # undefined inputs always return undefined
1433
+ if arg is None:
1434
+ return None
1435
+
1436
+ return math.floor(float(arg))
1437
+
1438
+ #
1439
+ # Rounds a number up to integer
1440
+ # @param {Number} arg - Argument
1441
+ # @returns {Number} rounded integer
1442
+ #
1443
+ @staticmethod
1444
+ def ceil(arg: Optional[float]) -> Optional[float]:
1445
+
1446
+ # undefined inputs always return undefined
1447
+ if arg is None:
1448
+ return None
1449
+
1450
+ return math.ceil(float(arg))
1451
+
1452
+ #
1453
+ # Round to half even
1454
+ # @param {Number} arg - Argument
1455
+ # @param {Number} [precision] - number of decimal places
1456
+ # @returns {Number} rounded integer
1457
+ #
1458
+ @staticmethod
1459
+ def round(arg: Optional[float], precision: Optional[float]) -> Optional[float]:
1460
+
1461
+ # undefined inputs always return undefined
1462
+ if arg is None:
1463
+ return None
1464
+
1465
+ d = decimal.Decimal(str(arg))
1466
+ return float(round(d, precision))
1467
+
1468
+ #
1469
+ # Square root of number
1470
+ # @param {Number} arg - Argument
1471
+ # @returns {Number} square root
1472
+ #
1473
+ @staticmethod
1474
+ def sqrt(arg: Optional[float]) -> Optional[float]:
1475
+
1476
+ # undefined inputs always return undefined
1477
+ if arg is None:
1478
+ return None
1479
+
1480
+ if float(arg) < 0:
1481
+ raise jexception.JException("D3060", 1, arg)
1482
+
1483
+ return math.sqrt(float(arg))
1484
+
1485
+ #
1486
+ # Raises number to the power of the second number
1487
+ # @param {Number} arg - the base
1488
+ # @param {Number} exp - the exponent
1489
+ # @returns {Number} rounded integer
1490
+ #
1491
+ @staticmethod
1492
+ def power(arg: Optional[float], exp: Optional[float]) -> Optional[float]:
1493
+
1494
+ # undefined inputs always return undefined
1495
+ if arg is None:
1496
+ return None
1497
+
1498
+ result = float(arg) ** float(exp)
1499
+
1500
+ if not math.isfinite(result):
1501
+ raise jexception.JException("D3061", 1, arg, exp)
1502
+
1503
+ return result
1504
+
1505
+ #
1506
+ # Returns a random number 0 <= n < 1
1507
+ # @returns {number} random number
1508
+ #
1509
+ @staticmethod
1510
+ def random() -> float:
1511
+ return random.random()
1512
+
1513
+ #
1514
+ # Evaluate an input and return a boolean
1515
+ # @param {*} arg - Arguments
1516
+ # @returns {boolean} Boolean
1517
+ #
1518
+ @staticmethod
1519
+ def to_boolean(arg: Optional[Any]) -> Optional[bool]:
1520
+ from jsonata import jsonata
1521
+ # cast arg to its effective boolean value
1522
+ # boolean: unchanged
1523
+ # string: zero-length -> false; otherwise -> true
1524
+ # number: 0 -> false; otherwise -> true
1525
+ # null -> false
1526
+ # array: empty -> false; length > 1 -> true
1527
+ # object: empty -> false; non-empty -> true
1528
+ # function -> false
1529
+
1530
+ # undefined inputs always return undefined
1531
+ if arg is None:
1532
+ return None # Uli: Null would need to be handled as false anyway
1533
+
1534
+ result = False
1535
+ if isinstance(arg, list):
1536
+ el = arg
1537
+ if len(el) == 1:
1538
+ result = Functions.to_boolean(el[0])
1539
+ elif len(el) > 1:
1540
+ result = any(jsonata.Jsonata.boolize(e) for e in el)
1541
+ elif isinstance(arg, str):
1542
+ s = str(arg)
1543
+ if s:
1544
+ result = True
1545
+ elif isinstance(arg, bool):
1546
+ result = bool(arg)
1547
+ elif isinstance(arg, (int, float)):
1548
+ if float(arg) != 0:
1549
+ result = True
1550
+ elif isinstance(arg, dict):
1551
+ if arg:
1552
+ result = True
1553
+ return result
1554
+
1555
+ #
1556
+ # returns the Boolean NOT of the arg
1557
+ # @param {*} arg - argument
1558
+ # @returns {boolean} - NOT arg
1559
+ #
1560
+ @staticmethod
1561
+ def not_(arg: Optional[Any]) -> Optional[bool]:
1562
+ # undefined inputs always return undefined
1563
+ if arg is None:
1564
+ return None
1565
+
1566
+ return not Functions.to_boolean(arg)
1567
+
1568
+ @staticmethod
1569
+ def get_function_arity(func: Any) -> int:
1570
+ from jsonata import jsonata
1571
+ if isinstance(func, jsonata.Jsonata.JFunction):
1572
+ return func.signature.get_min_number_of_args()
1573
+ elif isinstance(func, jsonata.Jsonata.JLambda):
1574
+ from inspect import signature
1575
+
1576
+ return len(signature(func.function).parameters)
1577
+ else:
1578
+ return len(func.arguments)
1579
+
1580
+ #
1581
+ # Helper function to build the arguments to be supplied to the function arg of the
1582
+ # HOFs map, filter, each, sift and single
1583
+ # @param {function} func - the function to be invoked
1584
+ # @param {*} arg1 - the first (required) arg - the value
1585
+ # @param {*} arg2 - the second (optional) arg - the position (index or key)
1586
+ # @param {*} arg3 - the third (optional) arg - the whole structure (array or object)
1587
+ # @returns {*[]} the argument list
1588
+ #
1589
+ @staticmethod
1590
+ def hof_func_args(func: Any, arg1: Optional[Any], arg2: Optional[Any], arg3: Optional[Any]) -> list:
1591
+ func_args = [arg1]
1592
+ # the other two are optional - only supply it if the function can take it
1593
+ length = Functions.get_function_arity(func)
1594
+ if length >= 2:
1595
+ func_args.append(arg2)
1596
+ if length >= 3:
1597
+ func_args.append(arg3)
1598
+ return func_args
1599
+
1600
+ #
1601
+ # Call helper for Java
1602
+ #
1603
+ # @param func
1604
+ # @param funcArgs
1605
+ # @return
1606
+ # @throws Throwable
1607
+ #
1608
+ @staticmethod
1609
+ def func_apply(func: Any, func_args: Optional[Sequence]) -> Optional[Any]:
1610
+ from jsonata import jsonata
1611
+ res = None
1612
+ if Functions.is_lambda(func):
1613
+ res = jsonata.Jsonata.CURRENT.jsonata.apply(func, func_args, None,
1614
+ jsonata.Jsonata.CURRENT.jsonata.environment)
1615
+ else:
1616
+ res = func.call(None, func_args)
1617
+ return res
1618
+
1619
+ #
1620
+ # Create a map from an array of arguments
1621
+ # @param {Array} [arr] - array to map over
1622
+ # @param {Function} func - function to apply
1623
+ # @returns {Array} Map array
1624
+ #
1625
+ @staticmethod
1626
+ def map(arr: Optional[Sequence], func: Any) -> Optional[list]:
1627
+ # undefined inputs always return undefined
1628
+ if arr is None:
1629
+ return None
1630
+
1631
+ result = (res for res in (Functions.func_apply(func, Functions.hof_func_args(func, arg, i, arr))
1632
+ for i, arg in enumerate(arr)) if res is not None)
1633
+ return utils.Utils.create_sequence_from_iter(result)
1634
+
1635
+ #
1636
+ # Create a map from an array of arguments
1637
+ # @param {Array} [arr] - array to filter
1638
+ # @param {Function} func - predicate function
1639
+ # @returns {Array} Map array
1640
+ #
1641
+ @staticmethod
1642
+ def filter(arr: Optional[Sequence], func: Any) -> Optional[list]:
1643
+ # undefined inputs always return undefined
1644
+ if arr is None:
1645
+ return None
1646
+
1647
+ result = (arg for i, arg in enumerate(arr) if Functions.to_boolean(
1648
+ Functions.func_apply(func, Functions.hof_func_args(func, arg, i, arr))))
1649
+ return utils.Utils.create_sequence_from_iter(result)
1650
+
1651
+ #
1652
+ # Given an array, find the single element matching a specified condition
1653
+ # Throws an exception if the number of matching elements is not exactly one
1654
+ # @param {Array} [arr] - array to filter
1655
+ # @param {Function} [func] - predicate function
1656
+ # @returns {*} Matching element
1657
+ #
1658
+ @staticmethod
1659
+ def single(arr: Optional[Sequence], func: Any) -> Optional[Any]:
1660
+ # undefined inputs always return undefined
1661
+ if arr is None:
1662
+ return None
1663
+
1664
+ has_found_match = False
1665
+ result = None
1666
+
1667
+ for i, entry in enumerate(arr):
1668
+ positive_result = True
1669
+ if func is not None:
1670
+ func_args = Functions.hof_func_args(func, entry, i, arr)
1671
+ # invoke func
1672
+ res = Functions.func_apply(func, func_args)
1673
+ positive_result = Functions.to_boolean(res)
1674
+ if positive_result:
1675
+ if not has_found_match:
1676
+ result = entry
1677
+ has_found_match = True
1678
+ else:
1679
+ raise jexception.JException("D3138", i)
1680
+
1681
+ if not has_found_match:
1682
+ raise jexception.JException("D3139", -1)
1683
+
1684
+ return result
1685
+
1686
+ #
1687
+ # Convolves (zips) each value from a set of arrays
1688
+ # @param {Array} [args] - arrays to zip
1689
+ # @returns {Array} Zipped array
1690
+ #
1691
+ @staticmethod
1692
+ def zip(*args: Sequence) -> list:
1693
+ if any(a is None for a in args):
1694
+ return []
1695
+
1696
+ return [list(a) for a in zip(*args)]
1697
+
1698
+ #
1699
+ # Fold left function
1700
+ # @param {Array} sequence - Sequence
1701
+ # @param {Function} func - Function
1702
+ # @param {Object} init - Initial value
1703
+ # @returns {*} Result
1704
+ #
1705
+ @staticmethod
1706
+ def fold_left(sequence: Optional[Sequence], func: Any, init: Optional[Any]) -> Optional[Any]:
1707
+ # undefined inputs always return undefined
1708
+ if sequence is None:
1709
+ return None
1710
+ result = None
1711
+
1712
+ arity = Functions.get_function_arity(func)
1713
+ if arity < 2:
1714
+ raise jexception.JException("D3050", 1)
1715
+
1716
+ index = 0
1717
+ if init is None and sequence:
1718
+ result = sequence[0]
1719
+ index = 1
1720
+ else:
1721
+ result = init
1722
+ index = 0
1723
+
1724
+ while index < len(sequence):
1725
+ args = [result, sequence[index]]
1726
+ if arity >= 3:
1727
+ args.append(index)
1728
+ if arity >= 4:
1729
+ args.append(sequence)
1730
+ result = Functions.func_apply(func, args)
1731
+ index += 1
1732
+
1733
+ return result
1734
+
1735
+ #
1736
+ # Return keys for an object
1737
+ # @param {Object} arg - Object
1738
+ # @returns {Array} Array of keys
1739
+ #
1740
+ @staticmethod
1741
+ def keys(arg: Union[Sequence, Mapping, None]) -> list:
1742
+ if isinstance(arg, list):
1743
+ # merge the keys of all of the items in the array
1744
+ keys = {k: '' for el in arg for k in Functions.keys(el)}
1745
+ result = utils.Utils.create_sequence_from_iter(keys.keys())
1746
+ elif isinstance(arg, dict):
1747
+ result = utils.Utils.create_sequence_from_iter(arg.keys())
1748
+ else:
1749
+ result = utils.Utils.create_sequence()
1750
+
1751
+ return result
1752
+
1753
+ # here: append, lookup
1754
+
1755
+ #
1756
+ # Determines if the argument is undefined
1757
+ # @param {*} arg - argument
1758
+ # @returns {boolean} False if argument undefined, otherwise true
1759
+ #
1760
+ @staticmethod
1761
+ def exists(arg: Optional[Any]) -> bool:
1762
+ return arg is not None
1763
+
1764
+ #
1765
+ # Splits an object into an array of object with one property each
1766
+ # @param {*} arg - the object to split
1767
+ # @returns {*} - the array
1768
+ #
1769
+ @staticmethod
1770
+ def spread(arg: Optional[Any]) -> Optional[Any]:
1771
+ result = utils.Utils.create_sequence()
1772
+
1773
+ if isinstance(arg, list):
1774
+ # spread all of the items in the array
1775
+ for item in arg:
1776
+ result = Functions.append(result, Functions.spread(item))
1777
+ elif isinstance(arg, dict):
1778
+ for k, v in arg.items():
1779
+ obj = {k: v}
1780
+ result.append(obj)
1781
+ else:
1782
+ return arg # result = arg;
1783
+ return result
1784
+
1785
+ #
1786
+ # Merges an array of objects into a single object. Duplicate properties are
1787
+ # overridden by entries later in the array
1788
+ # @param {*} arg - the objects to merge
1789
+ # @returns {*} - the object
1790
+ #
1791
+ @staticmethod
1792
+ def merge(arg: Optional[Sequence[Mapping]]) -> Optional[dict]:
1793
+ # undefined inputs always return undefined
1794
+ if arg is None:
1795
+ return None
1796
+
1797
+ return {k: v for obj in arg for k, v in obj.items()}
1798
+
1799
+ #
1800
+ # Reverses the order of items in an array
1801
+ # @param {Array} arr - the array to reverse
1802
+ # @returns {Array} - the reversed array
1803
+ #
1804
+ @staticmethod
1805
+ def reverse(arr: Optional[Sequence]) -> Optional[Sequence]:
1806
+ # undefined inputs always return undefined
1807
+ if arr is None:
1808
+ return None
1809
+
1810
+ if len(arr) <= 1:
1811
+ return arr
1812
+
1813
+ result = list(arr)
1814
+ result.reverse()
1815
+ return result
1816
+
1817
+ #
1818
+ #
1819
+ # @param {*} obj - the input object to iterate over
1820
+ # @param {*} func - the function to apply to each key/value pair
1821
+ # @throws Throwable
1822
+ # @returns {Array} - the resultant array
1823
+ #
1824
+ @staticmethod
1825
+ def each(obj: Optional[Mapping], func: Any) -> Optional[list]:
1826
+ if obj is None:
1827
+ return None
1828
+
1829
+ result = (res for res in (Functions.func_apply(func, Functions.hof_func_args(func, value, key, obj))
1830
+ for key, value in obj.items()) if res is not None)
1831
+ return utils.Utils.create_sequence_from_iter(result)
1832
+
1833
+ #
1834
+ #
1835
+ # @param {string} [message] - the message to attach to the error
1836
+ # @throws custom error with code 'D3137'
1837
+ #
1838
+ @staticmethod
1839
+ def error(message: Optional[str]) -> NoReturn:
1840
+ raise jexception.JException("D3137", -1, message if message is not None else "$error() function evaluated")
1841
+
1842
+ #
1843
+ #
1844
+ # @param {boolean} condition - the condition to evaluate
1845
+ # @param {string} [message] - the message to attach to the error
1846
+ # @throws custom error with code 'D3137'
1847
+ # @returns {undefined}
1848
+ #
1849
+ @staticmethod
1850
+ def assert_fn(condition: Optional[bool], message: Optional[str]) -> None:
1851
+ if condition is utils.Utils.NULL_VALUE:
1852
+ raise jexception.JException("T0410", -1)
1853
+
1854
+ if not condition:
1855
+ raise jexception.JException("D3141", -1, "$assert() statement failed")
1856
+ # message: message || "$assert() statement failed"
1857
+
1858
+ #
1859
+ #
1860
+ # @param {*} [value] - the input to which the type will be checked
1861
+ # @returns {string} - the type of the input
1862
+ #
1863
+ @staticmethod
1864
+ def type(value: Optional[Any]) -> Optional[str]:
1865
+ if value is None:
1866
+ return None
1867
+
1868
+ if value is utils.Utils.NULL_VALUE:
1869
+ return "null"
1870
+
1871
+ if isinstance(value, bool):
1872
+ return "boolean"
1873
+
1874
+ if isinstance(value, (int, float)):
1875
+ return "number"
1876
+
1877
+ if isinstance(value, str):
1878
+ return "string"
1879
+
1880
+ if isinstance(value, list):
1881
+ return "array"
1882
+
1883
+ if utils.Utils.is_function(value) or Functions.is_lambda(value):
1884
+ return "function"
1885
+
1886
+ return "object"
1887
+
1888
+ #
1889
+ # Implements the merge sort (stable) with optional comparator function
1890
+ #
1891
+ # @param {Array} arr - the array to sort
1892
+ # @param {*} comparator - comparator function
1893
+ # @returns {Array} - sorted array
1894
+ #
1895
+ @staticmethod
1896
+ def sort(arr: Optional[Sequence], comparator: Optional[Any]) -> Optional[Sequence]:
1897
+ # undefined inputs always return undefined
1898
+ if arr is None:
1899
+ return None
1900
+
1901
+ if len(arr) <= 1:
1902
+ return arr
1903
+
1904
+ result = list(arr)
1905
+
1906
+ if comparator is not None:
1907
+ comp = Functions.Comparator(comparator).compare
1908
+ result = sorted(result, key=functools.cmp_to_key(comp))
1909
+ else:
1910
+ result = sorted(result)
1911
+
1912
+ return result
1913
+
1914
+ class Comparator:
1915
+ _comparator: Optional[Any]
1916
+
1917
+ def __init__(self, comparator):
1918
+ from jsonata import jsonata
1919
+ if isinstance(comparator, Callable):
1920
+ self._comparator = jsonata.Jsonata.JLambda(comparator)
1921
+ else:
1922
+ self._comparator = comparator
1923
+
1924
+ def compare(self, o1, o2):
1925
+ res = Functions.func_apply(self._comparator, [o1, o2])
1926
+ if isinstance(res, bool):
1927
+ return 1 if res else -1
1928
+ return int(res)
1929
+
1930
+ #
1931
+ # Randomly shuffles the contents of an array
1932
+ # @param {Array} arr - the input array
1933
+ # @returns {Array} the shuffled array
1934
+ #
1935
+ @staticmethod
1936
+ def shuffle(arr: Optional[Sequence]) -> Optional[Sequence]:
1937
+ # undefined inputs always return undefined
1938
+ if arr is None:
1939
+ return None
1940
+
1941
+ if len(arr) <= 1:
1942
+ return arr
1943
+
1944
+ result = list(arr)
1945
+ random.shuffle(result)
1946
+ return result
1947
+
1948
+ #
1949
+ # Returns the values that appear in a sequence, with duplicates eliminated.
1950
+ # @param {Array} arr - An array or sequence of values
1951
+ # @returns {Array} - sequence of distinct values
1952
+ #
1953
+ @staticmethod
1954
+ def distinct(arr: Optional[Any]) -> Optional[Any]:
1955
+ # undefined inputs always return undefined
1956
+ if arr is None:
1957
+ return None
1958
+
1959
+ if not (isinstance(arr, list)) or len(arr) <= 1:
1960
+ return arr
1961
+
1962
+ results = utils.Utils.create_sequence() if (isinstance(arr, utils.Utils.JList)) else []
1963
+
1964
+ for el in arr:
1965
+ if el not in results:
1966
+ results.append(el)
1967
+
1968
+ return results
1969
+
1970
+ #
1971
+ # Applies a predicate function to each key/value pair in an object, and returns an object containing
1972
+ # only the key/value pairs that passed the predicate
1973
+ #
1974
+ # @param {object} arg - the object to be sifted
1975
+ # @param {object} func - the predicate function (lambda or native)
1976
+ # @throws Throwable
1977
+ # @returns {object} - sifted object
1978
+ #
1979
+ @staticmethod
1980
+ def sift(arg: Optional[Mapping], func: Any) -> Optional[dict]:
1981
+ from jsonata import jsonata
1982
+ if arg is None:
1983
+ return None
1984
+
1985
+ result = {item: entry for item, entry in arg.items() if jsonata.Jsonata.boolize(
1986
+ Functions.func_apply(func, Functions.hof_func_args(func, entry, item, arg)))}
1987
+
1988
+ # empty objects should be changed to undefined
1989
+ if not result:
1990
+ result = None
1991
+
1992
+ return result
1993
+
1994
+ #
1995
+ # Append second argument to first
1996
+ # @param {Array|Object} arg1 - First argument
1997
+ # @param {Array|Object} arg2 - Second argument
1998
+ # @returns {*} Appended arguments
1999
+ #
2000
+ @staticmethod
2001
+ def append(arg1: Optional[Any], arg2: Optional[Any]) -> Optional[Any]:
2002
+ # disregard undefined args
2003
+ if arg1 is None:
2004
+ return arg2
2005
+ if arg2 is None:
2006
+ return arg1
2007
+
2008
+ # if either argument is not an array, make it so
2009
+ if not (isinstance(arg1, list)):
2010
+ arg1 = utils.Utils.create_sequence(arg1)
2011
+ if not (isinstance(arg2, list)):
2012
+ arg2 = utils.Utils.JList([arg2])
2013
+
2014
+ arg1 = utils.Utils.JList(arg1) # create a new copy!
2015
+ arg1.extend(arg2)
2016
+ return arg1
2017
+
2018
+ @staticmethod
2019
+ def is_lambda(result: Optional[Any]) -> bool:
2020
+ return isinstance(result, parser.Parser.Symbol) and result._jsonata_lambda
2021
+
2022
+ #
2023
+ # Return value from an object for a given key
2024
+ # @param {Object} input - Object/Array
2025
+ # @param {String} key - Key in object
2026
+ # @returns {*} Value of key in object
2027
+ #
2028
+ @staticmethod
2029
+ def lookup(input: Union[Mapping, Optional[Sequence]], key: Optional[str]) -> Optional[Any]:
2030
+ # lookup the 'name' item in the input
2031
+ result = None
2032
+ if isinstance(input, list):
2033
+ result = utils.Utils.create_sequence()
2034
+ for inp in input:
2035
+ res = Functions.lookup(inp, key)
2036
+ if res is not None:
2037
+ if isinstance(res, list):
2038
+ result.extend(res)
2039
+ else:
2040
+ result.append(res)
2041
+ elif isinstance(input, dict):
2042
+ result = input.get(key, utils.Utils.NONE)
2043
+ # Detect the case where the value is null:
2044
+ if result is None and key in input:
2045
+ result = utils.Utils.NULL_VALUE
2046
+ elif result is utils.Utils.NONE:
2047
+ result = None
2048
+ return result
2049
+
2050
+ @staticmethod
2051
+ def test(a: Optional[str], b: Optional[str]) -> str:
2052
+ return a + b
2053
+
2054
+ @staticmethod
2055
+ def get_function(clz: Optional[Type], name: Optional[str]) -> Optional[Any]:
2056
+ if name is None:
2057
+ return None
2058
+ return getattr(clz, name)
2059
+
2060
+ @staticmethod
2061
+ def call(clz: Optional[Type], name: Optional[str], args: Optional[Sequence]) -> Optional[Any]:
2062
+ m = Functions.get_function(clz, name)
2063
+ nargs = len(inspect.signature(m).parameters)
2064
+ return Functions._call(m, nargs, args)
2065
+
2066
+ @staticmethod
2067
+ def _call(m: Callable, nargs: int, args: Optional[Sequence]) -> Optional[Any]:
2068
+ call_args = list(args)
2069
+ while len(call_args) < nargs:
2070
+ # Add default arg null if not enough args were provided
2071
+ call_args.append(None)
2072
+
2073
+ res = m(*call_args)
2074
+ if utils.Utils.is_numeric(res):
2075
+ res = utils.Utils.convert_number(res)
2076
+ return res
2077
+
2078
+ #
2079
+ # DateTime
2080
+ #
2081
+
2082
+ #
2083
+ # Converts an ISO 8601 timestamp to milliseconds since the epoch
2084
+ #
2085
+ # @param {string} timestamp - the timestamp to be converted
2086
+ # @param {string} [picture] - the picture string defining the format of the timestamp (defaults to ISO 8601)
2087
+ # @throws ParseException
2088
+ # @returns {Number} - milliseconds since the epoch
2089
+ #
2090
+ @staticmethod
2091
+ def datetime_to_millis(timestamp: Optional[str], picture: Optional[str]) -> Optional[int]:
2092
+ # undefined inputs always return undefined
2093
+ if timestamp is None:
2094
+ return None
2095
+
2096
+ if picture is None:
2097
+ if Functions.is_numeric(timestamp):
2098
+ dt = datetime.datetime.strptime(timestamp, "%Y")
2099
+ else:
2100
+ dt = datetime.datetime.fromisoformat(timestamp)
2101
+ dt = dt.replace(tzinfo=datetime.timezone.utc)
2102
+ return int(dt.timestamp() * 1000)
2103
+ # try:
2104
+ # size = len(timestamp)
2105
+ # if size > 5:
2106
+ # if timestamp[size - 5] == '+' or timestamp[size - 5] == '-':
2107
+ # if (timestamp[size - 4]).isdigit() and (timestamp[size - 3]).isdigit() and (
2108
+ # timestamp[size - 2]).isdigit() and (timestamp[size - 1]).isdigit():
2109
+ # timestamp = timestamp[0:size - 2] + ':' + timestamp[size - 2:size]
2110
+ # return java.time.OffsetDateTime.parse(timestamp).toInstant().toEpochMilli()
2111
+ # except RuntimeError as e:
2112
+ # ldt = java.time.LocalDate.parse(timestamp, java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd"))
2113
+ # return ldt.atStartOfDay().atZone(java.time.ZoneId.of("UTC")).toInstant().toEpochMilli()
2114
+ else:
2115
+ return datetimeutils.DateTimeUtils.parse_datetime(timestamp, picture)
2116
+
2117
+ # Adapted from: org.apache.commons.lang3.StringUtils
2118
+ @staticmethod
2119
+ def is_numeric(cs: Optional[str]) -> bool:
2120
+ if cs is None or not cs:
2121
+ return False
2122
+ for c in cs:
2123
+ if not c.isdigit():
2124
+ return False
2125
+ return True
2126
+
2127
+ #
2128
+ # Converts milliseconds since the epoch to an ISO 8601 timestamp
2129
+ # @param {Number} millis - milliseconds since the epoch to be converted
2130
+ # @param {string} [picture] - the picture string defining the format of the timestamp (defaults to ISO 8601)
2131
+ # @param {string} [timezone] - the timezone to format the timestamp in (defaults to UTC)
2132
+ # @returns {String} - the formatted timestamp
2133
+ #
2134
+ @staticmethod
2135
+ def datetime_from_millis(millis: Optional[float], picture: Optional[str], timezone: Optional[str]) -> Optional[str]:
2136
+ # undefined inputs always return undefined
2137
+ if millis is None:
2138
+ return None
2139
+
2140
+ return datetimeutils.DateTimeUtils.format_datetime(int(millis), picture, timezone)
2141
+
2142
+ #
2143
+ # Formats an integer as specified by the XPath fn:format-integer function
2144
+ # See https://www.w3.org/TR/xpath-functions-31/#func-format-integer
2145
+ # @param {number} value - the number to be formatted
2146
+ # @param {string} picture - the picture string that specifies the format
2147
+ # @returns {string} - the formatted number
2148
+ #
2149
+ @staticmethod
2150
+ def format_integer(value: Optional[float], picture: Optional[str]) -> Optional[str]:
2151
+ if value is None:
2152
+ return None
2153
+ return datetimeutils.DateTimeUtils.format_integer(int(value), picture)
2154
+
2155
+ #
2156
+ # parse a string containing an integer as specified by the picture string
2157
+ # @param {string} value - the string to parse
2158
+ # @param {string} picture - the picture string
2159
+ # @throws ParseException
2160
+ # @returns {number} - the parsed number
2161
+ #
2162
+ @staticmethod
2163
+ def parse_integer(value: Optional[str], picture: Optional[str]) -> Optional[int]:
2164
+ if value is None:
2165
+ return None
2166
+ return datetimeutils.DateTimeUtils.parse_integer(value, picture)
2167
+
2168
+ #
2169
+ # Clones an object
2170
+ # @param {Object} arg - object to clone (deep copy)
2171
+ # @returns {*} - the cloned object
2172
+ #
2173
+ @staticmethod
2174
+ def function_clone(arg: Optional[Any]) -> Optional[Any]:
2175
+ # undefined inputs always return undefined
2176
+ if arg is None:
2177
+ return None
2178
+
2179
+ res = json.loads(Functions.string(arg, False))
2180
+ return res
2181
+
2182
+ #
2183
+ # parses and evaluates the supplied expression
2184
+ # @param {string} expr - expression to evaluate
2185
+ # @returns {*} - result of evaluating the expression
2186
+ #
2187
+ @staticmethod
2188
+ def function_eval(expr: Optional[str], focus: Optional[Any]) -> Optional[Any]:
2189
+ from jsonata import jsonata
2190
+ # undefined inputs always return undefined
2191
+ if expr is None:
2192
+ return None
2193
+ input = jsonata.Jsonata.CURRENT.jsonata.input # = this.input;
2194
+ if focus is not None:
2195
+ input = focus
2196
+ # if the input is a JSON array, then wrap it in a singleton sequence so it gets treated as a single input
2197
+ if (isinstance(input, list)) and not utils.Utils.is_sequence(input):
2198
+ input = utils.Utils.create_sequence(input)
2199
+ input.outer_wrapper = True
2200
+
2201
+ ast = None
2202
+ try:
2203
+ ast = jsonata.Jsonata(expr)
2204
+ except Exception as err:
2205
+ # error parsing the expression passed to $eval
2206
+ # populateMessage(err)
2207
+ raise jexception.JException("D3120", -1)
2208
+ result = None
2209
+ try:
2210
+ result = ast.evaluate(input, jsonata.Jsonata.CURRENT.jsonata.environment)
2211
+ except Exception as err:
2212
+ # error evaluating the expression passed to $eval
2213
+ # populateMessage(err)
2214
+ raise jexception.JException("D3121", -1)
2215
+
2216
+ return result
2217
+
2218
+ # environment.bind("now", defineFunction(function(picture, timezone) {
2219
+ # return datetime.fromMillis(timestamp.getTime(), picture, timezone)
2220
+ # }, "<s?s?:s>"))
2221
+ @staticmethod
2222
+ def now(picture: Optional[str], timezone: Optional[str]) -> Optional[str]:
2223
+ from jsonata import jsonata
2224
+ t = jsonata.Jsonata.CURRENT.jsonata.timestamp
2225
+ return Functions.datetime_from_millis(t, picture, timezone)
2226
+
2227
+ # environment.bind("millis", defineFunction(function() {
2228
+ # return timestamp.getTime()
2229
+ # }, "<:n>"))
2230
+ @staticmethod
2231
+ def millis() -> int:
2232
+ from jsonata import jsonata
2233
+ t = jsonata.Jsonata.CURRENT.jsonata.timestamp
2234
+ return t