| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- """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"]
|