"""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 # Credit to https://stackoverflow.com/a/1700069 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") # The first week of the year always contains 4 Jan. 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"]