Coverage for portality / lib / dates.py: 88%

119 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-04 09:41 +0100

1# ~~ Dates:Library~~ 

2 

3import math 

4from datetime import datetime, timedelta 

5from random import randint 

6import time 

7 

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", 

12 

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 ], 

31 

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} 

36 

37 

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" 

44 

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' 

52 

53FMT_TIME_SHORT = '%H%M' 

54FMT_YEAR = '%Y' 

55 

56DEFAULT_TIMESTAMP_VAL = config.get('DEFAULT_TIMESTAMP', '1970-01-01T00:00:00Z') 

57 

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) 

60 

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 

69 

70 for f in config.get("DATE_FORMATS", []): 

71 try: 

72 return datetime.strptime(s, f) 

73 except ValueError as e: 

74 pass 

75 

76 raise ValueError("Unable to parse {x} with any known format".format(x=s)) 

77 

78 

79def format(d, format=None) -> str: 

80 return d.strftime(format or FMT_DATETIME_STD) 

81 

82 

83def reformat(s, in_format=None, out_format=None) -> str: 

84 return format(parse(s, format=in_format), format=out_format) 

85 

86 

87def now() -> datetime: 

88 """ standard now function for DOAJ """ 

89 return datetime.utcnow() 

90 

91 

92def now_str(fmt=FMT_DATETIME_STD) -> str: 

93 return format(now(), format=fmt) 

94 

95 

96def now_str_with_microseconds() -> str: 

97 return format(now(), format=FMT_DATETIME_MS_STD) 

98 

99 

100def today(str_format=FMT_DATE_STD) -> str: 

101 return format(now(), format=str_format) 

102 

103 

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) 

113 

114 span = int((to - fro).total_seconds()) 

115 s = randint(0, span) 

116 return format(to - timedelta(seconds=s)) 

117 

118 

119def before(timestamp, seconds) -> datetime: 

120 return timestamp - timedelta(seconds=seconds) 

121 

122 

123def before_now(seconds: int) -> datetime: 

124 return before(now(), seconds) 

125 

126 

127def seconds_after(timestamp, seconds) -> datetime: 

128 return timestamp + timedelta(seconds=seconds) 

129 

130 

131def seconds_after_now(seconds: int): 

132 return seconds_after(datetime.utcnow(), seconds) 

133 

134 

135def days_after(timestamp, days): 

136 return timestamp + timedelta(days=days) 

137 

138 

139def days_after_now(days: int): 

140 return days_after(datetime.utcnow(), days) 

141 

142 

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) 

149 

150 

151def day_ranges(fro: datetime, to: datetime) -> 'list[str]': 

152 aday = timedelta(days=1) 

153 

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) 

157 

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))] 

162 

163 # start the range off with the remainder of the first day 

164 ranges = [(format(fro), format(next_midnight))] 

165 

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 

177 

178 return ranges 

179 

180 

181def human_date(stamp, string_format=FMT_DATE_HUMAN) -> str: 

182 return reformat(stamp, out_format=string_format) 

183 

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 

192 

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 

201 

202def timestruct2datetime(ts): 

203 return datetime.fromtimestamp(time.mktime(ts)) 

204 

205 

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)