|
"""Helpful functions used internally within arrow.""" |
|
|
|
import datetime |
|
from typing import Any, Optional, cast |
|
|
|
from dateutil.rrule import WEEKLY, rrule |
|
|
|
from arrow.constants import ( |
|
MAX_ORDINAL, |
|
MAX_TIMESTAMP, |
|
MAX_TIMESTAMP_MS, |
|
MAX_TIMESTAMP_US, |
|
MIN_ORDINAL, |
|
) |
|
|
|
|
|
def next_weekday( |
|
start_date: Optional[datetime.date], weekday: int |
|
) -> datetime.datetime: |
|
"""Get next weekday from the specified start date. |
|
|
|
:param start_date: Datetime object representing the start date. |
|
:param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday). |
|
:return: Datetime object corresponding to the next weekday after start_date. |
|
|
|
Usage:: |
|
|
|
# Get first Monday after epoch |
|
>>> next_weekday(datetime(1970, 1, 1), 0) |
|
1970-01-05 00:00:00 |
|
|
|
# Get first Thursday after epoch |
|
>>> next_weekday(datetime(1970, 1, 1), 3) |
|
1970-01-01 00:00:00 |
|
|
|
# Get first Sunday after epoch |
|
>>> next_weekday(datetime(1970, 1, 1), 6) |
|
1970-01-04 00:00:00 |
|
""" |
|
if weekday < 0 or weekday > 6: |
|
raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).") |
|
return cast( |
|
datetime.datetime, |
|
rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0], |
|
) |
|
|
|
|
|
def is_timestamp(value: Any) -> bool: |
|
"""Check if value is a valid timestamp.""" |
|
if isinstance(value, bool): |
|
return False |
|
if not isinstance(value, (int, float, str)): |
|
return False |
|
try: |
|
float(value) |
|
return True |
|
except ValueError: |
|
return False |
|
|
|
|
|
def validate_ordinal(value: Any) -> None: |
|
"""Raise an exception if value is an invalid Gregorian ordinal. |
|
|
|
:param value: the input to be checked |
|
|
|
""" |
|
if isinstance(value, bool) or not isinstance(value, int): |
|
raise TypeError(f"Ordinal must be an integer (got type {type(value)}).") |
|
if not (MIN_ORDINAL <= value <= MAX_ORDINAL): |
|
raise ValueError(f"Ordinal {value} is out of range.") |
|
|
|
|
|
def normalize_timestamp(timestamp: float) -> float: |
|
"""Normalize millisecond and microsecond timestamps into normal timestamps.""" |
|
if timestamp > MAX_TIMESTAMP: |
|
if timestamp < MAX_TIMESTAMP_MS: |
|
timestamp /= 1000 |
|
elif timestamp < MAX_TIMESTAMP_US: |
|
timestamp /= 1_000_000 |
|
else: |
|
raise ValueError(f"The specified timestamp {timestamp!r} is too large.") |
|
return timestamp |
|
|
|
|
|
|
|
def iso_to_gregorian(iso_year: int, iso_week: int, iso_day: int) -> datetime.date: |
|
"""Converts an ISO week date into a datetime object. |
|
|
|
:param iso_year: the year |
|
:param iso_week: the week number, each year has either 52 or 53 weeks |
|
:param iso_day: the day numbered 1 through 7, beginning with Monday |
|
|
|
""" |
|
|
|
if not 1 <= iso_week <= 53: |
|
raise ValueError("ISO Calendar week value must be between 1-53.") |
|
|
|
if not 1 <= iso_day <= 7: |
|
raise ValueError("ISO Calendar day value must be between 1-7") |
|
|
|
|
|
fourth_jan = datetime.date(iso_year, 1, 4) |
|
delta = datetime.timedelta(fourth_jan.isoweekday() - 1) |
|
year_start = fourth_jan - delta |
|
gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1) |
|
|
|
return gregorian |
|
|
|
|
|
def validate_bounds(bounds: str) -> None: |
|
if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]": |
|
raise ValueError( |
|
"Invalid bounds. Please select between '()', '(]', '[)', or '[]'." |
|
) |
|
|
|
|
|
__all__ = ["next_weekday", "is_timestamp", "validate_ordinal", "iso_to_gregorian"] |
|
|