d8s-dates 0.8.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.
d8s_dates/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .dates_and_times import *
2
+
3
+ __version__ = "0.8.0"
4
+ __author__ = """Floyd Hightower"""
5
+ __email__ = "floyd.hightower27@gmail.com"
@@ -0,0 +1,532 @@
1
+ """Democritus functions for working with dates and times in Python."""
2
+
3
+ import datetime
4
+ import functools
5
+ import re
6
+ import time
7
+ from typing import Iterable, List, Optional, Union
8
+
9
+ import dateutil.parser
10
+ import maya
11
+ import parsedatetime
12
+ from d8s_hypothesis import hypothesis_get_strategy_results
13
+ from d8s_math import number_zero_pad
14
+ from d8s_strings import string_remove_from_end
15
+ from d8s_timezones import pytz_timezone_object
16
+ from hypothesis.strategies import dates, datetimes, timedeltas, times
17
+
18
+ DateOrString = Union[datetime.date, datetime.datetime, str]
19
+
20
+ DAY_NAMES = ("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
21
+ DAY_ABBREVIATIONS = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun")
22
+ MONTH_NAMES = (
23
+ "January",
24
+ "February",
25
+ "March",
26
+ "April",
27
+ "May",
28
+ "June",
29
+ "July",
30
+ "August",
31
+ "September",
32
+ "October",
33
+ "November",
34
+ "December",
35
+ )
36
+ MONTH_ABBREVIATIONS = ("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec")
37
+
38
+ STRF_DATA = (
39
+ {"patterns": DAY_ABBREVIATIONS, "replacement": "%a"},
40
+ {"patterns": DAY_NAMES, "replacement": "%A"},
41
+ {"patterns": MONTH_ABBREVIATIONS, "replacement": "%b"},
42
+ {"patterns": MONTH_NAMES, "replacement": "%B"},
43
+ {"regex": r"[0123]?[0-9]/%b/[0-9]{4}", "replacement": "%d/%b/%Y"},
44
+ {"regex": r"[0-9]?[0-9]:[0-9]{2}:[0-9]{2}", "replacement": "%X"},
45
+ {"regex": r"[01]?[0-9]/[0123]?[0-9]/[0-9]{3,4}", "replacement": "%-m/%-d/%Y"},
46
+ {"regex": r"[01]?[0-9]/[0123]?[0-9]/[0-9]{2}", "replacement": "%x"},
47
+ {"regex": r"[01]?[0-9]/[0123]?[0-9]/[0-9]", "replacement": "%-m/%-d/%Y"},
48
+ {"regex": r"[0-9]{4}-[01]?[0-9]-[0123]?[0-9]", "replacement": "%Y-%-m-%-d"},
49
+ {"regex": r"\*[0-9]{3,6}", "replacement": "*%f"},
50
+ {"regex": r"\.[0-9]{3,6}", "replacement": ".%f"},
51
+ {"regex": r"\,[0-9]{3,6}", "replacement": ",%f"},
52
+ {"patterns": [f"-{number_zero_pad(i, 4)}" for i in range(1200, -1, -100)], "replacement": "%z"},
53
+ {"patterns": [f"+{number_zero_pad(i, 4)}" for i in range(1200, -1, -100)], "replacement": "%z"},
54
+ {"patterns": [str(i) for i in range(3000, 1600, -1)], "replacement": "%Y"},
55
+ {"patterns": [number_zero_pad(i, 2) for i in range(1, 31)], "replacement": "%d"},
56
+ {"patterns": [str(i) for i in range(31, 0, -1)], "replacement": "%-d"},
57
+ {"patterns": [number_zero_pad(i, 2) for i in range(0, 12)], "replacement": "%m"},
58
+ {"patterns": [str(i) for i in range(12, 0, -1)], "replacement": "%-m"},
59
+ {"patterns": [number_zero_pad(i, 2) for i in range(99, 0, -1)], "replacement": "%y"},
60
+ {"patterns": ["AM", "PM"], "replacement": "%p"},
61
+ )
62
+
63
+
64
+ def _handle_patterns(patterns: List[str], replacement: str, date_string: str) -> str:
65
+ for pattern in patterns:
66
+ if pattern in date_string:
67
+ date_string = date_string.replace(pattern, replacement)
68
+ break
69
+ return date_string
70
+
71
+
72
+ def date_string_to_strftime_format(date_string):
73
+ """Predict the strftime format from the given date_string."""
74
+ for data in STRF_DATA:
75
+ if data.get("patterns"):
76
+ date_string = _handle_patterns(data["patterns"], data["replacement"], date_string)
77
+ elif data.get("regex"):
78
+ date_string = re.sub(data["regex"], data["replacement"], date_string)
79
+
80
+ return date_string
81
+
82
+
83
+ def date_parse( # noqa: CCR001
84
+ date: DateOrString, *, convert_to_current_timezone: bool = False
85
+ ) -> Union[datetime.datetime, datetime.date, datetime.time]:
86
+ """Parse the given date (can parse dates in most formats) (returns a datetime object)."""
87
+ if isinstance(date, (datetime.date, datetime.time, datetime.datetime)):
88
+ return date
89
+
90
+ # try to parse the date as an epoch datetime...
91
+ # we start with epoch datetime as it is the most discrete form of a date
92
+ try:
93
+ date = epoch_to_date(date)
94
+ except ValueError:
95
+ # try to parse the given date with the dateutil module
96
+ try:
97
+ date = _dateutil_parser_parse(date)
98
+ # if the given date could not be parsed by the dateutil module, try to parse the date using parsedatetime
99
+ except ValueError as e:
100
+ parsed_time_struct, parse_status = _parsedatetime_parse(date)
101
+
102
+ # convert the parsed_time_struct to a datetime object and return it
103
+ if parse_status > 0:
104
+ date = time_struct_to_datetime(parsed_time_struct)
105
+ else:
106
+ message = f'Unable to convert the date "{date}" into a standard date format.'
107
+ raise ValueError(message) from e
108
+
109
+ if convert_to_current_timezone:
110
+ date = date_make_timezone_aware(date)
111
+
112
+ return date # type: ignore
113
+
114
+
115
+ def date_now(*, convert_to_current_timezone: bool = False, utc: bool = False):
116
+ """Get the current date.
117
+
118
+ If convert_to_current_timezone is True, convert the date to the current timezone.
119
+ If utc is True, convert the date to UTC.
120
+ """
121
+ now = datetime.datetime.now()
122
+
123
+ if convert_to_current_timezone and utc:
124
+ raise ValueError("Only one input parameter from utc and convert_to_current_timezone can be true.")
125
+
126
+ if convert_to_current_timezone:
127
+ now = date_make_timezone_aware(now)
128
+
129
+ if utc:
130
+ now = date_to_utc(now)
131
+
132
+ return now
133
+
134
+
135
+ def date_parse_first_argument(func):
136
+ """."""
137
+
138
+ @functools.wraps(func)
139
+ def wrapper(*args, **kwargs):
140
+ date_arg = args[0]
141
+ other_args = args[1:]
142
+
143
+ parsed_date_arg = date_parse(date_arg)
144
+ return func(parsed_date_arg, *other_args, **kwargs)
145
+
146
+ return wrapper
147
+
148
+
149
+ @date_parse_first_argument
150
+ def date_2_string(date, date_format_string: str):
151
+ """."""
152
+ formatted_date_string = date.strftime(date_format_string)
153
+ return formatted_date_string
154
+
155
+
156
+ @date_parse_first_argument
157
+ def date_hour(date):
158
+ """Find the hour from the given date."""
159
+ return date.hour
160
+
161
+
162
+ @date_parse_first_argument
163
+ def date_minute(date):
164
+ """Find the minute from the given date."""
165
+ return date.minute
166
+
167
+
168
+ @date_parse_first_argument
169
+ def date_second(date):
170
+ """Find the second from the given date."""
171
+ return date.second
172
+
173
+
174
+ @date_parse_first_argument
175
+ def date_day(date):
176
+ """Find the day of the month from the given date."""
177
+ return date_day_of_month(date)
178
+
179
+
180
+ @date_parse_first_argument
181
+ def date_day_of_month(date):
182
+ """Find the day of the month from the given date."""
183
+ return date.day
184
+
185
+
186
+ @date_parse_first_argument
187
+ def date_month(date):
188
+ """Find the month from the given date."""
189
+ return date.month
190
+
191
+
192
+ @date_parse_first_argument
193
+ def date_year(date):
194
+ """Find the year from the given date."""
195
+ return date.year
196
+
197
+
198
+ @date_parse_first_argument
199
+ def date_convert_to_timezone(date, timezone_string):
200
+ """Convert the given date to the given timezone_string.
201
+
202
+ This will actually **convert** time given date; it will change the hour/day of the date to the given timezone).
203
+ """
204
+ # if the given date does not have a timezone, use the system's timezone
205
+ if date.tzinfo is None:
206
+ date = date_make_timezone_aware(date)
207
+
208
+ timezone_object = pytz_timezone_object(timezone_string)
209
+ converted_date = date.astimezone(timezone_object)
210
+ return converted_date
211
+
212
+
213
+ def date_make_timezone_aware(datetime_object, timezone_string=None):
214
+ """Make the given datetime_object timezone aware.
215
+
216
+ This function does NOT convert the datetime_object.
217
+ It will never change the hour/day or any value of the datetime...
218
+ it will simply make the given datetime timezone aware.
219
+ """
220
+ if timezone_string:
221
+ # make the date timezone aware using the given timezone_string
222
+ timezone_object = pytz_timezone_object(timezone_string)
223
+ timezone_aware_datetime_object = timezone_object.localize(datetime_object)
224
+ else:
225
+ # make the date timezone aware using the timezone of the current system
226
+ timezone_aware_datetime_object = datetime_object.astimezone()
227
+
228
+ return timezone_aware_datetime_object
229
+
230
+
231
+ def time_delta_examples(n=10, *, time_deltas_as_strings: bool = True):
232
+ """Return n time deltas."""
233
+ time_delta_objects = hypothesis_get_strategy_results(timedeltas, n=n)
234
+ if time_deltas_as_strings:
235
+ return [str(time_delta) for time_delta in time_delta_objects]
236
+ else:
237
+ return time_delta_objects
238
+
239
+
240
+ def time_examples(n=10, *, times_as_strings: bool = True):
241
+ """Return n times."""
242
+ time_objects = hypothesis_get_strategy_results(times, n=n)
243
+ if times_as_strings:
244
+ return [str(time) for time in time_objects]
245
+ else:
246
+ return time_objects
247
+
248
+
249
+ def date_examples(n=10, *, dates_as_strings: bool = True, date_string_format: Optional[str] = None):
250
+ """Return n dates."""
251
+ date_objects = hypothesis_get_strategy_results(dates, n=n)
252
+ if dates_as_strings:
253
+ if date_string_format is None:
254
+ return [str(date) for date in date_objects]
255
+ else:
256
+ return [date_2_string(date, date_string_format) for date in date_objects]
257
+ else:
258
+ return date_objects
259
+
260
+
261
+ def datetime_examples(n=10, *, datetimes_as_strings: bool = True, datetime_string_format: Optional[str] = None):
262
+ """Return n datetimes."""
263
+ datetime_objects = hypothesis_get_strategy_results(datetimes, n=n)
264
+ if datetimes_as_strings:
265
+ if datetime_string_format is None:
266
+ return [str(datetime) for datetime in datetime_objects]
267
+ else:
268
+ return [date_2_string(datetime, datetime_string_format) for datetime in datetime_objects]
269
+ else:
270
+ return datetime_objects
271
+
272
+
273
+ def time_struct_to_datetime(struct_time_object):
274
+ """Convert a python time.struct_time object into a datetime object."""
275
+ return datetime.datetime(*struct_time_object[:6])
276
+
277
+
278
+ def _parsedatetime_parse(date_string):
279
+ """Parse the given date_string using the parsedatetime module."""
280
+ # for more details on how the parsedatetime.Calendar.parse function works, see:
281
+ # https://github.com/bear/parsedatetime/blob/830775dc5e36395622b41f12317f5e10c303d3a2/parsedatetime/__init__.py#L1779
282
+ cal = parsedatetime.Calendar()
283
+ parsed_date = cal.parse(date_string)
284
+ return parsed_date
285
+
286
+
287
+ def _dateutil_parser_parse(date_string):
288
+ """Parse the given date_string using the dateutil.parser module."""
289
+ parsed_date = dateutil.parser.parse(date_string)
290
+ return parsed_date
291
+
292
+
293
+ def _maya_time_parse(date_object, *, convert_to_utc: bool = True):
294
+ """Parse the given date_object using maya (see https://github.com/timofurrer/maya).
295
+
296
+ By default, the given date_object is converted to UTC because maya will assume that any given date is in UTC.
297
+ """
298
+ if convert_to_utc:
299
+ # convert the given date to UTC (this is necessary b/c maya will assume that the given date is in UTC)
300
+ date = date_to_utc(date_object)
301
+
302
+ maya_date = maya.parse(date)
303
+ return maya_date
304
+
305
+
306
+ def epoch_time_now():
307
+ """Get the current epoch time."""
308
+ return int(time.time())
309
+
310
+
311
+ def is_date(possible_date_string):
312
+ """Determine if the given possible_date_string can be processed as a date."""
313
+ try:
314
+ date_parse(possible_date_string)
315
+ except Exception: # pylint: disable=broad-except
316
+ return False
317
+ else:
318
+ return True
319
+
320
+
321
+ def time_now():
322
+ """Return the current, epoch time."""
323
+ return time.time()
324
+
325
+
326
+ @date_parse_first_argument
327
+ def time_since(date):
328
+ """Return a time of the time since the given date."""
329
+ now = date_now()
330
+ return now - date
331
+
332
+
333
+ @date_parse_first_argument
334
+ def time_until(date):
335
+ """Return an English description of the time since the given date."""
336
+ now = date_now()
337
+ return date - now
338
+
339
+
340
+ @date_parse_first_argument
341
+ def time_since_slang(date):
342
+ """Return an English description of the time since the given date."""
343
+ maya_date = _maya_time_parse(date)
344
+ slang_time = maya_date.slang_time()
345
+ return slang_time
346
+
347
+
348
+ @date_parse_first_argument
349
+ def time_until_slang(date):
350
+ """Return an English description of the time until the given date."""
351
+ maya_date = _maya_time_parse(date)
352
+ slang_time = maya_date.slang_time()
353
+ return slang_time
354
+
355
+
356
+ @date_parse_first_argument
357
+ def date_to_utc(date):
358
+ """Convert the given date to UTC. Assume that the given date is in the system's timezone and convert it to UTC."""
359
+ utc_date = date_convert_to_timezone(date, "utc")
360
+ return utc_date
361
+
362
+
363
+ def time_after(time_a, time_b=None) -> bool:
364
+ """Check if one time is before the other."""
365
+ if time_b is None:
366
+ time_b = time_now()
367
+
368
+ # make sure both times are floats
369
+ time_a = float(date_to_epoch(time_a))
370
+ time_b = float(date_to_epoch(time_b))
371
+ return time_a > time_b
372
+
373
+
374
+ def time_before(time_a, time_b=None) -> bool:
375
+ """Check if one time is before the other."""
376
+ if time_b is None:
377
+ time_b = time_now()
378
+
379
+ # make sure both times are floats
380
+ time_a = float(date_to_epoch(time_a))
381
+ time_b = float(date_to_epoch(time_b))
382
+ return time_a < time_b
383
+
384
+
385
+ @date_parse_first_argument
386
+ def date_in_future(date) -> bool:
387
+ """Return whether or not the given date is in the future."""
388
+ is_in_the_future = time_after(date)
389
+ return is_in_the_future
390
+
391
+
392
+ def time_is() -> str:
393
+ """Time and money spent in helping men to do more for themselves is far better than mere giving. -Henry Ford"""
394
+ return "$"
395
+
396
+
397
+ @date_parse_first_argument
398
+ def date_to_iso(date, *, timezone_is_utc: bool = False, use_trailing_z: bool = False):
399
+ """Return the ISO 8601 version of the given date as a string (see https://en.wikipedia.org/wiki/ISO_8601)."""
400
+ if timezone_is_utc:
401
+ # replace any timezones on the date with UTC - this is not a conversion - it is a hard-replace...
402
+ # if there is a timezone on the given date, it will NOT be *converted* to UTC...
403
+ # the time will remain the same, but the timezone will change to UTC
404
+ date = date.replace(tzinfo=datetime.timezone.utc)
405
+
406
+ iso_format_date = date.isoformat()
407
+
408
+ if use_trailing_z and iso_format_date.endswith("+00:00"):
409
+ # remove the timezone from the end
410
+ iso_format_date = string_remove_from_end(iso_format_date, "+00:00")
411
+ # add a 'Z'
412
+ iso_format_date = iso_format_date + "Z"
413
+
414
+ return iso_format_date
415
+
416
+
417
+ def epoch_time_standardization(epoch_time):
418
+ """Convert the given epoch time to an epoch time in seconds."""
419
+ epoch_time_string = str(epoch_time)
420
+ # if the given epoch time appears to include milliseconds (or some other level of precision)...
421
+ # and does not have a decimal in it, add a decimal point
422
+ if len(epoch_time_string) > 10 and "." not in epoch_time_string:
423
+ epoch_time = f"{epoch_time_string[:10]}.{epoch_time_string[10:]}"
424
+ return epoch_time
425
+
426
+
427
+ def epoch_to_date(epoch_time) -> datetime.datetime:
428
+ """Convert the epoch_time into a datetime."""
429
+ epoch_time = float(epoch_time_standardization(epoch_time))
430
+ return datetime.datetime.fromtimestamp(epoch_time)
431
+
432
+
433
+ @date_parse_first_argument
434
+ def date_day_of_week(date):
435
+ """Return the day of the week on which the given date occurred."""
436
+ day_of_week = date.strftime("%A")
437
+ return day_of_week
438
+
439
+
440
+ @date_parse_first_argument
441
+ def date_week_of_year(date, *, sunday_is_first_day_of_week: bool = False):
442
+ """Find the week of the year for the given date. If no date is given, return the week of the current date."""
443
+ if sunday_is_first_day_of_week:
444
+ return date.strftime("%U")
445
+ else:
446
+ return date.strftime("%V")
447
+
448
+
449
+ @date_parse_first_argument
450
+ def date_to_epoch(date):
451
+ """Convert a datetime stamp to epoch time."""
452
+ epoch_time = date.strftime("%s")
453
+ return int(epoch_time)
454
+
455
+
456
+ def chrome_timestamp_to_epoch(chrome_timestamp):
457
+ """Convert the given Chrome timestamp to epoch time.
458
+
459
+ For more information, see: https://stackoverflow.com/questions/20458406/what-is-the-format-of-chromes-timestamps.
460
+ """
461
+ return (chrome_timestamp / 1000000) - 11644473600
462
+
463
+
464
+ def time_waste(n=3):
465
+ """If time be of all things the most precious, wasting time must be the greatest prodigality. -Benjamin Franklin"""
466
+ time.sleep(n)
467
+ message = f"I just wasted {n} seconds of your life."
468
+ print(message)
469
+
470
+
471
+ def time_as_float(time_string: str) -> float:
472
+ """converts a given HH:MM time string to float"""
473
+ try:
474
+ hours, minutes = list(map(int, time_string.split(":"))) # parse given time string
475
+ except ValueError as e:
476
+ message = f"Invalid time string, ensure that the argument is in HH:MM format. Provided value: {time_string}"
477
+ raise ValueError(message) from e
478
+ else:
479
+ if hours > 23 or minutes > 59:
480
+ message = f"Invalid time string, should be between 00:00 and 23:59. Provided value: {time_string}"
481
+ raise ValueError(message)
482
+
483
+ return hours + (minutes / 60)
484
+
485
+
486
+ @date_parse_first_argument
487
+ def datetime_date(date: DateOrString) -> datetime.date:
488
+ """Return a datetime.date version of the given date."""
489
+ if isinstance(date, datetime.datetime):
490
+ date = date.date()
491
+ return date # type: ignore
492
+
493
+
494
+ @date_parse_first_argument
495
+ def age(date_of_birth: DateOrString, as_of: Optional[DateOrString] = None):
496
+ """Find the age of a person with the given date_of_birth."""
497
+ # Set as_of to today if it doesn't exist already
498
+ if not as_of:
499
+ as_of = date_now()
500
+ else:
501
+ as_of = date_parse(as_of) # type: ignore
502
+
503
+ # Get everything into date format
504
+ if isinstance(date_of_birth, datetime.datetime):
505
+ date_of_birth = date_of_birth.date()
506
+ if isinstance(as_of, datetime.datetime):
507
+ as_of = as_of.date()
508
+
509
+ try:
510
+ tmp = date_of_birth.replace(year=as_of.year) # type: ignore
511
+ # ValueError is raised when date_of_birth is February 29 and the current year is not a leap year
512
+ except ValueError:
513
+ tmp = date_of_birth.replace(year=as_of.year, day=date_of_birth.day - 1) # type: ignore
514
+
515
+ if tmp > as_of: # type: ignore
516
+ return as_of.year - date_of_birth.year - 1 # type: ignore
517
+ else:
518
+ return as_of.year - date_of_birth.year # type: ignore
519
+
520
+
521
+ @date_parse_first_argument
522
+ def date_range_days(start_date: DateOrString, end_date: DateOrString) -> Iterable[datetime.date]:
523
+ """Yield datetime.date objects representing each day between the given start and end dates (inclusive)."""
524
+ start_date = datetime_date(start_date)
525
+ end_date = datetime_date(end_date)
526
+
527
+ while True:
528
+ yield start_date # type: ignore
529
+
530
+ start_date = start_date + datetime.timedelta(days=1) # type: ignore
531
+ if start_date > end_date: # type: ignore
532
+ break