Coverage for portality / lib / dates.py: 88%
119 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 00:09 +0100
« prev ^ index » next coverage.py v7.13.5, created at 2026-05-05 00:09 +0100
1# ~~ Dates:Library~~
3import math
4from datetime import datetime, timedelta
5from random import randint
6import time
8# Extracted from settings.py to prevent circular import
9config = {
10 # when dates.format is called without a format argument, what format to use?
11 'DEFAULT_DATE_FORMAT': "%Y-%m-%dT%H:%M:%SZ",
13 # date formats that we know about, and should try, in order, when parsing
14 'DATE_FORMATS': [
15 "%Y-%m-%dT%H:%M:%S.%fZ", # e.g. 2010-01-01T00:00:00.000Z
16 "%Y-%m-%dT%H:%M:%SZ", # e.g. 2014-09-23T11:30:45Z
17 "%Y-%m-%d", # e.g. 2014-09-23
18 "%d/%m/%y", # e.g. 29/02/80
19 "%d/%m/%Y", # e.g. 29/02/1980
20 "%d-%m-%Y", # e.g. 01-01-2015
21 "%Y.%m.%d", # e.g. 2014.09.12
22 "%d.%m.%Y", # e.g. 12.9.2014
23 "%d.%m.%y", # e.g. 12.9.14
24 "%d %B %Y", # e.g. 21 June 2014
25 "%d-%b-%Y", # e.g. 31-Jul-13
26 "%d-%b-%y", # e.g. 31-Jul-2013
27 "%b-%y", # e.g. Aug-13
28 "%B %Y", # e.g. February 2014
29 "%Y" # e.g. 1978
30 ],
32 # The last_manual_update field was initialised to this value. Used to label as 'never'.
33 'DEFAULT_TIMESTAMP': "1970-01-01T00:00:00Z",
34 'FAR_IN_THE_FUTURE': "9999-12-31"
35}
38FMT_DATETIME_STD = config.get('DEFAULT_DATE_FORMAT', '%Y-%m-%dT%H:%M:%SZ')
39FMT_DATETIME_A = '%Y-%m-%d %H:%M:%S'
40FMT_DATETIME_NO_SECS = '%Y-%m-%d %H:%M'
41FMT_DATETIME_MS_STD = '%Y-%m-%dT%H:%M:%S.%fZ'
42FMT_DATETIME_SHORT = '%Y%m%d_%H%M'
43FMT_DATETIME_LONG = "%Y%m%d_%H%M%S_%f"
45FMT_DATE_STD = '%Y-%m-%d'
46FMT_DATE_SHORT = '%Y%m%d'
47FMT_DATE_DOT = '%Y.%m.%d'
48FMT_DATE_HUMAN = '%d %B %Y'
49FMT_DATE_HUMAN_A = '%d/%b/%Y'
50FMT_DATE_YM = '%Y-%m'
51FMT_DATE_YMDOT = '%Y.%m'
53FMT_TIME_SHORT = '%H%M'
54FMT_YEAR = '%Y'
56DEFAULT_TIMESTAMP_VAL = config.get('DEFAULT_TIMESTAMP', '1970-01-01T00:00:00Z')
58def far_in_the_future (out_format=FMT_DATE_STD):
59 return reformat(config.get("FAR_IN_THE_FUTURE", "9999-12-31"), FMT_DATE_STD, out_format)
61def parse(s, format=None, guess=True) -> datetime:
62 s = s.strip()
63 if format is not None:
64 try:
65 return datetime.strptime(s, format)
66 except ValueError as e:
67 if not guess:
68 raise e
70 for f in config.get("DATE_FORMATS", []):
71 try:
72 return datetime.strptime(s, f)
73 except ValueError as e:
74 pass
76 raise ValueError("Unable to parse {x} with any known format".format(x=s))
79def format(d, format=None) -> str:
80 return d.strftime(format or FMT_DATETIME_STD)
83def reformat(s, in_format=None, out_format=None) -> str:
84 return format(parse(s, format=in_format), format=out_format)
87def now() -> datetime:
88 """ standard now function for DOAJ """
89 return datetime.utcnow()
92def now_str(fmt=FMT_DATETIME_STD) -> str:
93 return format(now(), format=fmt)
96def now_str_with_microseconds() -> str:
97 return format(now(), format=FMT_DATETIME_MS_STD)
100def today(str_format=FMT_DATE_STD) -> str:
101 return format(now(), format=str_format)
104def random_date(fro: datetime = None, to: datetime = None) -> str:
105 if fro is None:
106 fro = parse(DEFAULT_TIMESTAMP_VAL)
107 if isinstance(fro, str):
108 fro = parse(fro)
109 if to is None:
110 to = now()
111 if isinstance(to, str):
112 to = parse(to)
114 span = int((to - fro).total_seconds())
115 s = randint(0, span)
116 return format(to - timedelta(seconds=s))
119def before(timestamp, seconds) -> datetime:
120 return timestamp - timedelta(seconds=seconds)
123def before_now(seconds: int) -> datetime:
124 return before(now(), seconds)
127def seconds_after(timestamp, seconds) -> datetime:
128 return timestamp + timedelta(seconds=seconds)
131def seconds_after_now(seconds: int):
132 return seconds_after(datetime.utcnow(), seconds)
135def days_after(timestamp, days):
136 return timestamp + timedelta(days=days)
139def days_after_now(days: int):
140 return days_after(datetime.utcnow(), days)
143def eta(since, sofar, total) -> str:
144 td = (now() - since).total_seconds()
145 spr = float(td) / float(sofar)
146 alltime = int(math.ceil(total * spr))
147 fin = seconds_after(since, alltime)
148 return format(fin)
151def day_ranges(fro: datetime, to: datetime) -> 'list[str]':
152 aday = timedelta(days=1)
154 # first, workout when the next midnight point is
155 next_day = fro + aday
156 next_midnight = datetime(next_day.year, next_day.month, next_day.day)
158 # in the degenerate case, to is before the next midnight, in which case they both
159 # fall within the one day range
160 if next_midnight > to:
161 return [(format(fro), format(to))]
163 # start the range off with the remainder of the first day
164 ranges = [(format(fro), format(next_midnight))]
166 # go through each day, adding to the range, until the next day is after
167 # the "to" date, then finish up and return
168 current = next_midnight
169 while True:
170 next = current + aday
171 if next > to:
172 ranges.append((format(current), format(to)))
173 break
174 else:
175 ranges.append((format(current), format(next)))
176 current = next
178 return ranges
181def human_date(stamp, string_format=FMT_DATE_HUMAN) -> str:
182 return reformat(stamp, out_format=string_format)
184def is_before(mydate, comparison=None):
185 if comparison is None:
186 comparison = datetime.utcnow()
187 if isinstance(mydate, str):
188 mydate = parse(mydate)
189 if isinstance(comparison, str):
190 comparison = parse(comparison)
191 return mydate < comparison
193def is_after(mydate, comparison=None):
194 if comparison is None:
195 comparison = datetime.utcnow()
196 if isinstance(mydate, str):
197 mydate = parse(mydate)
198 if isinstance(comparison, str):
199 comparison = parse(comparison)
200 return mydate > comparison
202def timestruct2datetime(ts):
203 return datetime.fromtimestamp(time.mktime(ts))
206def find_earliest_date(dates_arr, dates_format, out_format=None):
207 parsed_dates = [parse(date, dates_format) for date in dates_arr]
208 earliest_date = min(parsed_dates)
209 return format(earliest_date, out_format or dates_format)