"""Utilities related to dates and times and datetimes."""
import datetime
+import enum
import logging
import re
-from typing import NewType
+from typing import Any, NewType, Tuple
+import holidays # type: ignore
import pytz
import constants
logger = logging.getLogger(__name__)
-def now_pst() -> datetime.datetime:
- return datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
+def replace_timezone(dt: datetime.datetime,
+ tz: datetime.tzinfo) -> datetime.datetime:
+ return dt.replace(tzinfo=None).astimezone(tz=tz)
def now() -> datetime.datetime:
return datetime.datetime.now()
-def datetime_to_string(
- dt: datetime.datetime,
- *,
- date_time_separator=" ",
- include_timezone=True,
- include_dayname=False,
- include_seconds=True,
- include_fractional=False,
- twelve_hour=True,
+def now_pst() -> datetime.datetime:
+ return replace_timezone(now(), pytz.timezone("US/Pacific"))
+
+
+def date_to_datetime(date: datetime.date) -> datetime.datetime:
+ return datetime.datetime(
+ date.year,
+ date.month,
+ date.day,
+ 0, 0, 0, 0
+ )
+
+
+def date_and_time_to_datetime(date: datetime.date,
+ time: datetime.time) -> datetime.datetime:
+ return datetime.datetime(
+ date.year,
+ date.month,
+ date.day,
+ time.hour,
+ time.minute,
+ time.second,
+ time.millisecond
+ )
+
+
+def datetime_to_date_and_time(
+ dt: datetime.datetime
+) -> Tuple[datetime.date, datetime.time]:
+ return (dt.date(), dt.timetz())
+
+
+def datetime_to_date(dt: datetime.datetime) -> datetime.date:
+ return datetime_to_date_and_time(dt)[0]
+
+
+def datetime_to_time(dt: datetime.datetime) -> datetime.time:
+ return datetime_to_date_and_time(dt)[1]
+
+
+# An enum to represent units with which we can compute deltas.
+class TimeUnit(enum.Enum):
+ MONDAYS = 0
+ TUESDAYS = 1
+ WEDNESDAYS = 2
+ THURSDAYS = 3
+ FRIDAYS = 4
+ SATURDAYS = 5
+ SUNDAYS = 6
+ SECONDS = 7
+ MINUTES = 8
+ HOURS = 9
+ DAYS = 10
+ WORKDAYS = 11
+ WEEKS = 12
+ MONTHS = 13
+ YEARS = 14
+
+ @classmethod
+ def is_valid(cls, value: Any):
+ if type(value) is int:
+ return value in cls._value2member_map_
+ elif type(value) is TimeUnit:
+ return value.value in cls._value2member_map_
+ elif type(value) is str:
+ return value in cls._member_names_
+ else:
+ print(type(value))
+ return False
+
+
+def n_timeunits_from_base(
+ count: int,
+ unit: TimeUnit,
+ base: datetime.datetime
+) -> datetime.datetime:
+ assert TimeUnit.is_valid(unit)
+ if count == 0:
+ return base
+
+ # N days from base
+ if unit == TimeUnit.DAYS:
+ timedelta = datetime.timedelta(days=count)
+ return base + timedelta
+
+ # N workdays from base
+ elif unit == TimeUnit.WORKDAYS:
+ if count < 0:
+ count = abs(count)
+ timedelta = datetime.timedelta(days=-1)
+ else:
+ timedelta = datetime.timedelta(days=1)
+ skips = holidays.US(years=base.year).keys()
+ while count > 0:
+ old_year = base.year
+ base += timedelta
+ if base.year != old_year:
+ skips = holidays.US(years=base.year).keys()
+ if (
+ base.weekday() < 5 and
+ datetime.date(base.year,
+ base.month,
+ base.day) not in skips
+ ):
+ count -= 1
+ return base
+
+ # N weeks from base
+ elif unit == TimeUnit.WEEKS:
+ timedelta = datetime.timedelta(weeks=count)
+ base = base + timedelta
+ return base
+
+ # N months from base
+ elif unit == TimeUnit.MONTHS:
+ month_term = count % 12
+ year_term = count // 12
+ new_month = base.month + month_term
+ if new_month > 12:
+ new_month %= 12
+ year_term += 1
+ new_year = base.year + year_term
+ return datetime.datetime(
+ new_year,
+ new_month,
+ base.day,
+ base.hour,
+ base.minute,
+ base.second,
+ base.microsecond,
+ )
+
+ # N years from base
+ elif unit == TimeUnit.YEARS:
+ new_year = base.year + count
+ return datetime.datetime(
+ new_year,
+ base.month,
+ base.day,
+ base.hour,
+ base.minute,
+ base.second,
+ base.microsecond,
+ )
+
+ # N weekdays from base (e.g. 4 wednesdays from today)
+ direction = 1 if count > 0 else -1
+ count = abs(count)
+ timedelta = datetime.timedelta(days=direction)
+ start = base
+ while True:
+ dow = base.weekday()
+ if dow == unit and start != base:
+ count -= 1
+ if count == 0:
+ return base
+ base = base + timedelta
+
+
+def get_format_string(
+ *,
+ date_time_separator=" ",
+ include_timezone=True,
+ include_dayname=False,
+ use_month_abbrevs=False,
+ include_seconds=True,
+ include_fractional=False,
+ twelve_hour=True,
) -> str:
- """A nice way to convert a datetime into a string."""
fstring = ""
if include_dayname:
fstring += "%a/"
- fstring = f"%Y/%b/%d{date_time_separator}"
+
+ if use_month_abbrevs:
+ fstring = f"%Y/%b/%d{date_time_separator}"
+ else:
+ fstring = f"%Y/%m/%d{date_time_separator}"
if twelve_hour:
fstring += "%I:%M"
if include_seconds:
fstring += ".%f"
if include_timezone:
fstring += "%z"
+ return fstring
+
+
+def datetime_to_string(
+ dt: datetime.datetime,
+ *,
+ date_time_separator=" ",
+ include_timezone=True,
+ include_dayname=False,
+ use_month_abbrevs=False,
+ include_seconds=True,
+ include_fractional=False,
+ twelve_hour=True,
+) -> str:
+ """A nice way to convert a datetime into a string."""
+ fstring = get_format_string(
+ date_time_separator=date_time_separator,
+ include_timezone=include_timezone,
+ include_dayname=include_dayname,
+ include_seconds=include_seconds,
+ include_fractional=include_fractional,
+ twelve_hour=twelve_hour)
return dt.strftime(fstring).strip()
+def string_to_datetime(
+ txt: str,
+ *,
+ date_time_separator=" ",
+ include_timezone=True,
+ include_dayname=False,
+ use_month_abbrevs=False,
+ include_seconds=True,
+ include_fractional=False,
+ twelve_hour=True,
+) -> Tuple[datetime.datetime, str]:
+ """A nice way to convert a string into a datetime. Also consider
+ dateparse.dateparse_utils for a full parser.
+ """
+ fstring = get_format_string(
+ date_time_separator=date_time_separator,
+ include_timezone=include_timezone,
+ include_dayname=include_dayname,
+ include_seconds=include_seconds,
+ include_fractional=include_fractional,
+ twelve_hour=twelve_hour)
+ return (
+ datetime.datetime.strptime(txt, fstring),
+ fstring
+ )
+
+
def timestamp() -> str:
"""Return a timestamp for now in Pacific timezone."""
ts = datetime.datetime.now(tz=pytz.timezone("US/Pacific"))
def minute_number_to_time_string(minute_num: MinuteOfDay) -> str:
- """Convert minute number from start of day into hour:minute am/pm string."""
+ """Convert minute number from start of day into hour:minute am/pm
+ string.
+ """
hour = minute_num // 60
minute = minute_num % 60
ampm = "a"
def parse_duration(duration: str) -> int:
"""Parse a duration in string form."""
+ if duration.isdigit():
+ return int(duration)
seconds = 0
m = re.search(r'(\d+) *d[ays]*', duration)
if m is not None:
descr = f"{int(days[0])}d "
if hours[0] > 0:
descr = descr + f"{int(hours[0])}h "
- descr = descr + f"{int(minutes[0])}m"
- return descr
+ if minutes[0] > 0 or len(descr) == 0:
+ descr = descr + f"{int(minutes[0])}m"
+ return descr.strip()