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