jsonata-python 0.1.0__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.
@@ -0,0 +1,1144 @@
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: JSONata4Java
23
+ # (c) Copyright 2018, 2019 IBM Corporation
24
+ # Licensed under the Apache License, Version 2.0 (the "License")
25
+ # 1 New Orchard Road,
26
+ # Armonk, New York, 10504-1722
27
+ # United States
28
+ # +1 914 499 1900
29
+ # support: Nathaniel Mills wnm3@us.ibm.com
30
+ #
31
+
32
+ import datetime
33
+ import functools
34
+ import math
35
+ import re
36
+ from collections import deque
37
+ from dataclasses import dataclass
38
+ from enum import Enum
39
+ from typing import Optional, Sequence
40
+
41
+ from jsonata import constants
42
+
43
+
44
+ class DateTimeUtils:
45
+ _few = ["Zero", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Eleven", "Twelve",
46
+ "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"]
47
+ _ordinals = ["Zeroth", "First", "Second", "Third", "Fourth", "Fifth", "Sixth", "Seventh", "Eighth", "Ninth",
48
+ "Tenth", "Eleventh", "Twelfth", "Thirteenth", "Fourteenth", "Fifteenth", "Sixteenth", "Seventeenth",
49
+ "Eighteenth", "Nineteenth"]
50
+ _decades = ["Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety", "Hundred"]
51
+ _magnitudes = ["Thousand", "Million", "Billion", "Trillion"]
52
+
53
+ @staticmethod
54
+ def number_to_words(value: int, ordinal: bool) -> str:
55
+ return DateTimeUtils._lookup(value, False, ordinal)
56
+
57
+ @staticmethod
58
+ def _lookup(num: int, prev: int, ordinal: bool) -> str:
59
+ if num <= 19:
60
+ words = (" and " if prev else "") + (
61
+ DateTimeUtils._ordinals[int(num)] if ordinal else DateTimeUtils._few[int(num)])
62
+ elif num < 100:
63
+ tens = math.trunc(int(num) / float(10))
64
+ remainder = int(math.fmod(int(num), 10))
65
+ words = (" and " if prev else "") + DateTimeUtils._decades[tens - 2]
66
+ if remainder > 0:
67
+ words += "-" + DateTimeUtils._lookup(remainder, False, ordinal)
68
+ elif ordinal:
69
+ words = words[0:len(words) - 1] + "ieth"
70
+ elif num < 1000:
71
+ hundreds = math.trunc(int(num) / float(100))
72
+ remainder = int(math.fmod(int(num), 100))
73
+ words = (", " if prev else "") + DateTimeUtils._few[hundreds] + " Hundred"
74
+ if remainder > 0:
75
+ words += DateTimeUtils._lookup(remainder, True, ordinal)
76
+ elif ordinal:
77
+ words += "th"
78
+ else:
79
+ mag = int(math.floor(math.log10(num) / 3))
80
+ if mag > len(DateTimeUtils._magnitudes):
81
+ mag = len(DateTimeUtils._magnitudes) # the largest word
82
+ factor = int(10 ** (mag * 3))
83
+ mant = int(math.floor(math.trunc(num / float(factor))))
84
+ remainder = num - mant * factor
85
+ words = ((", " if prev else "") + DateTimeUtils._lookup(mant, False, False) +
86
+ " " + str(DateTimeUtils._magnitudes[mag - 1]))
87
+ if remainder > 0:
88
+ words += DateTimeUtils._lookup(remainder, True, ordinal)
89
+ elif ordinal:
90
+ words += "th"
91
+ return words
92
+
93
+ _word_values = {}
94
+
95
+ @staticmethod
96
+ def _static_initializer():
97
+ i = 0
98
+ while i < len(DateTimeUtils._few):
99
+ DateTimeUtils._word_values[DateTimeUtils._few[i].casefold()] = i
100
+ i += 1
101
+ i = 0
102
+ while i < len(DateTimeUtils._ordinals):
103
+ DateTimeUtils._word_values[DateTimeUtils._ordinals[i].casefold()] = i
104
+ i += 1
105
+ i = 0
106
+ while i < len(DateTimeUtils._decades):
107
+ lword = DateTimeUtils._decades[i].casefold()
108
+ DateTimeUtils._word_values[lword] = (i + 2) * 10
109
+ DateTimeUtils._word_values[lword[0:len(lword) - 1] + "ieth"] = DateTimeUtils._word_values[lword]
110
+ i += 1
111
+ DateTimeUtils._word_values["hundredth"] = 100
112
+ DateTimeUtils._word_values["hundreth"] = 100
113
+ i = 0
114
+ while i < len(DateTimeUtils._magnitudes):
115
+ lword = DateTimeUtils._magnitudes[i].casefold()
116
+ val = int(10 ** ((i + 1) * 3))
117
+ DateTimeUtils._word_values[lword] = val
118
+ DateTimeUtils._word_values[lword + "th"] = val
119
+ i += 1
120
+ i = 0
121
+ while i < len(DateTimeUtils._few):
122
+ DateTimeUtils._word_values_long[DateTimeUtils._few[i].casefold()] = int(i)
123
+ i += 1
124
+ i = 0
125
+ while i < len(DateTimeUtils._ordinals):
126
+ DateTimeUtils._word_values_long[DateTimeUtils._ordinals[i].casefold()] = int(i)
127
+ i += 1
128
+ i = 0
129
+ while i < len(DateTimeUtils._decades):
130
+ lword = DateTimeUtils._decades[i].casefold()
131
+ DateTimeUtils._word_values_long[lword] = int((i + 2)) * 10
132
+ DateTimeUtils._word_values_long[lword[0:len(lword) - 1] + "ieth"] = DateTimeUtils._word_values_long[lword]
133
+ i += 1
134
+ DateTimeUtils._word_values_long["hundredth"] = 100
135
+ DateTimeUtils._word_values_long["hundreth"] = 100
136
+ i = 0
137
+ while i < len(DateTimeUtils._magnitudes):
138
+ lword = DateTimeUtils._magnitudes[i].casefold()
139
+ val = int(10 ** ((i + 1) * 3))
140
+ DateTimeUtils._word_values_long[lword] = val
141
+ DateTimeUtils._word_values_long[lword + "th"] = val
142
+ i += 1
143
+
144
+ _word_values_long = {}
145
+
146
+ @staticmethod
147
+ def words_to_number(text: str) -> int:
148
+ parts = re.split(",\\s|\\sand\\s|[\\s\\-]", text)
149
+ values = [0 for _ in range(len(parts))]
150
+ i = 0
151
+ while i < len(parts):
152
+ values[i] = DateTimeUtils._word_values[parts[i]]
153
+ i += 1
154
+ segs = deque()
155
+ segs.append(0)
156
+ for value in values:
157
+ if value < 100:
158
+ top = segs.pop()
159
+ if top >= 1000:
160
+ segs.append(top)
161
+ top = 0
162
+ segs.append(top + value)
163
+ else:
164
+ segs.append(segs.pop() * value)
165
+ return sum(segs)
166
+
167
+ #
168
+ # long version of above
169
+ #
170
+ @staticmethod
171
+ def words_to_long(text: str) -> int:
172
+ parts = re.split(",\\s|\\sand\\s|[\\s\\-]", text)
173
+ values = [0 for _ in range(len(parts))]
174
+ i = 0
175
+ while i < len(parts):
176
+ values[i] = DateTimeUtils._word_values_long[parts[i]]
177
+ i += 1
178
+ segs = deque()
179
+ segs.append(int(0))
180
+ for value in values:
181
+ if value < 100:
182
+ top = segs.pop()
183
+ if top >= 1000:
184
+ segs.append(top)
185
+ top = int(0)
186
+ segs.append(top + value)
187
+ else:
188
+ segs.append(segs.pop() * value)
189
+ return sum(segs)
190
+
191
+ class RomanNumeral:
192
+ _value: int
193
+ _letters: str
194
+
195
+ def __init__(self, value, letters):
196
+ self._value = value
197
+ self._letters = letters
198
+
199
+ def get_value(self) -> int:
200
+ return self._value
201
+
202
+ def get_letters(self) -> str:
203
+ return self._letters
204
+
205
+ @staticmethod
206
+ def _create_roman_values() -> dict[str, int]:
207
+ values = {"M": 1000, "D": 500, "C": 100, "L": 50, "X": 10, "V": 5, "I": 1}
208
+ return values
209
+
210
+ _roman_numerals = [RomanNumeral(1000, "m"), RomanNumeral(900, "cm"), RomanNumeral(500, "d"),
211
+ RomanNumeral(400, "cd"),
212
+ RomanNumeral(100, "c"), RomanNumeral(90, "xc"), RomanNumeral(50, "l"), RomanNumeral(40, "xl"),
213
+ RomanNumeral(10, "x"), RomanNumeral(9, "ix"), RomanNumeral(5, "v"), RomanNumeral(4, "iv"),
214
+ RomanNumeral(1, "i")]
215
+
216
+ _roman_values = _create_roman_values.__func__()
217
+
218
+ @staticmethod
219
+ def _decimal_to_roman(value: int) -> str:
220
+ i = 0
221
+ while i < len(DateTimeUtils._roman_numerals):
222
+ numeral = DateTimeUtils._roman_numerals[i]
223
+ if value >= numeral.get_value():
224
+ return numeral.get_letters() + DateTimeUtils._decimal_to_roman(value - numeral.get_value())
225
+ i += 1
226
+ return ""
227
+
228
+ @staticmethod
229
+ def roman_to_decimal(roman: str) -> int:
230
+ decimal = 0
231
+ max = 1
232
+ for i in range(len(roman) - 1, -1, -1):
233
+ digit = roman[i]
234
+ value = DateTimeUtils._roman_values[digit]
235
+ if value < max:
236
+ decimal -= value
237
+ else:
238
+ max = value
239
+ decimal += value
240
+ return decimal
241
+
242
+ @staticmethod
243
+ def _decimal_to_letters(value: int, a_char: str) -> str:
244
+ letters = []
245
+ a_code = a_char[0]
246
+ while value > 0:
247
+ letters.insert(0, chr((value - 1) % 26 + ord(a_code)))
248
+ value = math.trunc((value - 1) / float(26))
249
+ return functools.reduce(lambda a, b: a + b, letters, "")
250
+
251
+ @staticmethod
252
+ def format_integer(value: int, picture: Optional[str]) -> str:
253
+ format = DateTimeUtils._analyse_integer_picture(picture)
254
+ return DateTimeUtils._format_integer(value, format)
255
+
256
+ @staticmethod
257
+ def parse_integer(value: Optional[str], picture: Optional[str]) -> Optional[int]:
258
+ format_spec = DateTimeUtils._analyse_integer_picture(picture)
259
+ match_spec = DateTimeUtils._generate_regex_with_component(None, format_spec)
260
+ # //const fullRegex = '^' + matchSpec.regex + '$'
261
+ # //const matcher = new RegExp(fullRegex)
262
+ # // TODO validate input based on the matcher regex
263
+ result = match_spec.parse(value)
264
+ return result
265
+
266
+ class Formats(Enum):
267
+ DECIMAL = "decimal"
268
+ LETTERS = "letters"
269
+ ROMAN = "roman"
270
+ WORDS = "words"
271
+ SEQUENCE = "sequence"
272
+
273
+ class TCase(Enum):
274
+ UPPER = "upper"
275
+ LOWER = "lower"
276
+ TITLE = "title"
277
+
278
+ class Format:
279
+ type: str
280
+ primary: 'DateTimeUtils.Formats'
281
+ caseType: 'DateTimeUtils.TCase'
282
+ ordinal: bool
283
+ zeroCode: int
284
+ mandatoryDigits: int
285
+ optionalDigits: int
286
+ regular: bool
287
+ groupingSeparators: list['DateTimeUtils.GroupingSeparator']
288
+ token: Optional[str]
289
+
290
+ def __init__(self):
291
+ self.type = "integer"
292
+ self.primary = DateTimeUtils.Formats.DECIMAL
293
+ self.case_type = DateTimeUtils.TCase.LOWER
294
+ self.ordinal = False
295
+ self.zeroCode = 0
296
+ self.mandatoryDigits = 0
297
+ self.optionalDigits = 0
298
+ self.regular = False
299
+ self.groupingSeparators = []
300
+ self.token = None
301
+
302
+ class GroupingSeparator:
303
+ position: int
304
+ character: str
305
+
306
+ def __init__(self, position, character):
307
+ self.position = position
308
+ self.character = character
309
+
310
+ @staticmethod
311
+ def _create_suffix_map() -> dict[str, str]:
312
+ suffix = {"1": "st", "2": "nd", "3": "rd"}
313
+ return suffix
314
+
315
+ _suffix123 = _create_suffix_map.__func__()
316
+
317
+ @staticmethod
318
+ def _format_integer(value: int, format: Optional[Format]) -> str:
319
+ from jsonata import functions
320
+ formatted_integer = ""
321
+ negative = value < 0
322
+ value = abs(value)
323
+ if format.primary == DateTimeUtils.Formats.LETTERS:
324
+ formatted_integer = DateTimeUtils._decimal_to_letters(int(value),
325
+ "A" if format.case_type == DateTimeUtils.TCase.UPPER else "a")
326
+ elif format.primary == DateTimeUtils.Formats.ROMAN:
327
+ formatted_integer = DateTimeUtils._decimal_to_roman(int(value))
328
+ if format.case_type == DateTimeUtils.TCase.UPPER:
329
+ formatted_integer = formatted_integer.upper()
330
+ elif format.primary == DateTimeUtils.Formats.WORDS:
331
+ formatted_integer = DateTimeUtils.number_to_words(value, format.ordinal)
332
+ if format.case_type == DateTimeUtils.TCase.UPPER:
333
+ formatted_integer = formatted_integer.upper()
334
+ elif format.case_type == DateTimeUtils.TCase.LOWER:
335
+ formatted_integer = formatted_integer.casefold()
336
+ elif format.primary == DateTimeUtils.Formats.DECIMAL:
337
+ formatted_integer = str(value)
338
+ pad_length = format.mandatoryDigits - len(formatted_integer)
339
+ if pad_length > 0:
340
+ formatted_integer = functions.Functions.left_pad(formatted_integer, format.mandatoryDigits, "0")
341
+ if format.zeroCode != 0x30:
342
+ chars = list(formatted_integer)
343
+ i = 0
344
+ while i < len(chars):
345
+ chars[i] = chr(ord(chars[i]) + format.zeroCode - 0x30)
346
+ i += 1
347
+ formatted_integer = ''.join(chars)
348
+ if format.regular:
349
+ n = int((len(formatted_integer) - 1) / format.groupingSeparators[0].position)
350
+ for i in range(n, 0, -1):
351
+ pos = len(formatted_integer) - i * format.groupingSeparators[0].position
352
+ formatted_integer = formatted_integer[0:pos] + format.groupingSeparators[
353
+ 0].character + formatted_integer[pos:]
354
+ else:
355
+ format.groupingSeparators.reverse()
356
+ for separator in format.groupingSeparators:
357
+ pos = len(formatted_integer) - separator.position
358
+ formatted_integer = formatted_integer[0:pos] + separator.character + formatted_integer[pos:]
359
+
360
+ if format.ordinal:
361
+ last_digit = formatted_integer[len(formatted_integer) - 1:]
362
+ suffix = DateTimeUtils._suffix123.get(last_digit)
363
+ if suffix is None or (
364
+ len(formatted_integer) > 1 and formatted_integer[len(formatted_integer) - 2] == '1'):
365
+ suffix = "th"
366
+ formatted_integer += suffix
367
+ elif format.primary == DateTimeUtils.Formats.SEQUENCE:
368
+ raise RuntimeError(constants.Constants.ERR_MSG_SEQUENCE_UNSUPPORTED.format(format.token))
369
+ if negative:
370
+ formatted_integer = "-" + formatted_integer
371
+
372
+ return formatted_integer
373
+
374
+ _decimal_groups = [0x30, 0x0660, 0x06F0, 0x07C0, 0x0966, 0x09E6, 0x0A66, 0x0AE6, 0x0B66, 0x0BE6, 0x0C66, 0x0CE6,
375
+ 0x0D66, 0x0DE6, 0x0E50, 0x0ED0, 0x0F20, 0x1040, 0x1090, 0x17E0, 0x1810, 0x1946, 0x19D0, 0x1A80,
376
+ 0x1A90, 0x1B50, 0x1BB0, 0x1C40, 0x1C50, 0xA620, 0xA8D0, 0xA900, 0xA9D0, 0xA9F0, 0xAA50, 0xABF0,
377
+ 0xFF10]
378
+
379
+ @staticmethod
380
+ def _analyse_integer_picture(picture: Optional[str]) -> Format:
381
+ format = DateTimeUtils.Format()
382
+ primary_format = None
383
+ format_modifier = None
384
+ semicolon = picture.rfind(";")
385
+ if semicolon == -1:
386
+ primary_format = picture
387
+ else:
388
+ primary_format = picture[0:semicolon]
389
+ format_modifier = picture[semicolon + 1:]
390
+ if format_modifier[0] == 'o':
391
+ format.ordinal = True
392
+
393
+ if primary_format == "A":
394
+ format.case_type = DateTimeUtils.TCase.UPPER
395
+ format.primary = DateTimeUtils.Formats.LETTERS
396
+ elif primary_format == "a":
397
+ format.primary = DateTimeUtils.Formats.LETTERS
398
+ elif primary_format == "I":
399
+ format.case_type = DateTimeUtils.TCase.UPPER
400
+ format.primary = DateTimeUtils.Formats.ROMAN
401
+ elif primary_format == "i":
402
+ format.primary = DateTimeUtils.Formats.ROMAN
403
+ elif primary_format == "W":
404
+ format.case_type = DateTimeUtils.TCase.UPPER
405
+ format.primary = DateTimeUtils.Formats.WORDS
406
+ elif primary_format == "Ww":
407
+ format.case_type = DateTimeUtils.TCase.TITLE
408
+ format.primary = DateTimeUtils.Formats.WORDS
409
+ elif primary_format == "w":
410
+ format.primary = DateTimeUtils.Formats.WORDS
411
+ else:
412
+ zero_code = None
413
+ mandatory_digits = 0
414
+ optional_digits = 0
415
+ grouping_separators = []
416
+ separator_position = 0
417
+ format_codepoints = list(primary_format)
418
+ # ArrayUtils.reverse(format_codepoints)
419
+ for ix in range(len(format_codepoints) - 1, -1, -1):
420
+ code_point = format_codepoints[ix]
421
+ digit = False
422
+ i = 0
423
+ while i < len(DateTimeUtils._decimal_groups):
424
+ group = DateTimeUtils._decimal_groups[i]
425
+ if chr(group) <= code_point <= chr(group + 9):
426
+ digit = True
427
+ mandatory_digits += 1
428
+ separator_position += 1
429
+ if zero_code is None:
430
+ zero_code = group
431
+ elif group != zero_code:
432
+ raise RuntimeError(constants.Constants.ERR_MSG_DIFF_DECIMAL_GROUP)
433
+ break
434
+ i += 1
435
+ if not digit:
436
+ if code_point == chr(0x23):
437
+ separator_position += 1
438
+ optional_digits += 1
439
+ else:
440
+ grouping_separators.append(DateTimeUtils.GroupingSeparator(separator_position, code_point))
441
+ if mandatory_digits > 0:
442
+ format.primary = DateTimeUtils.Formats.DECIMAL
443
+ format.zeroCode = zero_code
444
+ format.mandatoryDigits = mandatory_digits
445
+ format.optionalDigits = optional_digits
446
+
447
+ regular = DateTimeUtils._get_regular_repeat(grouping_separators)
448
+ if regular > 0:
449
+ format.regular = True
450
+ format.groupingSeparators.append(
451
+ DateTimeUtils.GroupingSeparator(regular, grouping_separators[0].character))
452
+ else:
453
+ format.regular = False
454
+ format.groupingSeparators = grouping_separators
455
+ else:
456
+ format.primary = DateTimeUtils.Formats.SEQUENCE
457
+ format.token = primary_format
458
+
459
+ return format
460
+
461
+ @staticmethod
462
+ def _get_regular_repeat(separators: Sequence['DateTimeUtils.GroupingSeparator']) -> int:
463
+ if len(separators) == 0:
464
+ return 0
465
+
466
+ sep_char = separators[0].character
467
+ for i in range(1, len(separators)):
468
+ if separators[i].character is not sep_char:
469
+ return 0
470
+
471
+ indexes = list(map(lambda separator: separator.position, separators))
472
+ factor = int(functools.reduce(math.gcd, indexes))
473
+ for index in range(1, len(indexes) + 1):
474
+ if (indexes.index(index * factor) if index * factor in indexes else -1) == -1:
475
+ return 0
476
+ return factor
477
+
478
+ @staticmethod
479
+ def _create_default_presentation_modifiers() -> dict[str, str]:
480
+ map = {'Y': "1", 'M': "1", 'D': "1", 'd': "1", 'F': "n", 'W': "1", 'w': "1", 'X': "1", 'x': "1", 'H': "1",
481
+ 'h': "1", 'P': "n", 'm': "01", 's': "01", 'f': "1", 'Z': "01:01", 'z': "01:01", 'C': "n", 'E': "n"}
482
+ return map
483
+
484
+ _default_presentation_modifiers = _create_default_presentation_modifiers.__func__()
485
+
486
+ class PictureFormat:
487
+ type: str
488
+ parts: list['DateTimeUtils.SpecPart']
489
+
490
+ def __init__(self, type):
491
+ self.type = type
492
+ self.parts = []
493
+
494
+ def add_literal(self, picture: str, start: int, end: int) -> None:
495
+ if end > start:
496
+ literal = picture[start:end]
497
+ if literal == "]]":
498
+ # handle special case where picture ends with ]], split yields empty array
499
+ literal = "]"
500
+ else:
501
+ literal = "]".join(literal.split("]]"))
502
+ self.parts.append(DateTimeUtils.SpecPart("literal", value=literal))
503
+
504
+ class SpecPart:
505
+ type: str
506
+ value: Optional[str]
507
+ component: str
508
+ width: (int, int)
509
+ presentation1: Optional[str]
510
+ presentation2: Optional[str]
511
+ ordinal: bool
512
+ names: 'Optional[DateTimeUtils.TCase]'
513
+ integerFormat: 'Optional[DateTimeUtils.Format]'
514
+ n: int
515
+
516
+ def __init__(self, type, component=None, value=None):
517
+ self.type = type
518
+ self.component = component
519
+ self.value = value
520
+
521
+ self.width = None
522
+ self.presentation1 = None
523
+ self.presentation2 = None
524
+ self.ordinal = False
525
+ self.names = None
526
+ self.integerFormat = None
527
+ self.n = 0
528
+
529
+ @staticmethod
530
+ def _analyse_datetime_picture(picture: str) -> PictureFormat:
531
+ format = DateTimeUtils.PictureFormat("datetime")
532
+ start = 0
533
+ pos = 0
534
+ while pos < len(picture):
535
+ if picture[pos] == '[':
536
+ # check it's not a doubled [[
537
+ if picture[pos + 1] == '[':
538
+ # literal [
539
+ format.add_literal(picture, start, pos)
540
+ format.parts.append(DateTimeUtils.SpecPart("literal", value="["))
541
+ pos += 2
542
+ start = pos
543
+ continue
544
+ format.add_literal(picture, start, pos)
545
+ start = pos
546
+ pos = picture.find("]", start)
547
+ if pos == -1:
548
+ raise RuntimeError(constants.Constants.ERR_MSG_NO_CLOSING_BRACKET)
549
+ marker = picture[start + 1:pos]
550
+ marker = "".join(re.split("\\s+", marker))
551
+ def_ = DateTimeUtils.SpecPart("marker", component=marker[0])
552
+ comma = marker.rfind(",")
553
+ pres_mod = None
554
+ if comma != -1:
555
+ width_mod = marker[comma + 1:]
556
+ dash = width_mod.find("-")
557
+ min = None
558
+ max = None
559
+ if dash == -1:
560
+ min = width_mod
561
+ else:
562
+ min = width_mod[0:dash]
563
+ max = width_mod[dash + 1:]
564
+ def_.width = (DateTimeUtils._parse_width(min), DateTimeUtils._parse_width(max))
565
+ pres_mod = marker[1:comma]
566
+ else:
567
+ pres_mod = marker[1:]
568
+ if len(pres_mod) == 1:
569
+ def_.presentation1 = pres_mod
570
+ elif len(pres_mod) > 1:
571
+ last_char = pres_mod[len(pres_mod) - 1]
572
+ if "atco".find(last_char) != -1:
573
+ def_.presentation2 = last_char
574
+ if last_char == 'o':
575
+ def_.ordinal = True
576
+ def_.presentation1 = pres_mod[0:len(pres_mod) - 1]
577
+ else:
578
+ def_.presentation1 = pres_mod
579
+ else:
580
+ def_.presentation1 = DateTimeUtils._default_presentation_modifiers[def_.component]
581
+ if def_.presentation1 is None:
582
+ raise RuntimeError(constants.Constants.ERR_MSG_UNKNOWN_COMPONENT_SPECIFIER.format(def_.component))
583
+ if def_.presentation1[0] == 'n':
584
+ def_.names = DateTimeUtils.TCase.LOWER
585
+ elif def_.presentation1[0] == 'N':
586
+ if len(def_.presentation1) > 1 and def_.presentation1[1] == 'n':
587
+ def_.names = DateTimeUtils.TCase.TITLE
588
+ else:
589
+ def_.names = DateTimeUtils.TCase.UPPER
590
+ elif "YMDdFWwXxHhmsf".find(def_.component) != -1:
591
+ integer_pattern = def_.presentation1
592
+ if def_.presentation2 is not None:
593
+ integer_pattern += ";" + def_.presentation2
594
+ def_.integerFormat = DateTimeUtils._analyse_integer_picture(integer_pattern)
595
+ def_.integerFormat.ordinal = def_.ordinal
596
+ if def_.width is not None and def_.width[0] is not None:
597
+ if def_.integerFormat.mandatoryDigits < def_.width[0]:
598
+ def_.integerFormat.mandatoryDigits = def_.width[0]
599
+ if def_.component == 'Y':
600
+ def_.n = -1
601
+ if def_.width is not None and def_.width[1] is not None:
602
+ def_.n = def_.width[1]
603
+ def_.integerFormat.mandatoryDigits = def_.n
604
+ else:
605
+ w = def_.integerFormat.mandatoryDigits + def_.integerFormat.optionalDigits
606
+ if w >= 2:
607
+ def_.n = w
608
+ if def_.component == 'Z' or def_.component == 'z':
609
+ def_.integerFormat = DateTimeUtils._analyse_integer_picture(def_.presentation1)
610
+ def_.integerFormat.ordinal = def_.ordinal
611
+ format.parts.append(def_)
612
+ start = pos + 1
613
+ pos += 1
614
+ format.add_literal(picture, start, pos)
615
+ return format
616
+
617
+ @staticmethod
618
+ def _parse_width(wm: Optional[str]) -> Optional[int]:
619
+ if wm is None or wm == "*":
620
+ return None
621
+ else:
622
+ return int(wm)
623
+
624
+ _days = ["", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
625
+ _months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October",
626
+ "November", "December"]
627
+
628
+ _iso8601_spec = None
629
+
630
+ @staticmethod
631
+ def format_datetime(millis: int, picture: Optional[str], timezone: Optional[str]) -> str:
632
+ offset_hours = 0
633
+ offset_minutes = 0
634
+
635
+ if timezone is not None:
636
+ offset = int(timezone)
637
+ offset_hours = math.trunc(offset / float(100))
638
+ offset_minutes = int(math.fmod(offset, 100))
639
+ format_spec = None
640
+ if picture is None:
641
+ if DateTimeUtils._iso8601_spec is None:
642
+ DateTimeUtils._iso8601_spec = DateTimeUtils._analyse_datetime_picture(
643
+ "[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01].[f001][Z01:01t]")
644
+ format_spec = DateTimeUtils._iso8601_spec
645
+ else:
646
+ format_spec = DateTimeUtils._analyse_datetime_picture(picture)
647
+
648
+ offset_millis = (60 * offset_hours + offset_minutes) * 60 * 1000
649
+ date_time = datetime.datetime.fromtimestamp((millis + offset_millis) / 1000.0, datetime.timezone.utc)
650
+ result = ""
651
+ for part in format_spec.parts:
652
+ if part.type == "literal":
653
+ result += part.value
654
+ else:
655
+ result += DateTimeUtils._format_component(date_time, part, offset_hours, offset_minutes)
656
+
657
+ return result
658
+
659
+ @staticmethod
660
+ def _format_component(date: datetime.datetime, marker_spec: SpecPart, offset_hours: int,
661
+ offset_minutes: int) -> str:
662
+ component_value = DateTimeUtils._get_datetime_fragment(date, marker_spec.component)
663
+
664
+ if "YMDdFWwXxHhms".find(marker_spec.component) != -1:
665
+ if marker_spec.component == 'Y':
666
+ if marker_spec.n != -1:
667
+ component_value = str(int((int(math.fmod(int(float(component_value)), 10 ** marker_spec.n)))))
668
+ if marker_spec.names is not None:
669
+ if marker_spec.component == 'M' or marker_spec.component == 'x':
670
+ component_value = DateTimeUtils._months[int(float(component_value)) - 1]
671
+ elif marker_spec.component == 'F':
672
+ component_value = DateTimeUtils._days[int(float(component_value))]
673
+ else:
674
+ raise RuntimeError(constants.Constants.ERR_MSG_INVALID_NAME_MODIFIER.format(marker_spec.component))
675
+ if marker_spec.names == DateTimeUtils.TCase.UPPER:
676
+ component_value = component_value.upper()
677
+ elif marker_spec.names == DateTimeUtils.TCase.LOWER:
678
+ component_value = component_value.casefold()
679
+ if marker_spec.width is not None and len(component_value) > marker_spec.width[1]:
680
+ component_value = component_value[0:marker_spec.width[1]]
681
+ else:
682
+ component_value = DateTimeUtils._format_integer(int(float(component_value)), marker_spec.integerFormat)
683
+ elif marker_spec.component == 'f':
684
+ component_value = DateTimeUtils._format_integer(int(float(component_value)), marker_spec.integerFormat)
685
+ elif marker_spec.component == 'Z' or marker_spec.component == 'z':
686
+ offset = offset_hours * 100 + offset_minutes
687
+ if marker_spec.integerFormat.regular:
688
+ component_value = DateTimeUtils._format_integer(offset, marker_spec.integerFormat)
689
+ else:
690
+ num_digits = marker_spec.integerFormat.mandatoryDigits
691
+ if num_digits == 1 or num_digits == 2:
692
+ component_value = DateTimeUtils._format_integer(offset_hours, marker_spec.integerFormat)
693
+ if offset_minutes != 0:
694
+ component_value += ":" + DateTimeUtils.format_integer(offset_minutes, "00")
695
+ elif num_digits == 3 or num_digits == 4:
696
+ component_value = DateTimeUtils._format_integer(offset, marker_spec.integerFormat)
697
+ else:
698
+ raise RuntimeError(constants.Constants.ERR_MSG_TIMEZONE_FORMAT)
699
+ if offset >= 0:
700
+ component_value = "+" + component_value
701
+ if marker_spec.component == 'z':
702
+ component_value = "GMT" + component_value
703
+ if offset == 0 and marker_spec.presentation2 is not None and marker_spec.presentation2 == 't':
704
+ component_value = "Z"
705
+ elif marker_spec.component == 'P':
706
+ # §9.8.4.7 Formatting Other Components
707
+ # Formatting P for am/pm
708
+ # getDateTimeFragment() always returns am/pm lower case so check for UPPER here
709
+ if marker_spec.names == DateTimeUtils.TCase.UPPER:
710
+ component_value = component_value.upper()
711
+ return component_value
712
+
713
+ @staticmethod
714
+ def _get_datetime_fragment(date: datetime.datetime, component: str) -> str:
715
+ component_value = ""
716
+ if component == 'Y': # year
717
+ component_value = str(date.year)
718
+ elif component == 'M': # month in year
719
+ component_value = str(date.month)
720
+ elif component == 'D': # day in month
721
+ component_value = str(date.day)
722
+ elif component == 'd': # day in year
723
+ component_value = str(date.timetuple().tm_yday)
724
+ elif component == 'F': # day of week
725
+ component_value = str(date.isoweekday())
726
+ elif component == 'W': # week in year
727
+ component_value = str(date.isocalendar().week)
728
+ elif component == 'w': # week in month
729
+ component_value = str(DateTimeUtils.week_in_month(date))
730
+ elif component == 'X':
731
+ component_value = str(DateTimeUtils.iso_week_numbering_year(date))
732
+ elif component == 'x':
733
+ component_value = str(DateTimeUtils.iso_week_numbering_month(date))
734
+ elif component == 'H': # hour in day (24 hours)
735
+ component_value = str(date.hour)
736
+ elif component == 'h': # hour in day (12 hours)
737
+ hour = date.hour
738
+ if hour > 12:
739
+ hour -= 12
740
+ elif hour == 0:
741
+ hour = 12
742
+ component_value = str(hour)
743
+ elif component == 'P':
744
+ component_value = "am" if date.hour < 12 else "pm"
745
+ elif component == 'm':
746
+ component_value = str(date.minute)
747
+ elif component == 's':
748
+ component_value = str(date.second)
749
+ elif component == 'f':
750
+ component_value = str(date.microsecond / 1000.0)
751
+ elif component == 'Z' or component == 'z':
752
+ pass
753
+ elif component == 'C':
754
+ component_value = "ISO"
755
+ elif component == 'E':
756
+ component_value = "ISO"
757
+ return component_value
758
+
759
+ @staticmethod
760
+ def week_in_month(dt: datetime.datetime) -> int:
761
+ this_month = DateTimeUtils.YearMonth(dt.year, dt.month)
762
+ start_of_week1 = DateTimeUtils.start_of_first_week(this_month)
763
+ today = datetime.date(this_month.year, this_month.month, dt.day)
764
+ week = DateTimeUtils.delta_weeks(start_of_week1, today)
765
+ if week > 4:
766
+ start_of_following_month = DateTimeUtils.start_of_first_week(this_month.next_month())
767
+ if today >= start_of_following_month:
768
+ week = 1
769
+ elif week < 1:
770
+ start_of_previous_month = DateTimeUtils.start_of_first_week(this_month.previous_month())
771
+ week = DateTimeUtils.delta_weeks(start_of_previous_month, today)
772
+ return math.floor(week)
773
+
774
+ @staticmethod
775
+ def iso_week_numbering_year(dt: datetime.datetime) -> int:
776
+ this_year = DateTimeUtils.YearMonth(dt.year, 1)
777
+ start_of_iso_year = DateTimeUtils.start_of_first_week(this_year)
778
+ end_of_iso_year = DateTimeUtils.start_of_first_week(this_year.next_year())
779
+ now = datetime.date(dt.year, dt.month, dt.day)
780
+ if now < start_of_iso_year:
781
+ return this_year.year - 1
782
+ elif now >= end_of_iso_year:
783
+ return this_year.year + 1
784
+ else:
785
+ return this_year.year
786
+
787
+ @staticmethod
788
+ def iso_week_numbering_month(dt: datetime.datetime) -> int:
789
+ this_month = DateTimeUtils.YearMonth(dt.year, dt.month)
790
+ start_of_iso_month = DateTimeUtils.start_of_first_week(this_month)
791
+ next_month = this_month.next_month()
792
+ end_of_iso_month = DateTimeUtils.start_of_first_week(next_month)
793
+ now = datetime.date(dt.year, dt.month, dt.day)
794
+ if now < start_of_iso_month:
795
+ return this_month.previous_month().month
796
+ elif now >= end_of_iso_month:
797
+ return next_month.month
798
+ else:
799
+ return this_month.month
800
+
801
+ @staticmethod
802
+ def start_of_first_week(year_month: 'DateTimeUtils.YearMonth') -> datetime.date:
803
+ # // ISO 8601 defines the first week of the year to be the week that contains the first Thursday
804
+ # // XPath F&O extends this same definition for the first week of a month
805
+ # // the week starts on a Monday - calculate the millis for the start of the first week
806
+ # // millis for given 1st Jan of that year (at 00:00 UTC)
807
+ jan1 = datetime.date(year_month.year, year_month.month, 1)
808
+ day_of_jan1 = jan1.isoweekday()
809
+ # if Jan 1 is Fri, Sat or Sun, then add the number of days ( in millis) to jan1 to get the start of week 1
810
+ return jan1 + datetime.timedelta(days=8 - day_of_jan1) if day_of_jan1 > 4 else jan1 - datetime.timedelta(
811
+ days=day_of_jan1 - 1)
812
+
813
+ @dataclass
814
+ class YearMonth:
815
+ year: int
816
+ month: int
817
+
818
+ def next_month(self):
819
+ return DateTimeUtils.YearMonth(self.year + 1, 1) if self.month == 12 else DateTimeUtils.YearMonth(self.year,
820
+ self.month + 1)
821
+
822
+ def previous_month(self):
823
+ return DateTimeUtils.YearMonth(self.year - 1, 12) if self.month == 1 else DateTimeUtils.YearMonth(self.year,
824
+ self.month - 1)
825
+
826
+ def next_year(self):
827
+ return DateTimeUtils.YearMonth(self.year + 1, self.month)
828
+
829
+ def previous_year(self):
830
+ return DateTimeUtils.YearMonth(self.year - 1, self.month)
831
+
832
+ @staticmethod
833
+ def delta_weeks(start: datetime.date, end: datetime.date) -> int:
834
+ return int((end - start).total_seconds() / (7 * 24 * 60 * 60) + 1)
835
+
836
+ @staticmethod
837
+ def parse_datetime(timestamp: Optional[str], picture: str) -> Optional[int]:
838
+ format_spec = DateTimeUtils._analyse_datetime_picture(picture)
839
+ match_spec = DateTimeUtils._generate_regex(format_spec)
840
+ full_regex = "^"
841
+ for part in match_spec.parts:
842
+ full_regex += "(" + part.regex + ")"
843
+ full_regex += "$"
844
+ pattern = re.compile(full_regex, re.IGNORECASE)
845
+ match = pattern.search(timestamp)
846
+ if match is not None:
847
+ dm_a = 161
848
+ dm_b = 130
849
+ dm_c = 84
850
+ dm_d = 72
851
+ tm_a = 23
852
+ tm_b = 47
853
+
854
+ components = {}
855
+ i = 1
856
+ while i <= len(match.groups()):
857
+ mpart = match_spec.parts[i - 1]
858
+ try:
859
+ components[mpart.component] = mpart.parse(match.group(i))
860
+ except NotImplementedError as e:
861
+ # do nothing
862
+ pass
863
+ i += 1
864
+
865
+ if len(components) == 0:
866
+ # nothing specified
867
+ return None
868
+
869
+ mask = 0
870
+
871
+ for part in "YXMxWwdD":
872
+ mask <<= 1
873
+ if components.get(part) is not None:
874
+ mask += 1
875
+ date_a = DateTimeUtils._is_type(dm_a, mask)
876
+ date_b = not date_a and DateTimeUtils._is_type(dm_b, mask)
877
+ date_c = DateTimeUtils._is_type(dm_c, mask)
878
+ date_d = not date_c and DateTimeUtils._is_type(dm_d, mask)
879
+
880
+ mask = 0
881
+ for part in "PHhmsf":
882
+ mask <<= 1
883
+ if components.get(part) is not None:
884
+ mask += 1
885
+ pass
886
+
887
+ time_a = DateTimeUtils._is_type(tm_a, mask)
888
+ time_b = not time_a and DateTimeUtils._is_type(tm_b, mask)
889
+
890
+ date_comps = "YB" if date_b else "XxwF" if date_c else "XWF" if date_d else "YMD"
891
+ time_comps = "Phmsf" if time_b else "Hmsf"
892
+ comps = date_comps + time_comps
893
+
894
+ now = datetime.datetime.utcnow()
895
+
896
+ start_specified = False
897
+ end_specified = False
898
+ for part in comps:
899
+ if components.get(part) is None:
900
+ if start_specified:
901
+ components[part] = 1 if "MDd".find(part) != -1 else 0
902
+ end_specified = True
903
+ else:
904
+ components[part] = int(float(DateTimeUtils._get_datetime_fragment(now, part)))
905
+ else:
906
+ start_specified = True
907
+ if end_specified:
908
+ raise RuntimeError(constants.Constants.ERR_MSG_MISSING_FORMAT)
909
+ if components.get('M') is not None and components['M'] > 0:
910
+ components['M'] = components['M'] - 1
911
+ else:
912
+ components['M'] = 0
913
+ if date_b:
914
+ first_jan = datetime.datetime(components['Y'], 1, 1, 0, 0)
915
+ first_jan = first_jan + datetime.timedelta(days=components['d'] - 1)
916
+ components['M'] = first_jan.month - 1
917
+ components['D'] = first_jan.day
918
+ if date_c:
919
+ # TODO implement this
920
+ # parsing this format not currently supported
921
+ raise RuntimeError(constants.Constants.ERR_MSG_MISSING_FORMAT)
922
+ if date_d:
923
+ # TODO implement this
924
+ # parsing this format (ISO week date) not currently supported
925
+ raise RuntimeError(constants.Constants.ERR_MSG_MISSING_FORMAT)
926
+ if time_b:
927
+ components['H'] = 0 if components['h'] == 12 else components['h']
928
+ if components['P'] == 1:
929
+ components['H'] = components['H'] + 12
930
+ cal = datetime.datetime(components['Y'], components['M'] + 1, components['D'], components['H'],
931
+ components['m'], components['s'], components['f'] * 1000, datetime.timezone.utc)
932
+ millis = cal.timestamp() * 1000
933
+ if components.get('Z') is not None:
934
+ millis -= components['Z'] * 60 * 1000
935
+ elif components.get('z') is not None:
936
+ millis -= components['z'] * 60 * 1000
937
+ return int(millis)
938
+ return None
939
+
940
+ @staticmethod
941
+ def _is_type(type: int, mask: int) -> bool:
942
+ return ((~type & mask) == 0) and (type & mask) != 0
943
+
944
+ @staticmethod
945
+ def _generate_regex(format_spec: PictureFormat) -> 'DateTimeUtils.PictureMatcher':
946
+ matcher = DateTimeUtils.PictureMatcher()
947
+ for part in format_spec.parts:
948
+ res = None
949
+ if part.type == "literal":
950
+ p = re.compile("[.*+?^${}()|\\[\\]\\\\]")
951
+
952
+ regex = re.sub(p, r"\g<0>", part.value)
953
+ res = DateTimeUtils.MatcherPart(regex)
954
+ elif part.component == 'Z' or part.component == 'z':
955
+ separator = len(part.integerFormat.groupingSeparators) == 1 and part.integerFormat.regular
956
+ regex = ""
957
+ if part.component == 'z':
958
+ regex = "GMT"
959
+ regex += "[-+][0-9]+"
960
+ if separator:
961
+ regex += part.integerFormat.groupingSeparators[0].character + "[0-9]+"
962
+ res = DateTimeUtils.MatcherPartTimeZone(regex, part, separator)
963
+ elif part.integerFormat is not None:
964
+ res = DateTimeUtils._generate_regex_with_component(part.component, part.integerFormat)
965
+ else:
966
+ regex = "[a-zA-Z]+"
967
+ lookup = {}
968
+ if part.component == 'M' or part.component == 'x':
969
+ i = 0
970
+ while i < len(DateTimeUtils._months):
971
+ if part.width is not None and part.width[1] is not None:
972
+ lookup[DateTimeUtils._months[i][0:part.width[1]]] = i + 1
973
+ else:
974
+ lookup[DateTimeUtils._months[i]] = i + 1
975
+ i += 1
976
+ elif part.component == 'F':
977
+ i = 1
978
+ while i < len(DateTimeUtils._days):
979
+ if part.width is not None and part.width[1] is not None:
980
+ lookup[DateTimeUtils._days[i][0:part.width[1]]] = i
981
+ else:
982
+ lookup[DateTimeUtils._days[i]] = i
983
+ i += 1
984
+ elif part.component == 'P':
985
+ lookup["am"] = 0
986
+ lookup["AM"] = 0
987
+ lookup["pm"] = 1
988
+ lookup["PM"] = 1
989
+ else:
990
+ raise RuntimeError(constants.Constants.ERR_MSG_INVALID_NAME_MODIFIER.format(part.component))
991
+ res = DateTimeUtils.MatcherPartLookup(regex, lookup)
992
+ res.component = part.component
993
+ matcher.parts.append(res)
994
+ return matcher
995
+
996
+ class MatcherPart:
997
+ regex: str
998
+ component: Optional[str]
999
+
1000
+ def __init__(self, regex):
1001
+ self.regex = regex
1002
+ self.component = None
1003
+
1004
+ def parse(self, value: str) -> int:
1005
+ raise NotImplementedError
1006
+
1007
+ class MatcherPartTimeZone(MatcherPart):
1008
+ _part: 'DateTimeUtils.SpecPart'
1009
+ _separator: bool
1010
+
1011
+ def __init__(self, regex, part, separator):
1012
+ super().__init__(regex)
1013
+ self._part = part
1014
+ self._separator = separator
1015
+
1016
+ def parse(self, value: str) -> int:
1017
+ if self._part.component == 'z':
1018
+ value = value[3:]
1019
+ offset_hours = 0
1020
+ offset_minutes = 0
1021
+ if self._separator:
1022
+ offset_hours = int(value[0:value.find(self._part.integerFormat.groupingSeparators[0].character)])
1023
+ offset_minutes = int(value[value.find(self._part.integerFormat.groupingSeparators[0].character) + 1:])
1024
+ else:
1025
+ numdigits = len(value) - 1
1026
+ if numdigits <= 2:
1027
+ offset_hours = int(value)
1028
+ else:
1029
+ offset_hours = int(value[0:3])
1030
+ offset_minutes = int(value[3:])
1031
+ return offset_hours * 60 + offset_minutes
1032
+
1033
+ class MatcherPartLookup(MatcherPart):
1034
+ _lookup: dict[str, int]
1035
+
1036
+ def __init__(self, regex, lookup):
1037
+ super().__init__(regex)
1038
+ self._lookup = lookup
1039
+
1040
+ def parse(self, value: str) -> int:
1041
+ return self._lookup[value]
1042
+
1043
+ @staticmethod
1044
+ def _generate_regex_with_component(component: Optional[str], format_spec: Optional[Format]) -> MatcherPart:
1045
+ is_upper = format_spec.case_type == DateTimeUtils.TCase.UPPER
1046
+ if format_spec.primary == DateTimeUtils.Formats.LETTERS:
1047
+ regex = "[A-Z]+" if is_upper else "[a-z]+"
1048
+ matcher = DateTimeUtils.MatcherPartLetters(regex, is_upper)
1049
+ elif format_spec.primary == DateTimeUtils.Formats.ROMAN:
1050
+ regex = "[MDCLXVI]+" if is_upper else "[mdclxvi]+"
1051
+ matcher = DateTimeUtils.MatcherPartRoman(regex, is_upper)
1052
+ elif format_spec.primary == DateTimeUtils.Formats.WORDS:
1053
+ words = set()
1054
+ words.update(DateTimeUtils._word_values.keys())
1055
+ words.add("and")
1056
+ words.add("[\\-, ]")
1057
+ regex = "(?:" + "|".join(words) + ")+"
1058
+ matcher = DateTimeUtils.MatcherPartWords(regex)
1059
+ elif format_spec.primary == DateTimeUtils.Formats.DECIMAL:
1060
+ regex = "[0-9]+"
1061
+ if component == 'Y':
1062
+ regex = "[0-9]{2,4}"
1063
+ elif (component == 'M') or (component == 'D') or (component == 'H') or (component == 'h') or (
1064
+ component == 'm') or (component == 's'):
1065
+ regex = "[0-9]{1,2}"
1066
+
1067
+ if format_spec.ordinal:
1068
+ regex += "(?:th|st|nd|rd)"
1069
+ matcher = DateTimeUtils.MatcherPartDecimal(regex, format_spec)
1070
+ else:
1071
+ raise RuntimeError(constants.Constants.ERR_MSG_SEQUENCE_UNSUPPORTED)
1072
+ return matcher
1073
+
1074
+ class MatcherPartLetters(MatcherPart):
1075
+ _is_upper: bool
1076
+
1077
+ def __init__(self, regex, is_upper):
1078
+ super().__init__(regex)
1079
+ self._is_upper = is_upper
1080
+
1081
+ def parse(self, value: str) -> int:
1082
+ return DateTimeUtils.letters_to_decimal(value, 'A' if self._is_upper else 'a')
1083
+
1084
+ class MatcherPartRoman(MatcherPart):
1085
+ _is_upper: bool
1086
+
1087
+ def __init__(self, regex, is_upper):
1088
+ super().__init__(regex)
1089
+ self._is_upper = is_upper
1090
+
1091
+ def parse(self, value: str) -> int:
1092
+ return DateTimeUtils.roman_to_decimal(value if self._is_upper else value.upper())
1093
+
1094
+ class MatcherPartWords(MatcherPart):
1095
+
1096
+ def __init__(self, regex):
1097
+ super().__init__(regex)
1098
+
1099
+ def parse(self, value: str) -> int:
1100
+ return DateTimeUtils.words_to_number(value.casefold())
1101
+
1102
+ class MatcherPartDecimal(MatcherPart):
1103
+ _format_spec: 'DateTimeUtils.Format'
1104
+
1105
+ def __init__(self, regex, format_spec):
1106
+ super().__init__(regex)
1107
+ self._format_spec = format_spec
1108
+
1109
+ def parse(self, value: str) -> int:
1110
+ digits = value
1111
+ if self._format_spec.ordinal:
1112
+ digits = value[0:len(value) - 2]
1113
+ if self._format_spec.regular:
1114
+ digits = "".join(digits.split(","))
1115
+ else:
1116
+ for sep in self._format_spec.groupingSeparators:
1117
+ digits = "".join(digits.split(sep.character))
1118
+ if self._format_spec.zeroCode != 0x30:
1119
+ chars = list(digits)
1120
+ i = 0
1121
+ while i < len(chars):
1122
+ chars[i] = chr(ord(chars[i]) - self._format_spec.zeroCode + 0x30)
1123
+ i += 1
1124
+ digits = ''.join(chars)
1125
+ return int(digits)
1126
+
1127
+ @staticmethod
1128
+ def letters_to_decimal(letters: str, a_char: str) -> int:
1129
+ decimal = 0
1130
+ chars = list(letters)
1131
+ i = 0
1132
+ while i < len(chars):
1133
+ decimal += (ord(chars[len(chars) - i - 1]) - ord(a_char) + 1) * 26 ** i
1134
+ i += 1
1135
+ return decimal
1136
+
1137
+ class PictureMatcher:
1138
+ parts: list['DateTimeUtils.MatcherPart']
1139
+
1140
+ def __init__(self):
1141
+ self.parts = []
1142
+
1143
+
1144
+ DateTimeUtils._static_initializer()