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/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