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.
- jsonata/__init__.py +10 -0
- jsonata/cli/__init__.py +0 -0
- jsonata/cli/__main__.py +242 -0
- jsonata/constants.py +68 -0
- jsonata/datetimeutils.py +1144 -0
- jsonata/functions.py +2179 -0
- jsonata/jexception.py +232 -0
- jsonata/jsonata.py +2045 -0
- jsonata/parser.py +1397 -0
- jsonata/signature.py +441 -0
- jsonata/timebox.py +89 -0
- jsonata/tokenizer.py +306 -0
- jsonata/utils.py +150 -0
- jsonata_python-0.1.0.dist-info/METADATA +338 -0
- jsonata_python-0.1.0.dist-info/RECORD +17 -0
- jsonata_python-0.1.0.dist-info/WHEEL +4 -0
- jsonata_python-0.1.0.dist-info/licenses/LICENSE +202 -0
jsonata/datetimeutils.py
ADDED
|
@@ -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()
|