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:
84 return value in cls._value2member_map_
85 elif type(value) is TimeUnit:
87 return value.value in cls._value2member_map_
88 elif type(value) is str:
90 return value in cls._member_names_
96 def n_timeunits_from_base(
99 base: datetime.datetime
100 ) -> datetime.datetime:
101 assert TimeUnit.is_valid(unit)
106 if unit == TimeUnit.DAYS:
107 timedelta = datetime.timedelta(days=count)
108 return base + timedelta
110 # N workdays from base
111 elif unit == TimeUnit.WORKDAYS:
114 timedelta = datetime.timedelta(days=-1)
116 timedelta = datetime.timedelta(days=1)
117 skips = holidays.US(years=base.year).keys()
121 if base.year != old_year:
122 skips = holidays.US(years=base.year).keys()
124 base.weekday() < 5 and
125 datetime.date(base.year,
127 base.day) not in skips
133 elif unit == TimeUnit.WEEKS:
134 timedelta = datetime.timedelta(weeks=count)
135 base = base + timedelta
139 elif unit == TimeUnit.MONTHS:
140 month_term = count % 12
141 year_term = count // 12
142 new_month = base.month + month_term
146 new_year = base.year + year_term
147 return datetime.datetime(
158 elif unit == TimeUnit.YEARS:
159 new_year = base.year + count
160 return datetime.datetime(
170 # N weekdays from base (e.g. 4 wednesdays from today)
171 direction = 1 if count > 0 else -1
173 timedelta = datetime.timedelta(days=direction)
177 if dow == unit and start != base:
181 base = base + timedelta
184 def get_format_string(
186 date_time_separator=" ",
187 include_timezone=True,
188 include_dayname=False,
189 use_month_abbrevs=False,
190 include_seconds=True,
191 include_fractional=False,
198 if use_month_abbrevs:
199 fstring = f"%Y/%b/%d{date_time_separator}"
201 fstring = f"%Y/%m/%d{date_time_separator}"
211 if include_fractional:
218 def datetime_to_string(
219 dt: datetime.datetime,
221 date_time_separator=" ",
222 include_timezone=True,
223 include_dayname=False,
224 use_month_abbrevs=False,
225 include_seconds=True,
226 include_fractional=False,
229 """A nice way to convert a datetime into a string."""
230 fstring = get_format_string(
231 date_time_separator=date_time_separator,
232 include_timezone=include_timezone,
233 include_dayname=include_dayname,
234 include_seconds=include_seconds,
235 include_fractional=include_fractional,
236 twelve_hour=twelve_hour)
237 return dt.strftime(fstring).strip()
240 def string_to_datetime(
243 date_time_separator=" ",
244 include_timezone=True,
245 include_dayname=False,
246 use_month_abbrevs=False,
247 include_seconds=True,
248 include_fractional=False,
250 ) -> Tuple[datetime.datetime, str]:
251 """A nice way to convert a string into a datetime. Also consider
252 dateparse.dateparse_utils for a full parser.
254 fstring = get_format_string(
255 date_time_separator=date_time_separator,
256 include_timezone=include_timezone,
257 include_dayname=include_dayname,
258 include_seconds=include_seconds,
259 include_fractional=include_fractional,
260 twelve_hour=twelve_hour)
262 datetime.datetime.strptime(txt, fstring),
267 def timestamp() -> str:
268 """Return a timestamp for now in Pacific timezone."""
269 ts = datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
270 return datetime_to_string(ts, include_timezone=True)
274 dt: datetime.datetime,
276 include_seconds=True,
277 include_fractional=False,
278 include_timezone=False,
281 """A nice way to convert a datetime into a time (only) string."""
292 if include_fractional:
296 return dt.strftime(fstring).strip()
299 def seconds_to_timedelta(seconds: int) -> datetime.timedelta:
300 """Convert a delta in seconds into a timedelta."""
301 return datetime.timedelta(seconds=seconds)
304 MinuteOfDay = NewType("MinuteOfDay", int)
307 def minute_number(hour: int, minute: int) -> MinuteOfDay:
308 """Convert hour:minute into minute number from start of day."""
309 return MinuteOfDay(hour * 60 + minute)
312 def datetime_to_minute_number(dt: datetime.datetime) -> MinuteOfDay:
313 """Convert a datetime into a minute number (of the day)"""
314 return minute_number(dt.hour, dt.minute)
317 def minute_number_to_time_string(minute_num: MinuteOfDay) -> str:
318 """Convert minute number from start of day into hour:minute am/pm
321 hour = minute_num // 60
322 minute = minute_num % 60
331 return f"{hour:2}:{minute:02}{ampm}"
334 def parse_duration(duration: str) -> int:
335 """Parse a duration in string form."""
337 m = re.search(r'(\d+) *d[ays]*', duration)
339 seconds += int(m.group(1)) * 60 * 60 * 24
340 m = re.search(r'(\d+) *h[ours]*', duration)
342 seconds += int(m.group(1)) * 60 * 60
343 m = re.search(r'(\d+) *m[inutes]*', duration)
345 seconds += int(m.group(1)) * 60
346 m = re.search(r'(\d+) *s[econds]*', duration)
348 seconds += int(m.group(1))
352 def describe_duration(age: int) -> str:
353 """Describe a duration."""
354 days = divmod(age, constants.SECONDS_PER_DAY)
355 hours = divmod(days[1], constants.SECONDS_PER_HOUR)
356 minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
360 descr = f"{int(days[0])} days, "
364 descr = descr + f"{int(hours[0])} hours, "
366 descr = descr + "1 hour, "
368 descr = descr + "and "
370 descr = descr + "1 minute"
372 descr = descr + f"{int(minutes[0])} minutes"
376 def describe_duration_briefly(age: int) -> str:
377 """Describe a duration briefly."""
378 days = divmod(age, constants.SECONDS_PER_DAY)
379 hours = divmod(days[1], constants.SECONDS_PER_HOUR)
380 minutes = divmod(hours[1], constants.SECONDS_PER_MINUTE)
384 descr = f"{int(days[0])}d "
386 descr = descr + f"{int(hours[0])}h "
387 if minutes[0] > 0 or len(descr) == 0:
388 descr = descr + f"{int(minutes[0])}m"