dately 1.0.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.
- dately/__init__.py +2 -0
- dately/core.py +507 -0
- dately/mold/__init__.py +0 -0
- dately/mold/pyd/Compiled.cp38-win_amd64.pyd +0 -0
- dately/mold/pyd/__init__.py +0 -0
- dately/mold/pyd/cdatetime/UniversalDateFormatter.cp38-win_amd64.pyd +0 -0
- dately/mold/pyd/cdatetime/__init__.py +0 -0
- dately/mold/pyd/cdatetime/iso8601T.cp38-win_amd64.pyd +0 -0
- dately/mold/pyd/cdatetime/iso8601Z.cp38-win_amd64.pyd +0 -0
- dately/mold/pyd/cdatetime/whichformat.cp38-win_amd64.pyd +0 -0
- dately/mold/pyd/clean_str.cp38-win_amd64.pyd +0 -0
- dately/mold/pyd/time_zones.cp38-win_amd64.pyd +0 -0
- dately/timeutils.py +376 -0
- dately/timezone.py +475 -0
- dately/utils.py +116 -0
- dately-1.0.0.dist-info/METADATA +31 -0
- dately-1.0.0.dist-info/RECORD +19 -0
- dately-1.0.0.dist-info/WHEEL +5 -0
- dately-1.0.0.dist-info/top_level.txt +1 -0
dately/timeutils.py
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import datetime
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
# Import all functions and classes from custom utility modules
|
|
6
|
+
from .utils import *
|
|
7
|
+
from .mold.pyd.Compiled import get_time_fragment as gtf, anytime_regex, timemeridiem_regex, timeboundary_regex, time_only_regex
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Define public interface
|
|
12
|
+
__all__ = [
|
|
13
|
+
"exist_meridiem",
|
|
14
|
+
"extract_time_fragment",
|
|
15
|
+
"make_datetime_string",
|
|
16
|
+
"offset_convert",
|
|
17
|
+
"replace_time_by_position",
|
|
18
|
+
"stripTimeIndicator",
|
|
19
|
+
"stripTime",
|
|
20
|
+
"datetime_offset",
|
|
21
|
+
"strTime",
|
|
22
|
+
"stripTimeZone",
|
|
23
|
+
"validate_timezone",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
def get_time():
|
|
27
|
+
# Get the current time as a timestamp
|
|
28
|
+
current_time = time.time()
|
|
29
|
+
# Convert timestamp to a time tuple in local time
|
|
30
|
+
local_time = time.localtime(current_time)
|
|
31
|
+
# Format the time to include hours, minutes, seconds, and microseconds
|
|
32
|
+
formatted_time = time.strftime("%H:%M:%S", local_time) + f".{int(current_time % 1 * 1_000_000)}"
|
|
33
|
+
return formatted_time
|
|
34
|
+
|
|
35
|
+
def offset_convert(number):
|
|
36
|
+
"""
|
|
37
|
+
Converts a numeric or string time offset into a formatted string representing the offset in hours and minutes.
|
|
38
|
+
|
|
39
|
+
The function takes either a floating-point, an integer, or a string representing a time offset in hours,
|
|
40
|
+
and returns a string formatted as +-HH:MM. The sign (plus or minus) is determined based on
|
|
41
|
+
whether the input number is non-negative or negative.
|
|
42
|
+
|
|
43
|
+
Parameters:
|
|
44
|
+
number (float, int, or str): The time offset in hours. Can be positive, negative, or zero.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: The formatted time offset as a string with a leading sign (either '+' or '-') followed
|
|
48
|
+
by two digits for hours and two digits for minutes, separated by a colon.
|
|
49
|
+
"""
|
|
50
|
+
# Convert string input to float if necessary
|
|
51
|
+
if isinstance(number, str):
|
|
52
|
+
number = float(number)
|
|
53
|
+
|
|
54
|
+
sign = '+' if number >= 0 else '-'
|
|
55
|
+
# Get the absolute value
|
|
56
|
+
abs_number = abs(number)
|
|
57
|
+
# Extract hours and minutes
|
|
58
|
+
hours = int(abs_number)
|
|
59
|
+
minutes = int((abs_number - hours) * 60)
|
|
60
|
+
# Format the result as +-HH:MM
|
|
61
|
+
formatted_time = f"{sign}{hours:02}:{minutes:02}"
|
|
62
|
+
return formatted_time
|
|
63
|
+
|
|
64
|
+
def datetime_offset(offset):
|
|
65
|
+
# Validate the offset
|
|
66
|
+
if not isinstance(offset, (int, float)):
|
|
67
|
+
raise ValueError("Offset must be an integer or float representing hours.")
|
|
68
|
+
# Create a timezone object for datetime's tzinfo
|
|
69
|
+
timezone = datetime.timezone(datetime.timedelta(hours=offset))
|
|
70
|
+
return timezone
|
|
71
|
+
|
|
72
|
+
def strTime(datetime_string):
|
|
73
|
+
""" Extracts and returns detailed time information from a datetime string. """
|
|
74
|
+
# Regex for finding time
|
|
75
|
+
time_exists = timeboundary_regex.search(datetime_string)
|
|
76
|
+
|
|
77
|
+
if time_exists:
|
|
78
|
+
full_time_start_position = time_exists.start()
|
|
79
|
+
full_time_end_position = time_exists.end()
|
|
80
|
+
full_time_string = time_exists.group()
|
|
81
|
+
|
|
82
|
+
# Search for more specific time details within the extracted time string
|
|
83
|
+
time_match = time_only_regex.search(full_time_string)
|
|
84
|
+
|
|
85
|
+
if time_match:
|
|
86
|
+
# If a specific time format is found, return start, end, and the actual time value as a dictionary
|
|
87
|
+
time_details = {
|
|
88
|
+
'time_found': time_match.group(),
|
|
89
|
+
'start': time_match.start() + full_time_start_position,
|
|
90
|
+
'end': time_match.end() + full_time_start_position
|
|
91
|
+
}
|
|
92
|
+
full_time_details = {
|
|
93
|
+
'full_time_string': full_time_string,
|
|
94
|
+
'start': full_time_start_position,
|
|
95
|
+
'end': full_time_end_position
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
result = {
|
|
99
|
+
'time_details': time_details,
|
|
100
|
+
'full_time_details': full_time_details
|
|
101
|
+
}
|
|
102
|
+
return result
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def stripTime(datetime_string):
|
|
106
|
+
""" Removes time from a datetime string. """
|
|
107
|
+
time_exists = timeboundary_regex.search(datetime_string)
|
|
108
|
+
if time_exists:
|
|
109
|
+
full_time_start_position = time_exists.start()
|
|
110
|
+
date_no_time = datetime_string[:full_time_start_position]
|
|
111
|
+
return cleanstr(date_no_time)
|
|
112
|
+
return datetime_string
|
|
113
|
+
|
|
114
|
+
def stripTimeIndicator(datetime_string):
|
|
115
|
+
""" Removes time indicators (like 'AM' or 'PM') from a given datetime string. """
|
|
116
|
+
match = strTime(datetime_string)
|
|
117
|
+
|
|
118
|
+
if match:
|
|
119
|
+
time_match = match['time_details']['time_found']
|
|
120
|
+
time_end = match['time_details']["end"]
|
|
121
|
+
fulltime_start = match['full_time_details']['start']
|
|
122
|
+
fulltime_end = match['full_time_details']["end"]
|
|
123
|
+
|
|
124
|
+
timezone_data = datetime_string[time_end:]
|
|
125
|
+
|
|
126
|
+
if timezone_data == '':
|
|
127
|
+
return datetime_string
|
|
128
|
+
|
|
129
|
+
# Remove the time indicator
|
|
130
|
+
cleaned_string = re.sub(timemeridiem_regex, ' ', timezone_data)
|
|
131
|
+
|
|
132
|
+
# Combine the parts
|
|
133
|
+
return datetime_string[:fulltime_start] + time_match + cleaned_string + datetime_string[fulltime_end:]
|
|
134
|
+
|
|
135
|
+
return datetime_string
|
|
136
|
+
|
|
137
|
+
def stripTimeZone(datetime_string):
|
|
138
|
+
""" Removes timezone info from a given datetime string. """
|
|
139
|
+
match = strTime(datetime_string)
|
|
140
|
+
|
|
141
|
+
if match:
|
|
142
|
+
time_match = match['time_details']['time_found']
|
|
143
|
+
time_start = match['time_details']['start']
|
|
144
|
+
time_end = match['time_details']["end"]
|
|
145
|
+
fulltime_start = match['full_time_details']['start']
|
|
146
|
+
fulltime_end = match['full_time_details']["end"]
|
|
147
|
+
|
|
148
|
+
timezone_data = datetime_string[time_end:]
|
|
149
|
+
|
|
150
|
+
if timezone_data == '':
|
|
151
|
+
return datetime_string
|
|
152
|
+
|
|
153
|
+
indicator_match = timemeridiem_regex.search(timezone_data)
|
|
154
|
+
|
|
155
|
+
if indicator_match:
|
|
156
|
+
indicator_end = indicator_match.end()
|
|
157
|
+
new_end_position = time_end + indicator_end
|
|
158
|
+
new_str = datetime_string[:new_end_position]
|
|
159
|
+
return cleanstr(new_str)
|
|
160
|
+
|
|
161
|
+
return datetime_string
|
|
162
|
+
|
|
163
|
+
def make_datetime_string(date_string):
|
|
164
|
+
"""
|
|
165
|
+
Generate a standardized datetime string from a given date string.
|
|
166
|
+
|
|
167
|
+
This function takes a date string and attempts to detect its format using the generate_date_formats function. If a valid format
|
|
168
|
+
is detected, it compiles the appropriate regex patterns using __compile_maketime_patterns__. The function then checks if the date
|
|
169
|
+
string matches the compiled pattern and, if the format is date-only, appends '00:00:00.000000 NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET' to
|
|
170
|
+
standardize the datetime string. If the format is not detected or includes time information, the original date string is returned.
|
|
171
|
+
|
|
172
|
+
Parameters:
|
|
173
|
+
date_string (str): The date string to be standardized.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
str: A standardized datetime string. If the format is date-only, '00:00:00.000000 NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET' is appended to the date string. If the format
|
|
177
|
+
includes time information or is not detected, the original date string is returned.
|
|
178
|
+
"""
|
|
179
|
+
# Define the regex pattern for time extraction
|
|
180
|
+
time_pattern = anytime_regex
|
|
181
|
+
no_other_time_placeholder = "NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET"
|
|
182
|
+
|
|
183
|
+
# Search for the time component in the date string
|
|
184
|
+
match = time_pattern.search(date_string)
|
|
185
|
+
|
|
186
|
+
if match:
|
|
187
|
+
# Check if the matched time component has a valid tzinfo
|
|
188
|
+
time_component = match.group()
|
|
189
|
+
tzinfo_pattern = gtf('tzinfo')
|
|
190
|
+
if tzinfo_pattern.search(time_component):
|
|
191
|
+
return date_string
|
|
192
|
+
else:
|
|
193
|
+
return f"{date_string.strip()} {get_time()} {no_other_time_placeholder}"
|
|
194
|
+
else:
|
|
195
|
+
# If no time component is found, append '00:00:00.000000 NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET' to the date string
|
|
196
|
+
return f"{date_string.strip()} {get_time()} {no_other_time_placeholder}"
|
|
197
|
+
|
|
198
|
+
def validate_timezone(datetime_string):
|
|
199
|
+
match = strTime(datetime_string)
|
|
200
|
+
|
|
201
|
+
if match:
|
|
202
|
+
time_end = match['time_details']["end"]
|
|
203
|
+
timezone_data = datetime_string[time_end:]
|
|
204
|
+
timezone_data = timezone_data.lstrip()
|
|
205
|
+
if timezone_data == '':
|
|
206
|
+
return True, "The time string is valid."
|
|
207
|
+
# Check the occurrences of each component
|
|
208
|
+
if len(timemeridiem_regex.findall(timezone_data)) > 1:
|
|
209
|
+
return False, "More than one time indicator found."
|
|
210
|
+
if len(timezone_offset_regex.findall(timezone_data)) > 1:
|
|
211
|
+
return False, "More than one timezone offset found."
|
|
212
|
+
if len(timezone_abbreviation_regex.findall(timezone_data)) > 1:
|
|
213
|
+
return False, "More than one timezone abbreviation found."
|
|
214
|
+
if len(iana_timezone_identifier_regex.findall(timezone_data)) > 1:
|
|
215
|
+
return False, "More than one IANA timezone identifier found."
|
|
216
|
+
if len(full_timezone_name_regex.findall(timezone_data)) > 1:
|
|
217
|
+
return False, "More than one full timezone name found."
|
|
218
|
+
return True, "The time string is valid."
|
|
219
|
+
return False, "No valid time string found"
|
|
220
|
+
|
|
221
|
+
def validate_date(date_string, date_format):
|
|
222
|
+
"""
|
|
223
|
+
Validates the given date string in the format of 'month/day/year'.
|
|
224
|
+
It first extracts any localized time fragment and cleans the string,
|
|
225
|
+
then identifies the components of the date (month, day, year) and
|
|
226
|
+
checks their validity based on standard calendar rules.
|
|
227
|
+
"""
|
|
228
|
+
# Extract any localized time fragment and clean the string.
|
|
229
|
+
date_str = stripTime(date_string)
|
|
230
|
+
|
|
231
|
+
# This dictionary will store the spans of each component.
|
|
232
|
+
components_spans = {"month": None, "day": None, "year": None}
|
|
233
|
+
|
|
234
|
+
# Single pass to find month, day, and year
|
|
235
|
+
pattern = datetime_pattern_search(date_format)
|
|
236
|
+
match = pattern.match(date_str)
|
|
237
|
+
if match:
|
|
238
|
+
# Iterate through possible date components
|
|
239
|
+
for key in components_spans.keys():
|
|
240
|
+
if key in match.groupdict(): # Check if the current component was found in the match
|
|
241
|
+
components_spans[key] = match.span(key) # Store the span of the component
|
|
242
|
+
|
|
243
|
+
# Extract day, month, and year based on their spans in the string
|
|
244
|
+
day = int(date_str[slice(*components_spans['day'])])
|
|
245
|
+
month = int(date_str[slice(*components_spans['month'])])
|
|
246
|
+
year = int(date_str[slice(*components_spans['year'])])
|
|
247
|
+
|
|
248
|
+
# Define the maximum days in each month, adjusting February based on leap year
|
|
249
|
+
month_days = {1: 31, 2: 29 if is_leap_year(year) else 28, 3: 31, 4: 30, 5: 31, 6: 30,
|
|
250
|
+
7: 31, 8: 31, 9: 30, 10: 31, 11: 30, 12: 31}
|
|
251
|
+
if month < 1 or month > 12:
|
|
252
|
+
return False # Invalid month
|
|
253
|
+
if day < 1 or day > month_days.get(month, 31):
|
|
254
|
+
return False # Day is not valid for the month
|
|
255
|
+
return True
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def exist_meridiem(time_fragment_str):
|
|
259
|
+
"""
|
|
260
|
+
Check if a meridiem indicator (AM/PM) exists in a given time fragment string.
|
|
261
|
+
|
|
262
|
+
This function uses a compiled regex pattern to search for the presence of meridiem indicators (AM or PM)
|
|
263
|
+
in the provided time fragment string. It returns True if a match is found, otherwise None.
|
|
264
|
+
|
|
265
|
+
Parameters:
|
|
266
|
+
time_fragment_str (str): The time fragment string to be checked for a meridiem indicator.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
bool or None: Returns True if a meridiem indicator is found, otherwise None.
|
|
270
|
+
"""
|
|
271
|
+
pattern = timemeridiem_regex
|
|
272
|
+
match = pattern.search(time_fragment_str)
|
|
273
|
+
if match:
|
|
274
|
+
return True
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def extract_time_fragment(date_time_string, locale=False):
|
|
279
|
+
"""
|
|
280
|
+
Parse a date-time string to extract the time fragment and provide its position for possible modification.
|
|
281
|
+
|
|
282
|
+
This function uses a compiled regex pattern to search for a time fragment within a given date-time string.
|
|
283
|
+
If a match is found, it returns the matched time fragment along with its start and end positions within the
|
|
284
|
+
original string. If the 'locale' parameter is set to True, the positions are returned; otherwise, they are None.
|
|
285
|
+
|
|
286
|
+
Parameters:
|
|
287
|
+
date_time_string (str): The date-time string to be parsed for a time fragment.
|
|
288
|
+
locale (bool): A flag indicating whether to return the start and end positions of the time fragment.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
tuple: A tuple containing the matched time fragment (str), and optionally its start (int) and end (int)
|
|
292
|
+
positions. If no match is found, (None, None, None) is returned.
|
|
293
|
+
"""
|
|
294
|
+
pattern = anytime_regex
|
|
295
|
+
match = pattern.search(date_time_string)
|
|
296
|
+
if match:
|
|
297
|
+
if locale:
|
|
298
|
+
return match.group(0), match.start(), match.end()
|
|
299
|
+
else:
|
|
300
|
+
return match.group(0), None, None
|
|
301
|
+
return None, None, None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def replace_time_by_position(datetime_string, component, new_value):
|
|
305
|
+
"""
|
|
306
|
+
Modifies a specified component of a time within a datetime string based on provided patterns.
|
|
307
|
+
|
|
308
|
+
This function updates the hour, minute, second, microsecond, or timezone information of a datetime string.
|
|
309
|
+
Each component is adjusted based on regular expressions which identify these elements within the string.
|
|
310
|
+
The function ensures that input values are within valid ranges and formats them accordingly before replacement.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
datetime_string (str): The datetime string to modify.
|
|
314
|
+
component (str): The component to modify, which can be 'hour', 'minute', 'second', 'microsecond', or 'tzinfo'.
|
|
315
|
+
new_value (str): The new value to insert for the specified component. Should be a string that is valid
|
|
316
|
+
within the context of the component (e.g., numeric for hour, minute, and second).
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
str: The modified datetime string with the specified component updated to the new value.
|
|
320
|
+
Returns the original datetime string unchanged if the component does not match or if the new
|
|
321
|
+
value is invalid for the specified component.
|
|
322
|
+
|
|
323
|
+
Raises:
|
|
324
|
+
ValueError: If the new value is out of the acceptable range for hours (0-23), minutes or seconds (0-59),
|
|
325
|
+
or if the microsecond value is not a digit.
|
|
326
|
+
"""
|
|
327
|
+
# Validate and adjust the new value based on the component
|
|
328
|
+
if component == 'hour':
|
|
329
|
+
new_value = str(max(0, min(23, int(new_value))))
|
|
330
|
+
elif component in ['minute', 'second']:
|
|
331
|
+
new_value = str(max(0, min(59, int(new_value)))).zfill(2)
|
|
332
|
+
elif component == 'microsecond':
|
|
333
|
+
if not str(new_value).isdigit():
|
|
334
|
+
return datetime_string
|
|
335
|
+
|
|
336
|
+
time_component_pattern = gtf(component)
|
|
337
|
+
gt_time_pattern_match = timeboundary_regex.search(datetime_string)
|
|
338
|
+
|
|
339
|
+
if gt_time_pattern_match:
|
|
340
|
+
time_start_position = gt_time_pattern_match.start()
|
|
341
|
+
time_end_position = gt_time_pattern_match.end()
|
|
342
|
+
time_matched_value = gt_time_pattern_match.group()
|
|
343
|
+
|
|
344
|
+
if component == 'tzinfo':
|
|
345
|
+
new_value = offset_convert(new_value)
|
|
346
|
+
time_component_matches = list(time_component_pattern.finditer(time_matched_value))
|
|
347
|
+
if time_component_matches:
|
|
348
|
+
# Find the match with the largest end position
|
|
349
|
+
largest_match = max(time_component_matches, key=lambda m: m.end())
|
|
350
|
+
# Replace all tzinfo matches with the new value
|
|
351
|
+
part_before = datetime_string[:time_start_position + time_component_matches[0].start()]
|
|
352
|
+
part_after = datetime_string[time_start_position + largest_match.end():]
|
|
353
|
+
datetime_string = part_before + new_value + part_after
|
|
354
|
+
return datetime_string.replace('NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET', new_value).strip()
|
|
355
|
+
else:
|
|
356
|
+
# Replace the placeholders with the new tzinfo value
|
|
357
|
+
return datetime_string.replace('NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET', new_value).strip()
|
|
358
|
+
|
|
359
|
+
# Match the specific component within the matched time
|
|
360
|
+
time_component_match = time_component_pattern.search(time_matched_value)
|
|
361
|
+
if time_component_match:
|
|
362
|
+
# Replace the component with the new value
|
|
363
|
+
part_before = datetime_string[:time_start_position + time_component_match.start(1)]
|
|
364
|
+
part_after = datetime_string[time_start_position + time_component_match.end(1):]
|
|
365
|
+
|
|
366
|
+
if component == 'microsecond':
|
|
367
|
+
new_value = hundred_thousandths_place(new_value, decimal=False)
|
|
368
|
+
|
|
369
|
+
datetime_string = part_before + new_value + part_after
|
|
370
|
+
|
|
371
|
+
return datetime_string.replace('NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET', '').strip()
|
|
372
|
+
|
|
373
|
+
# Always remove placeholders if they exist
|
|
374
|
+
datetime_string = datetime_string.replace('NO_MERIDIEM_NO_TIMEZONE_NO_OFFSET', '').strip()
|
|
375
|
+
|
|
376
|
+
return datetime_string
|