3 """Utilities related to dates and times and datetimes."""
9 from typing import Any, 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 is_valid(cls, value: Any):
82 if type(value) is int:
83 return value in cls._value2member_map_
84 elif type(value) is TimeUnit:
85 return value.value in cls._value2member_map_
86 elif type(value) is str:
87 return value in cls._member_names_
93 def n_timeunits_from_base(
96 base: datetime.datetime
97 ) -> datetime.datetime:
98 assert TimeUnit.is_valid(unit)
103 if unit == TimeUnit.DAYS:
104 timedelta = datetime.timedelta(days=count)
105 return base + timedelta
107 # N workdays from base
108 elif unit == TimeUnit.WORKDAYS:
111 timedelta = datetime.timedelta(days=-1)
113 timedelta = datetime.timedelta(days=1)
114 skips = holidays.US(years=base.year).keys()
118 if base.year != old_year:
119 skips = holidays.US(years=base.year).keys()
121 base.weekday() < 5 and
122 datetime.date(base.year,
124 base.day) not in skips
130 elif unit == TimeUnit.WEEKS:
131 timedelta = datetime.timedelta(weeks=count)
132 base = base + timedelta
136 elif unit == TimeUnit.MONTHS:
137 month_term = count % 12
138 year_term = count // 12
139 new_month = base.month + month_term
143 new_year = base.year + year_term
144 return datetime.datetime(
155 elif unit == TimeUnit.YEARS:
156 new_year = base.year + count
157 return datetime.datetime(
167 # N weekdays from base (e.g. 4 wednesdays from today)
168 direction = 1 if count > 0 else -1
170 timedelta = datetime.timedelta(days=direction)
174 if dow == unit and start != base:
178 base = base + timedelta
181 def get_format_string(
183 date_time_separator=" ",
184 include_timezone=True,
185 include_dayname=False,
186 use_month_abbrevs=False,
187 include_seconds=True,
188 include_fractional=False,
195 if use_month_abbrevs:
196 fstring = f"%Y/%b/%d{date_time_separator}"
198 fstring = f"%Y/%m/%d{date_time_separator}"
208 if include_fractional:
215 def datetime_to_string(
216 dt: datetime.datetime,
218 date_time_separator=" ",
219 include_timezone=True,
220 include_dayname=False,
221 use_month_abbrevs=False,
222 include_seconds=True,
223 include_fractional=False,
226 """A nice way to convert a datetime into a string."""
227 fstring = get_format_string(
228 date_time_separator=date_time_separator,
229 include_timezone=include_timezone,
230 include_dayname=include_dayname,
231 include_seconds=include_seconds,
232 include_fractional=include_fractional,
233 twelve_hour=twelve_hour)
234 return dt.strftime(fstring).strip()
237 def string_to_datetime(
240 date_time_separator=" ",
241 include_timezone=True,
242 include_dayname=False,
243 use_month_abbrevs=False,
244 include_seconds=True,
245 include_fractional=False,
247 ) -> Tuple[datetime.datetime, str]:
248 """A nice way to convert a string into a datetime. Also consider
249 dateparse.dateparse_utils for a full parser.
251 fstring = get_format_string(
252 date_time_separator=date_time_separator,
253 include_timezone=include_timezone,
254 include_dayname=include_dayname,
255 include_seconds=include_seconds,
256 include_fractional=include_fractional,
257 twelve_hour=twelve_hour)
259 datetime.datetime.strptime(txt, fstring),
264 def timestamp() -> str:
265 """Return a timestamp for now in Pacific timezone."""
266 ts = datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
267 return datetime_to_string(ts, include_timezone=True)
271 dt: datetime.datetime,
273 include_seconds=True,
274 include_fractional=False,
275 include_timezone=False,
278 """A nice way to convert a datetime into a time (only) string."""
289 if include_fractional:
293 return dt.strftime(fstring).strip()
296 def seconds_to_timedelta(seconds: int) -> datetime.timedelta:
297 """Convert a delta in seconds into a timedelta."""
298 return datetime.timedelta(seconds=seconds)
301 MinuteOfDay = NewType("MinuteOfDay", int)
304 def minute_number(hour: int, minute: int) -> MinuteOfDay:
305 """Convert hour:minute into minute number from start of day."""
306 return MinuteOfDay(hour * 60 + minute)
309 def datetime_to_minute_number(dt: datetime.datetime) -> MinuteOfDay:
310 """Convert a datetime into a minute number (of the day)"""
311 return minute_number(dt.hour, dt.minute)
314 def minute_number_to_time_string(minute_num: MinuteOfDay) -> str:
315 """Convert minute number from start of day into hour:minute am/pm
318 hour = minute_num // 60
319 minute = minute_num % 60
328 return f"{hour:2}:{minute:02}{ampm}"
331 def parse_duration(duration: str) -> int:
332 """Parse a duration in string form."""
334 m = re.search(r'(\d+) *d[ays]*', duration)
336 seconds += int(m.group(1)) * 60 * 60 * 24
337 m = re.search(r'(\d+) *h[ours]*', duration)
339 seconds += int(m.group(1)) * 60 * 60
340 m = re.search(r'(\d+) *m[inutes]*', duration)
342 seconds += int(m.group(1)) * 60
343 m = re.search(r'(\d+) *s[econds]*', duration)
345 seconds += int(m.group(1))
349 def describe_duration(age: int) -> str:
350 """Describe a duration."""
351 days = divmod(age, constants.SECONDS_PER_DAY)
352 hours = divmod(days[1], constants.SECONDS_PER_HOUR)
353 minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
357 descr = f"{int(days[0])} days, "
361 descr = descr + f"{int(hours[0])} hours, "
363 descr = descr + "1 hour, "
365 descr = descr + "and "
367 descr = descr + "1 minute"
369 descr = descr + f"{int(minutes[0])} minutes"
373 def describe_duration_briefly(age: int) -> str:
374 """Describe a duration briefly."""
375 days = divmod(age, constants.SECONDS_PER_DAY)
376 hours = divmod(days[1], constants.SECONDS_PER_HOUR)
377 minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
381 descr = f"{int(days[0])}d "
383 descr = descr + f"{int(hours[0])}h "
384 if minutes[0] > 0 or len(descr) == 0:
385 descr = descr + f"{int(minutes[0])}m"