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_and_time(
56 ) -> Tuple[datetime.date, datetime.time]:
57 return (dt.date(), dt.timetz())
60 def datetime_to_date(dt: datetime.datetime) -> datetime.date:
61 return datetime_to_date_and_time(dt)[0]
64 def datetime_to_time(dt: datetime.datetime) -> datetime.time:
65 return datetime_to_date_and_time(dt)[1]
68 # An enum to represent units with which we can compute deltas.
69 class TimeUnit(enum.Enum):
87 def is_valid(cls, value: Any):
88 if type(value) is int:
89 return value in cls._value2member_map_
90 elif type(value) is TimeUnit:
91 return value.value in cls._value2member_map_
92 elif type(value) is str:
93 return value in cls._member_names_
99 def n_timeunits_from_base(
102 base: datetime.datetime
103 ) -> datetime.datetime:
104 assert TimeUnit.is_valid(unit)
109 if unit == TimeUnit.DAYS:
110 timedelta = datetime.timedelta(days=count)
111 return base + timedelta
113 # N workdays from base
114 elif unit == TimeUnit.WORKDAYS:
117 timedelta = datetime.timedelta(days=-1)
119 timedelta = datetime.timedelta(days=1)
120 skips = holidays.US(years=base.year).keys()
124 if base.year != old_year:
125 skips = holidays.US(years=base.year).keys()
127 base.weekday() < 5 and
128 datetime.date(base.year,
130 base.day) not in skips
136 elif unit == TimeUnit.WEEKS:
137 timedelta = datetime.timedelta(weeks=count)
138 base = base + timedelta
142 elif unit == TimeUnit.MONTHS:
143 month_term = count % 12
144 year_term = count // 12
145 new_month = base.month + month_term
149 new_year = base.year + year_term
150 return datetime.datetime(
161 elif unit == TimeUnit.YEARS:
162 new_year = base.year + count
163 return datetime.datetime(
173 # N weekdays from base (e.g. 4 wednesdays from today)
174 direction = 1 if count > 0 else -1
176 timedelta = datetime.timedelta(days=direction)
180 if dow == unit and start != base:
184 base = base + timedelta
187 def get_format_string(
189 date_time_separator=" ",
190 include_timezone=True,
191 include_dayname=False,
192 use_month_abbrevs=False,
193 include_seconds=True,
194 include_fractional=False,
201 if use_month_abbrevs:
202 fstring = f"%Y/%b/%d{date_time_separator}"
204 fstring = f"%Y/%m/%d{date_time_separator}"
214 if include_fractional:
221 def datetime_to_string(
222 dt: datetime.datetime,
224 date_time_separator=" ",
225 include_timezone=True,
226 include_dayname=False,
227 use_month_abbrevs=False,
228 include_seconds=True,
229 include_fractional=False,
232 """A nice way to convert a datetime into a string."""
233 fstring = get_format_string(
234 date_time_separator=date_time_separator,
235 include_timezone=include_timezone,
236 include_dayname=include_dayname,
237 include_seconds=include_seconds,
238 include_fractional=include_fractional,
239 twelve_hour=twelve_hour)
240 return dt.strftime(fstring).strip()
243 def string_to_datetime(
246 date_time_separator=" ",
247 include_timezone=True,
248 include_dayname=False,
249 use_month_abbrevs=False,
250 include_seconds=True,
251 include_fractional=False,
253 ) -> Tuple[datetime.datetime, str]:
254 """A nice way to convert a string into a datetime. Also consider
255 dateparse.dateparse_utils for a full parser.
257 fstring = get_format_string(
258 date_time_separator=date_time_separator,
259 include_timezone=include_timezone,
260 include_dayname=include_dayname,
261 include_seconds=include_seconds,
262 include_fractional=include_fractional,
263 twelve_hour=twelve_hour)
265 datetime.datetime.strptime(txt, fstring),
270 def timestamp() -> str:
271 """Return a timestamp for now in Pacific timezone."""
272 ts = datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
273 return datetime_to_string(ts, include_timezone=True)
277 dt: datetime.datetime,
279 include_seconds=True,
280 include_fractional=False,
281 include_timezone=False,
284 """A nice way to convert a datetime into a time (only) string."""
295 if include_fractional:
299 return dt.strftime(fstring).strip()
302 def seconds_to_timedelta(seconds: int) -> datetime.timedelta:
303 """Convert a delta in seconds into a timedelta."""
304 return datetime.timedelta(seconds=seconds)
307 MinuteOfDay = NewType("MinuteOfDay", int)
310 def minute_number(hour: int, minute: int) -> MinuteOfDay:
311 """Convert hour:minute into minute number from start of day."""
312 return MinuteOfDay(hour * 60 + minute)
315 def datetime_to_minute_number(dt: datetime.datetime) -> MinuteOfDay:
316 """Convert a datetime into a minute number (of the day)"""
317 return minute_number(dt.hour, dt.minute)
320 def minute_number_to_time_string(minute_num: MinuteOfDay) -> str:
321 """Convert minute number from start of day into hour:minute am/pm
324 hour = minute_num // 60
325 minute = minute_num % 60
334 return f"{hour:2}:{minute:02}{ampm}"
337 def parse_duration(duration: str) -> int:
338 """Parse a duration in string form."""
339 if duration.isdigit():
342 m = re.search(r'(\d+) *d[ays]*', duration)
344 seconds += int(m.group(1)) * 60 * 60 * 24
345 m = re.search(r'(\d+) *h[ours]*', duration)
347 seconds += int(m.group(1)) * 60 * 60
348 m = re.search(r'(\d+) *m[inutes]*', duration)
350 seconds += int(m.group(1)) * 60
351 m = re.search(r'(\d+) *s[econds]*', duration)
353 seconds += int(m.group(1))
357 def describe_duration(age: int) -> str:
358 """Describe a duration."""
359 days = divmod(age, constants.SECONDS_PER_DAY)
360 hours = divmod(days[1], constants.SECONDS_PER_HOUR)
361 minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
365 descr = f"{int(days[0])} days, "
369 descr = descr + f"{int(hours[0])} hours, "
371 descr = descr + "1 hour, "
373 descr = descr + "and "
375 descr = descr + "1 minute"
377 descr = descr + f"{int(minutes[0])} minutes"
381 def describe_duration_briefly(age: int) -> str:
382 """Describe a duration briefly."""
383 days = divmod(age, constants.SECONDS_PER_DAY)
384 hours = divmod(days[1], constants.SECONDS_PER_HOUR)
385 minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
389 descr = f"{int(days[0])}d "
391 descr = descr + f"{int(hours[0])}h "
392 if minutes[0] > 0 or len(descr) == 0:
393 descr = descr + f"{int(minutes[0])}m"