util.py 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. """Helpful functions used internally within arrow."""
  2. import datetime
  3. from typing import Any, Optional, cast
  4. from dateutil.rrule import WEEKLY, rrule
  5. from arrow.constants import (
  6. MAX_ORDINAL,
  7. MAX_TIMESTAMP,
  8. MAX_TIMESTAMP_MS,
  9. MAX_TIMESTAMP_US,
  10. MIN_ORDINAL,
  11. )
  12. def next_weekday(
  13. start_date: Optional[datetime.date], weekday: int
  14. ) -> datetime.datetime:
  15. """Get next weekday from the specified start date.
  16. :param start_date: Datetime object representing the start date.
  17. :param weekday: Next weekday to obtain. Can be a value between 0 (Monday) and 6 (Sunday).
  18. :return: Datetime object corresponding to the next weekday after start_date.
  19. Usage::
  20. # Get first Monday after epoch
  21. >>> next_weekday(datetime(1970, 1, 1), 0)
  22. 1970-01-05 00:00:00
  23. # Get first Thursday after epoch
  24. >>> next_weekday(datetime(1970, 1, 1), 3)
  25. 1970-01-01 00:00:00
  26. # Get first Sunday after epoch
  27. >>> next_weekday(datetime(1970, 1, 1), 6)
  28. 1970-01-04 00:00:00
  29. """
  30. if weekday < 0 or weekday > 6:
  31. raise ValueError("Weekday must be between 0 (Monday) and 6 (Sunday).")
  32. return cast(
  33. datetime.datetime,
  34. rrule(freq=WEEKLY, dtstart=start_date, byweekday=weekday, count=1)[0],
  35. )
  36. def is_timestamp(value: Any) -> bool:
  37. """Check if value is a valid timestamp."""
  38. if isinstance(value, bool):
  39. return False
  40. if not isinstance(value, (int, float, str)):
  41. return False
  42. try:
  43. float(value)
  44. return True
  45. except ValueError:
  46. return False
  47. def validate_ordinal(value: Any) -> None:
  48. """Raise an exception if value is an invalid Gregorian ordinal.
  49. :param value: the input to be checked
  50. """
  51. if isinstance(value, bool) or not isinstance(value, int):
  52. raise TypeError(f"Ordinal must be an integer (got type {type(value)}).")
  53. if not (MIN_ORDINAL <= value <= MAX_ORDINAL):
  54. raise ValueError(f"Ordinal {value} is out of range.")
  55. def normalize_timestamp(timestamp: float) -> float:
  56. """Normalize millisecond and microsecond timestamps into normal timestamps."""
  57. if timestamp > MAX_TIMESTAMP:
  58. if timestamp < MAX_TIMESTAMP_MS:
  59. timestamp /= 1000
  60. elif timestamp < MAX_TIMESTAMP_US:
  61. timestamp /= 1_000_000
  62. else:
  63. raise ValueError(f"The specified timestamp {timestamp!r} is too large.")
  64. return timestamp
  65. # Credit to https://stackoverflow.com/a/1700069
  66. def iso_to_gregorian(iso_year: int, iso_week: int, iso_day: int) -> datetime.date:
  67. """Converts an ISO week date into a datetime object.
  68. :param iso_year: the year
  69. :param iso_week: the week number, each year has either 52 or 53 weeks
  70. :param iso_day: the day numbered 1 through 7, beginning with Monday
  71. """
  72. if not 1 <= iso_week <= 53:
  73. raise ValueError("ISO Calendar week value must be between 1-53.")
  74. if not 1 <= iso_day <= 7:
  75. raise ValueError("ISO Calendar day value must be between 1-7")
  76. # The first week of the year always contains 4 Jan.
  77. fourth_jan = datetime.date(iso_year, 1, 4)
  78. delta = datetime.timedelta(fourth_jan.isoweekday() - 1)
  79. year_start = fourth_jan - delta
  80. gregorian = year_start + datetime.timedelta(days=iso_day - 1, weeks=iso_week - 1)
  81. return gregorian
  82. def validate_bounds(bounds: str) -> None:
  83. if bounds != "()" and bounds != "(]" and bounds != "[)" and bounds != "[]":
  84. raise ValueError(
  85. "Invalid bounds. Please select between '()', '(]', '[)', or '[]'."
  86. )
  87. __all__ = ["next_weekday", "is_timestamp", "validate_ordinal", "iso_to_gregorian"]