3 """Utilities related to dates and times and datetimes."""
9 from typing import NewType, Tuple
11 import holidays # type: ignore
16 logger = logging.getLogger(__name__)
19 def replace_timezone(dt: datetime.datetime,
20 tz: datetime.tzinfo) -> datetime.datetime:
21 return dt.replace(tzinfo=None).astimezone(tz=tz)
24 def now() -> datetime.datetime:
25 return datetime.datetime.now()
28 def now_pst() -> datetime.datetime:
29 return replace_timezone(now(), pytz.timezone("US/Pacific"))
32 def date_to_datetime(date: datetime.date) -> datetime.datetime:
33 return datetime.datetime(
41 def date_and_time_to_datetime(date: datetime.date,
42 time: datetime.time) -> datetime.datetime:
43 return datetime.datetime(
54 def datetime_to_date(date: datetime.datetime) -> datetime.date:
62 # An enum to represent units with which we can compute deltas.
63 class TimeUnit(enum.Enum):
81 def n_timeunits_from_base(
84 base: datetime.datetime
85 ) -> datetime.datetime:
90 if unit == TimeUnit.DAYS:
91 timedelta = datetime.timedelta(days=count)
92 return base + timedelta
94 # N workdays from base
95 elif unit == TimeUnit.WORKDAYS:
98 timedelta = datetime.timedelta(days=-1)
100 timedelta = datetime.timedelta(days=1)
101 skips = holidays.US(years=base.year).keys()
105 if base.year != old_year:
106 skips = holidays.US(years=base.year).keys()
108 base.weekday() < 5 and
109 datetime.date(base.year,
111 base.day) not in skips
117 elif unit == TimeUnit.WEEKS:
118 timedelta = datetime.timedelta(weeks=count)
119 base = base + timedelta
123 elif unit == TimeUnit.MONTHS:
124 month_term = count % 12
125 year_term = count // 12
126 new_month = base.month + month_term
130 new_year = base.year + year_term
131 return datetime.datetime(
142 elif unit == TimeUnit.YEARS:
143 new_year = base.year + count
144 return datetime.datetime(
154 # N weekdays from base (e.g. 4 wednesdays from today)
155 direction = 1 if count > 0 else -1
157 timedelta = datetime.timedelta(days=direction)
161 if dow == unit and start != base:
165 base = base + timedelta
168 def get_format_string(
170 date_time_separator=" ",
171 include_timezone=True,
172 include_dayname=False,
173 use_month_abbrevs=False,
174 include_seconds=True,
175 include_fractional=False,
182 if use_month_abbrevs:
183 fstring = f"%Y/%b/%d{date_time_separator}"
185 fstring = f"%Y/%m/%d{date_time_separator}"
195 if include_fractional:
202 def datetime_to_string(
203 dt: datetime.datetime,
205 date_time_separator=" ",
206 include_timezone=True,
207 include_dayname=False,
208 use_month_abbrevs=False,
209 include_seconds=True,
210 include_fractional=False,
213 """A nice way to convert a datetime into a string."""
214 fstring = get_format_string(
215 date_time_separator=date_time_separator,
216 include_timezone=include_timezone,
217 include_dayname=include_dayname,
218 include_seconds=include_seconds,
219 include_fractional=include_fractional,
220 twelve_hour=twelve_hour)
221 return dt.strftime(fstring).strip()
224 def string_to_datetime(
227 date_time_separator=" ",
228 include_timezone=True,
229 include_dayname=False,
230 use_month_abbrevs=False,
231 include_seconds=True,
232 include_fractional=False,
234 ) -> Tuple[datetime.datetime, str]:
235 """A nice way to convert a string into a datetime. Also consider
236 dateparse.dateparse_utils for a full parser.
238 fstring = get_format_string(
239 date_time_separator=date_time_separator,
240 include_timezone=include_timezone,
241 include_dayname=include_dayname,
242 include_seconds=include_seconds,
243 include_fractional=include_fractional,
244 twelve_hour=twelve_hour)
246 datetime.datetime.strptime(txt, fstring),
251 def timestamp() -> str:
252 """Return a timestamp for now in Pacific timezone."""
253 ts = datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
254 return datetime_to_string(ts, include_timezone=True)
258 dt: datetime.datetime,
260 include_seconds=True,
261 include_fractional=False,
262 include_timezone=False,
265 """A nice way to convert a datetime into a time (only) string."""
276 if include_fractional:
280 return dt.strftime(fstring).strip()
283 def seconds_to_timedelta(seconds: int) -> datetime.timedelta:
284 """Convert a delta in seconds into a timedelta."""
285 return datetime.timedelta(seconds=seconds)
288 MinuteOfDay = NewType("MinuteOfDay", int)
291 def minute_number(hour: int, minute: int) -> MinuteOfDay:
292 """Convert hour:minute into minute number from start of day."""
293 return MinuteOfDay(hour * 60 + minute)
296 def datetime_to_minute_number(dt: datetime.datetime) -> MinuteOfDay:
297 """Convert a datetime into a minute number (of the day)"""
298 return minute_number(dt.hour, dt.minute)
301 def minute_number_to_time_string(minute_num: MinuteOfDay) -> str:
302 """Convert minute number from start of day into hour:minute am/pm
305 hour = minute_num // 60
306 minute = minute_num % 60
315 return f"{hour:2}:{minute:02}{ampm}"
318 def parse_duration(duration: str) -> int:
319 """Parse a duration in string form."""
321 m = re.search(r'(\d+) *d[ays]*', duration)
323 seconds += int(m.group(1)) * 60 * 60 * 24
324 m = re.search(r'(\d+) *h[ours]*', duration)
326 seconds += int(m.group(1)) * 60 * 60
327 m = re.search(r'(\d+) *m[inutes]*', duration)
329 seconds += int(m.group(1)) * 60
330 m = re.search(r'(\d+) *s[econds]*', duration)
332 seconds += int(m.group(1))
336 def describe_duration(age: int) -> str:
337 """Describe a duration."""
338 days = divmod(age, constants.SECONDS_PER_DAY)
339 hours = divmod(days[1], constants.SECONDS_PER_HOUR)
340 minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
344 descr = f"{int(days[0])} days, "
348 descr = descr + f"{int(hours[0])} hours, "
350 descr = descr + "1 hour, "
352 descr = descr + "and "
354 descr = descr + "1 minute"
356 descr = descr + f"{int(minutes[0])} minutes"
360 def describe_duration_briefly(age: int) -> str:
361 """Describe a duration briefly."""
362 days = divmod(age, constants.SECONDS_PER_DAY)
363 hours = divmod(days[1], constants.SECONDS_PER_HOUR)
364 minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
368 descr = f"{int(days[0])}d "
370 descr = descr + f"{int(hours[0])}h "
371 if minutes[0] > 0 or len(descr) == 0:
372 descr = descr + f"{int(minutes[0])}m"