|
""" |
|
Provides the :class:`Arrow <arrow.arrow.Arrow>` class, an enhanced ``datetime`` |
|
replacement. |
|
|
|
""" |
|
|
|
|
|
import calendar |
|
import re |
|
import sys |
|
from datetime import date |
|
from datetime import datetime as dt_datetime |
|
from datetime import time as dt_time |
|
from datetime import timedelta |
|
from datetime import tzinfo as dt_tzinfo |
|
from math import trunc |
|
from time import struct_time |
|
from typing import ( |
|
Any, |
|
ClassVar, |
|
Generator, |
|
Iterable, |
|
List, |
|
Mapping, |
|
Optional, |
|
Tuple, |
|
Union, |
|
cast, |
|
overload, |
|
) |
|
|
|
from dateutil import tz as dateutil_tz |
|
from dateutil.relativedelta import relativedelta |
|
|
|
from arrow import formatter, locales, parser, util |
|
from arrow.constants import DEFAULT_LOCALE, DEHUMANIZE_LOCALES |
|
from arrow.locales import TimeFrameLiteral |
|
|
|
if sys.version_info < (3, 8): |
|
from typing_extensions import Final, Literal |
|
else: |
|
from typing import Final, Literal |
|
|
|
|
|
TZ_EXPR = Union[dt_tzinfo, str] |
|
|
|
_T_FRAMES = Literal[ |
|
"year", |
|
"years", |
|
"month", |
|
"months", |
|
"day", |
|
"days", |
|
"hour", |
|
"hours", |
|
"minute", |
|
"minutes", |
|
"second", |
|
"seconds", |
|
"microsecond", |
|
"microseconds", |
|
"week", |
|
"weeks", |
|
"quarter", |
|
"quarters", |
|
] |
|
|
|
_BOUNDS = Literal["[)", "()", "(]", "[]"] |
|
|
|
_GRANULARITY = Literal[ |
|
"auto", |
|
"second", |
|
"minute", |
|
"hour", |
|
"day", |
|
"week", |
|
"month", |
|
"quarter", |
|
"year", |
|
] |
|
|
|
|
|
class Arrow: |
|
"""An :class:`Arrow <arrow.arrow.Arrow>` object. |
|
|
|
Implements the ``datetime`` interface, behaving as an aware ``datetime`` while implementing |
|
additional functionality. |
|
|
|
:param year: the calendar year. |
|
:param month: the calendar month. |
|
:param day: the calendar day. |
|
:param hour: (optional) the hour. Defaults to 0. |
|
:param minute: (optional) the minute, Defaults to 0. |
|
:param second: (optional) the second, Defaults to 0. |
|
:param microsecond: (optional) the microsecond. Defaults to 0. |
|
:param tzinfo: (optional) A timezone expression. Defaults to UTC. |
|
:param fold: (optional) 0 or 1, used to disambiguate repeated wall times. Defaults to 0. |
|
|
|
.. _tz-expr: |
|
|
|
Recognized timezone expressions: |
|
|
|
- A ``tzinfo`` object. |
|
- A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. |
|
- A ``str`` in ISO 8601 style, as in '+07:00'. |
|
- A ``str``, one of the following: 'local', 'utc', 'UTC'. |
|
|
|
Usage:: |
|
|
|
>>> import arrow |
|
>>> arrow.Arrow(2013, 5, 5, 12, 30, 45) |
|
<Arrow [2013-05-05T12:30:45+00:00]> |
|
|
|
""" |
|
|
|
resolution: ClassVar[timedelta] = dt_datetime.resolution |
|
min: ClassVar["Arrow"] |
|
max: ClassVar["Arrow"] |
|
|
|
_ATTRS: Final[List[str]] = [ |
|
"year", |
|
"month", |
|
"day", |
|
"hour", |
|
"minute", |
|
"second", |
|
"microsecond", |
|
] |
|
_ATTRS_PLURAL: Final[List[str]] = [f"{a}s" for a in _ATTRS] |
|
_MONTHS_PER_QUARTER: Final[int] = 3 |
|
_SECS_PER_MINUTE: Final[int] = 60 |
|
_SECS_PER_HOUR: Final[int] = 60 * 60 |
|
_SECS_PER_DAY: Final[int] = 60 * 60 * 24 |
|
_SECS_PER_WEEK: Final[int] = 60 * 60 * 24 * 7 |
|
_SECS_PER_MONTH: Final[float] = 60 * 60 * 24 * 30.5 |
|
_SECS_PER_QUARTER: Final[float] = 60 * 60 * 24 * 30.5 * 3 |
|
_SECS_PER_YEAR: Final[int] = 60 * 60 * 24 * 365 |
|
|
|
_SECS_MAP: Final[Mapping[TimeFrameLiteral, float]] = { |
|
"second": 1.0, |
|
"minute": _SECS_PER_MINUTE, |
|
"hour": _SECS_PER_HOUR, |
|
"day": _SECS_PER_DAY, |
|
"week": _SECS_PER_WEEK, |
|
"month": _SECS_PER_MONTH, |
|
"quarter": _SECS_PER_QUARTER, |
|
"year": _SECS_PER_YEAR, |
|
} |
|
|
|
_datetime: dt_datetime |
|
|
|
def __init__( |
|
self, |
|
year: int, |
|
month: int, |
|
day: int, |
|
hour: int = 0, |
|
minute: int = 0, |
|
second: int = 0, |
|
microsecond: int = 0, |
|
tzinfo: Optional[TZ_EXPR] = None, |
|
**kwargs: Any, |
|
) -> None: |
|
if tzinfo is None: |
|
tzinfo = dateutil_tz.tzutc() |
|
|
|
elif ( |
|
isinstance(tzinfo, dt_tzinfo) |
|
and hasattr(tzinfo, "localize") |
|
and hasattr(tzinfo, "zone") |
|
and tzinfo.zone |
|
): |
|
tzinfo = parser.TzinfoParser.parse(tzinfo.zone) |
|
elif isinstance(tzinfo, str): |
|
tzinfo = parser.TzinfoParser.parse(tzinfo) |
|
|
|
fold = kwargs.get("fold", 0) |
|
|
|
self._datetime = dt_datetime( |
|
year, month, day, hour, minute, second, microsecond, tzinfo, fold=fold |
|
) |
|
|
|
|
|
|
|
@classmethod |
|
def now(cls, tzinfo: Optional[dt_tzinfo] = None) -> "Arrow": |
|
"""Constructs an :class:`Arrow <arrow.arrow.Arrow>` object, representing "now" in the given |
|
timezone. |
|
|
|
:param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.now('Asia/Baku') |
|
<Arrow [2019-01-24T20:26:31.146412+04:00]> |
|
|
|
""" |
|
|
|
if tzinfo is None: |
|
tzinfo = dateutil_tz.tzlocal() |
|
|
|
dt = dt_datetime.now(tzinfo) |
|
|
|
return cls( |
|
dt.year, |
|
dt.month, |
|
dt.day, |
|
dt.hour, |
|
dt.minute, |
|
dt.second, |
|
dt.microsecond, |
|
dt.tzinfo, |
|
fold=getattr(dt, "fold", 0), |
|
) |
|
|
|
@classmethod |
|
def utcnow(cls) -> "Arrow": |
|
"""Constructs an :class:`Arrow <arrow.arrow.Arrow>` object, representing "now" in UTC |
|
time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow() |
|
<Arrow [2019-01-24T16:31:40.651108+00:00]> |
|
|
|
""" |
|
|
|
dt = dt_datetime.now(dateutil_tz.tzutc()) |
|
|
|
return cls( |
|
dt.year, |
|
dt.month, |
|
dt.day, |
|
dt.hour, |
|
dt.minute, |
|
dt.second, |
|
dt.microsecond, |
|
dt.tzinfo, |
|
fold=getattr(dt, "fold", 0), |
|
) |
|
|
|
@classmethod |
|
def fromtimestamp( |
|
cls, |
|
timestamp: Union[int, float, str], |
|
tzinfo: Optional[TZ_EXPR] = None, |
|
) -> "Arrow": |
|
"""Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a timestamp, converted to |
|
the given timezone. |
|
|
|
:param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. |
|
:param tzinfo: (optional) a ``tzinfo`` object. Defaults to local time. |
|
|
|
""" |
|
|
|
if tzinfo is None: |
|
tzinfo = dateutil_tz.tzlocal() |
|
elif isinstance(tzinfo, str): |
|
tzinfo = parser.TzinfoParser.parse(tzinfo) |
|
|
|
if not util.is_timestamp(timestamp): |
|
raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") |
|
|
|
timestamp = util.normalize_timestamp(float(timestamp)) |
|
dt = dt_datetime.fromtimestamp(timestamp, tzinfo) |
|
|
|
return cls( |
|
dt.year, |
|
dt.month, |
|
dt.day, |
|
dt.hour, |
|
dt.minute, |
|
dt.second, |
|
dt.microsecond, |
|
dt.tzinfo, |
|
fold=getattr(dt, "fold", 0), |
|
) |
|
|
|
@classmethod |
|
def utcfromtimestamp(cls, timestamp: Union[int, float, str]) -> "Arrow": |
|
"""Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a timestamp, in UTC time. |
|
|
|
:param timestamp: an ``int`` or ``float`` timestamp, or a ``str`` that converts to either. |
|
|
|
""" |
|
|
|
if not util.is_timestamp(timestamp): |
|
raise ValueError(f"The provided timestamp {timestamp!r} is invalid.") |
|
|
|
timestamp = util.normalize_timestamp(float(timestamp)) |
|
dt = dt_datetime.utcfromtimestamp(timestamp) |
|
|
|
return cls( |
|
dt.year, |
|
dt.month, |
|
dt.day, |
|
dt.hour, |
|
dt.minute, |
|
dt.second, |
|
dt.microsecond, |
|
dateutil_tz.tzutc(), |
|
fold=getattr(dt, "fold", 0), |
|
) |
|
|
|
@classmethod |
|
def fromdatetime(cls, dt: dt_datetime, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": |
|
"""Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a ``datetime`` and |
|
optional replacement timezone. |
|
|
|
:param dt: the ``datetime`` |
|
:param tzinfo: (optional) A :ref:`timezone expression <tz-expr>`. Defaults to ``dt``'s |
|
timezone, or UTC if naive. |
|
|
|
Usage:: |
|
|
|
>>> dt |
|
datetime.datetime(2021, 4, 7, 13, 48, tzinfo=tzfile('/usr/share/zoneinfo/US/Pacific')) |
|
>>> arrow.Arrow.fromdatetime(dt) |
|
<Arrow [2021-04-07T13:48:00-07:00]> |
|
|
|
""" |
|
|
|
if tzinfo is None: |
|
if dt.tzinfo is None: |
|
tzinfo = dateutil_tz.tzutc() |
|
else: |
|
tzinfo = dt.tzinfo |
|
|
|
return cls( |
|
dt.year, |
|
dt.month, |
|
dt.day, |
|
dt.hour, |
|
dt.minute, |
|
dt.second, |
|
dt.microsecond, |
|
tzinfo, |
|
fold=getattr(dt, "fold", 0), |
|
) |
|
|
|
@classmethod |
|
def fromdate(cls, date: date, tzinfo: Optional[TZ_EXPR] = None) -> "Arrow": |
|
"""Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a ``date`` and optional |
|
replacement timezone. All time values are set to 0. |
|
|
|
:param date: the ``date`` |
|
:param tzinfo: (optional) A :ref:`timezone expression <tz-expr>`. Defaults to UTC. |
|
|
|
""" |
|
|
|
if tzinfo is None: |
|
tzinfo = dateutil_tz.tzutc() |
|
|
|
return cls(date.year, date.month, date.day, tzinfo=tzinfo) |
|
|
|
@classmethod |
|
def strptime( |
|
cls, date_str: str, fmt: str, tzinfo: Optional[TZ_EXPR] = None |
|
) -> "Arrow": |
|
"""Constructs an :class:`Arrow <arrow.arrow.Arrow>` object from a date string and format, |
|
in the style of ``datetime.strptime``. Optionally replaces the parsed timezone. |
|
|
|
:param date_str: the date string. |
|
:param fmt: the format string using datetime format codes. |
|
:param tzinfo: (optional) A :ref:`timezone expression <tz-expr>`. Defaults to the parsed |
|
timezone if ``fmt`` contains a timezone directive, otherwise UTC. |
|
|
|
Usage:: |
|
|
|
>>> arrow.Arrow.strptime('20-01-2019 15:49:10', '%d-%m-%Y %H:%M:%S') |
|
<Arrow [2019-01-20T15:49:10+00:00]> |
|
|
|
""" |
|
|
|
dt = dt_datetime.strptime(date_str, fmt) |
|
if tzinfo is None: |
|
tzinfo = dt.tzinfo |
|
|
|
return cls( |
|
dt.year, |
|
dt.month, |
|
dt.day, |
|
dt.hour, |
|
dt.minute, |
|
dt.second, |
|
dt.microsecond, |
|
tzinfo, |
|
fold=getattr(dt, "fold", 0), |
|
) |
|
|
|
@classmethod |
|
def fromordinal(cls, ordinal: int) -> "Arrow": |
|
"""Constructs an :class:`Arrow <arrow.arrow.Arrow>` object corresponding |
|
to the Gregorian Ordinal. |
|
|
|
:param ordinal: an ``int`` corresponding to a Gregorian Ordinal. |
|
|
|
Usage:: |
|
|
|
>>> arrow.fromordinal(737741) |
|
<Arrow [2020-11-12T00:00:00+00:00]> |
|
|
|
""" |
|
|
|
util.validate_ordinal(ordinal) |
|
dt = dt_datetime.fromordinal(ordinal) |
|
return cls( |
|
dt.year, |
|
dt.month, |
|
dt.day, |
|
dt.hour, |
|
dt.minute, |
|
dt.second, |
|
dt.microsecond, |
|
dt.tzinfo, |
|
fold=getattr(dt, "fold", 0), |
|
) |
|
|
|
|
|
|
|
@classmethod |
|
def range( |
|
cls, |
|
frame: _T_FRAMES, |
|
start: Union["Arrow", dt_datetime], |
|
end: Union["Arrow", dt_datetime, None] = None, |
|
tz: Optional[TZ_EXPR] = None, |
|
limit: Optional[int] = None, |
|
) -> Generator["Arrow", None, None]: |
|
"""Returns an iterator of :class:`Arrow <arrow.arrow.Arrow>` objects, representing |
|
points in time between two inputs. |
|
|
|
:param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). |
|
:param start: A datetime expression, the start of the range. |
|
:param end: (optional) A datetime expression, the end of the range. |
|
:param tz: (optional) A :ref:`timezone expression <tz-expr>`. Defaults to |
|
``start``'s timezone, or UTC if ``start`` is naive. |
|
:param limit: (optional) A maximum number of tuples to return. |
|
|
|
**NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to |
|
return the entire range. Call with ``limit`` alone to return a maximum # of results from |
|
the start. Call with both to cap a range at a maximum # of results. |
|
|
|
**NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before |
|
iterating. As such, either call with naive objects and ``tz``, or aware objects from the |
|
same timezone and no ``tz``. |
|
|
|
Supported frame values: year, quarter, month, week, day, hour, minute, second, microsecond. |
|
|
|
Recognized datetime expressions: |
|
|
|
- An :class:`Arrow <arrow.arrow.Arrow>` object. |
|
- A ``datetime`` object. |
|
|
|
Usage:: |
|
|
|
>>> start = datetime(2013, 5, 5, 12, 30) |
|
>>> end = datetime(2013, 5, 5, 17, 15) |
|
>>> for r in arrow.Arrow.range('hour', start, end): |
|
... print(repr(r)) |
|
... |
|
<Arrow [2013-05-05T12:30:00+00:00]> |
|
<Arrow [2013-05-05T13:30:00+00:00]> |
|
<Arrow [2013-05-05T14:30:00+00:00]> |
|
<Arrow [2013-05-05T15:30:00+00:00]> |
|
<Arrow [2013-05-05T16:30:00+00:00]> |
|
|
|
**NOTE**: Unlike Python's ``range``, ``end`` *may* be included in the returned iterator:: |
|
|
|
>>> start = datetime(2013, 5, 5, 12, 30) |
|
>>> end = datetime(2013, 5, 5, 13, 30) |
|
>>> for r in arrow.Arrow.range('hour', start, end): |
|
... print(repr(r)) |
|
... |
|
<Arrow [2013-05-05T12:30:00+00:00]> |
|
<Arrow [2013-05-05T13:30:00+00:00]> |
|
|
|
""" |
|
|
|
_, frame_relative, relative_steps = cls._get_frames(frame) |
|
|
|
tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) |
|
|
|
start = cls._get_datetime(start).replace(tzinfo=tzinfo) |
|
end, limit = cls._get_iteration_params(end, limit) |
|
end = cls._get_datetime(end).replace(tzinfo=tzinfo) |
|
|
|
current = cls.fromdatetime(start) |
|
original_day = start.day |
|
day_is_clipped = False |
|
i = 0 |
|
|
|
while current <= end and i < limit: |
|
i += 1 |
|
yield current |
|
|
|
values = [getattr(current, f) for f in cls._ATTRS] |
|
current = cls(*values, tzinfo=tzinfo).shift( |
|
**{frame_relative: relative_steps} |
|
) |
|
|
|
if frame in ["month", "quarter", "year"] and current.day < original_day: |
|
day_is_clipped = True |
|
|
|
if day_is_clipped and not cls._is_last_day_of_month(current): |
|
current = current.replace(day=original_day) |
|
|
|
def span( |
|
self, |
|
frame: _T_FRAMES, |
|
count: int = 1, |
|
bounds: _BOUNDS = "[)", |
|
exact: bool = False, |
|
week_start: int = 1, |
|
) -> Tuple["Arrow", "Arrow"]: |
|
"""Returns a tuple of two new :class:`Arrow <arrow.arrow.Arrow>` objects, representing the timespan |
|
of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe. |
|
|
|
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). |
|
:param count: (optional) the number of frames to span. |
|
:param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies |
|
whether to include or exclude the start and end values in the span. '(' excludes |
|
the start, '[' includes the start, ')' excludes the end, and ']' includes the end. |
|
If the bounds are not specified, the default bound '[)' is used. |
|
:param exact: (optional) whether to have the start of the timespan begin exactly |
|
at the time specified by ``start`` and the end of the timespan truncated |
|
so as not to extend beyond ``end``. |
|
:param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where |
|
Monday is 1 and Sunday is 7. |
|
|
|
Supported frame values: year, quarter, month, week, day, hour, minute, second. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow() |
|
<Arrow [2013-05-09T03:32:36.186203+00:00]> |
|
|
|
>>> arrow.utcnow().span('hour') |
|
(<Arrow [2013-05-09T03:00:00+00:00]>, <Arrow [2013-05-09T03:59:59.999999+00:00]>) |
|
|
|
>>> arrow.utcnow().span('day') |
|
(<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-09T23:59:59.999999+00:00]>) |
|
|
|
>>> arrow.utcnow().span('day', count=2) |
|
(<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-10T23:59:59.999999+00:00]>) |
|
|
|
>>> arrow.utcnow().span('day', bounds='[]') |
|
(<Arrow [2013-05-09T00:00:00+00:00]>, <Arrow [2013-05-10T00:00:00+00:00]>) |
|
|
|
>>> arrow.utcnow().span('week') |
|
(<Arrow [2021-02-22T00:00:00+00:00]>, <Arrow [2021-02-28T23:59:59.999999+00:00]>) |
|
|
|
>>> arrow.utcnow().span('week', week_start=6) |
|
(<Arrow [2021-02-20T00:00:00+00:00]>, <Arrow [2021-02-26T23:59:59.999999+00:00]>) |
|
|
|
""" |
|
if not 1 <= week_start <= 7: |
|
raise ValueError("week_start argument must be between 1 and 7.") |
|
|
|
util.validate_bounds(bounds) |
|
|
|
frame_absolute, frame_relative, relative_steps = self._get_frames(frame) |
|
|
|
if frame_absolute == "week": |
|
attr = "day" |
|
elif frame_absolute == "quarter": |
|
attr = "month" |
|
else: |
|
attr = frame_absolute |
|
|
|
floor = self |
|
if not exact: |
|
index = self._ATTRS.index(attr) |
|
frames = self._ATTRS[: index + 1] |
|
|
|
values = [getattr(self, f) for f in frames] |
|
|
|
for _ in range(3 - len(values)): |
|
values.append(1) |
|
|
|
floor = self.__class__(*values, tzinfo=self.tzinfo) |
|
|
|
if frame_absolute == "week": |
|
|
|
delta = 7 if week_start > self.isoweekday() else 0 |
|
floor = floor.shift(days=-(self.isoweekday() - week_start) - delta) |
|
elif frame_absolute == "quarter": |
|
floor = floor.shift(months=-((self.month - 1) % 3)) |
|
|
|
ceil = floor.shift(**{frame_relative: count * relative_steps}) |
|
|
|
if bounds[0] == "(": |
|
floor = floor.shift(microseconds=+1) |
|
|
|
if bounds[1] == ")": |
|
ceil = ceil.shift(microseconds=-1) |
|
|
|
return floor, ceil |
|
|
|
def floor(self, frame: _T_FRAMES) -> "Arrow": |
|
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the "floor" |
|
of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe. |
|
Equivalent to the first element in the 2-tuple returned by |
|
:func:`span <arrow.arrow.Arrow.span>`. |
|
|
|
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().floor('hour') |
|
<Arrow [2013-05-09T03:00:00+00:00]> |
|
|
|
""" |
|
|
|
return self.span(frame)[0] |
|
|
|
def ceil(self, frame: _T_FRAMES) -> "Arrow": |
|
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the "ceiling" |
|
of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe. |
|
Equivalent to the second element in the 2-tuple returned by |
|
:func:`span <arrow.arrow.Arrow.span>`. |
|
|
|
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...). |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().ceil('hour') |
|
<Arrow [2013-05-09T03:59:59.999999+00:00]> |
|
|
|
""" |
|
|
|
return self.span(frame)[1] |
|
|
|
@classmethod |
|
def span_range( |
|
cls, |
|
frame: _T_FRAMES, |
|
start: dt_datetime, |
|
end: dt_datetime, |
|
tz: Optional[TZ_EXPR] = None, |
|
limit: Optional[int] = None, |
|
bounds: _BOUNDS = "[)", |
|
exact: bool = False, |
|
) -> Iterable[Tuple["Arrow", "Arrow"]]: |
|
"""Returns an iterator of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects, |
|
representing a series of timespans between two inputs. |
|
|
|
:param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). |
|
:param start: A datetime expression, the start of the range. |
|
:param end: (optional) A datetime expression, the end of the range. |
|
:param tz: (optional) A :ref:`timezone expression <tz-expr>`. Defaults to |
|
``start``'s timezone, or UTC if ``start`` is naive. |
|
:param limit: (optional) A maximum number of tuples to return. |
|
:param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies |
|
whether to include or exclude the start and end values in each span in the range. '(' excludes |
|
the start, '[' includes the start, ')' excludes the end, and ']' includes the end. |
|
If the bounds are not specified, the default bound '[)' is used. |
|
:param exact: (optional) whether to have the first timespan start exactly |
|
at the time specified by ``start`` and the final span truncated |
|
so as not to extend beyond ``end``. |
|
|
|
**NOTE**: The ``end`` or ``limit`` must be provided. Call with ``end`` alone to |
|
return the entire range. Call with ``limit`` alone to return a maximum # of results from |
|
the start. Call with both to cap a range at a maximum # of results. |
|
|
|
**NOTE**: ``tz`` internally **replaces** the timezones of both ``start`` and ``end`` before |
|
iterating. As such, either call with naive objects and ``tz``, or aware objects from the |
|
same timezone and no ``tz``. |
|
|
|
Supported frame values: year, quarter, month, week, day, hour, minute, second, microsecond. |
|
|
|
Recognized datetime expressions: |
|
|
|
- An :class:`Arrow <arrow.arrow.Arrow>` object. |
|
- A ``datetime`` object. |
|
|
|
**NOTE**: Unlike Python's ``range``, ``end`` will *always* be included in the returned |
|
iterator of timespans. |
|
|
|
Usage: |
|
|
|
>>> start = datetime(2013, 5, 5, 12, 30) |
|
>>> end = datetime(2013, 5, 5, 17, 15) |
|
>>> for r in arrow.Arrow.span_range('hour', start, end): |
|
... print(r) |
|
... |
|
(<Arrow [2013-05-05T12:00:00+00:00]>, <Arrow [2013-05-05T12:59:59.999999+00:00]>) |
|
(<Arrow [2013-05-05T13:00:00+00:00]>, <Arrow [2013-05-05T13:59:59.999999+00:00]>) |
|
(<Arrow [2013-05-05T14:00:00+00:00]>, <Arrow [2013-05-05T14:59:59.999999+00:00]>) |
|
(<Arrow [2013-05-05T15:00:00+00:00]>, <Arrow [2013-05-05T15:59:59.999999+00:00]>) |
|
(<Arrow [2013-05-05T16:00:00+00:00]>, <Arrow [2013-05-05T16:59:59.999999+00:00]>) |
|
(<Arrow [2013-05-05T17:00:00+00:00]>, <Arrow [2013-05-05T17:59:59.999999+00:00]>) |
|
|
|
""" |
|
|
|
tzinfo = cls._get_tzinfo(start.tzinfo if tz is None else tz) |
|
start = cls.fromdatetime(start, tzinfo).span(frame, exact=exact)[0] |
|
end = cls.fromdatetime(end, tzinfo) |
|
_range = cls.range(frame, start, end, tz, limit) |
|
if not exact: |
|
for r in _range: |
|
yield r.span(frame, bounds=bounds, exact=exact) |
|
|
|
for r in _range: |
|
floor, ceil = r.span(frame, bounds=bounds, exact=exact) |
|
if ceil > end: |
|
ceil = end |
|
if bounds[1] == ")": |
|
ceil += relativedelta(microseconds=-1) |
|
if floor == end: |
|
break |
|
elif floor + relativedelta(microseconds=-1) == end: |
|
break |
|
yield floor, ceil |
|
|
|
@classmethod |
|
def interval( |
|
cls, |
|
frame: _T_FRAMES, |
|
start: dt_datetime, |
|
end: dt_datetime, |
|
interval: int = 1, |
|
tz: Optional[TZ_EXPR] = None, |
|
bounds: _BOUNDS = "[)", |
|
exact: bool = False, |
|
) -> Iterable[Tuple["Arrow", "Arrow"]]: |
|
"""Returns an iterator of tuples, each :class:`Arrow <arrow.arrow.Arrow>` objects, |
|
representing a series of intervals between two inputs. |
|
|
|
:param frame: The timeframe. Can be any ``datetime`` property (day, hour, minute...). |
|
:param start: A datetime expression, the start of the range. |
|
:param end: (optional) A datetime expression, the end of the range. |
|
:param interval: (optional) Time interval for the given time frame. |
|
:param tz: (optional) A timezone expression. Defaults to UTC. |
|
:param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies |
|
whether to include or exclude the start and end values in the intervals. '(' excludes |
|
the start, '[' includes the start, ')' excludes the end, and ']' includes the end. |
|
If the bounds are not specified, the default bound '[)' is used. |
|
:param exact: (optional) whether to have the first timespan start exactly |
|
at the time specified by ``start`` and the final interval truncated |
|
so as not to extend beyond ``end``. |
|
|
|
Supported frame values: year, quarter, month, week, day, hour, minute, second |
|
|
|
Recognized datetime expressions: |
|
|
|
- An :class:`Arrow <arrow.arrow.Arrow>` object. |
|
- A ``datetime`` object. |
|
|
|
Recognized timezone expressions: |
|
|
|
- A ``tzinfo`` object. |
|
- A ``str`` describing a timezone, similar to 'US/Pacific', or 'Europe/Berlin'. |
|
- A ``str`` in ISO 8601 style, as in '+07:00'. |
|
- A ``str``, one of the following: 'local', 'utc', 'UTC'. |
|
|
|
Usage: |
|
|
|
>>> start = datetime(2013, 5, 5, 12, 30) |
|
>>> end = datetime(2013, 5, 5, 17, 15) |
|
>>> for r in arrow.Arrow.interval('hour', start, end, 2): |
|
... print(r) |
|
... |
|
(<Arrow [2013-05-05T12:00:00+00:00]>, <Arrow [2013-05-05T13:59:59.999999+00:00]>) |
|
(<Arrow [2013-05-05T14:00:00+00:00]>, <Arrow [2013-05-05T15:59:59.999999+00:00]>) |
|
(<Arrow [2013-05-05T16:00:00+00:00]>, <Arrow [2013-05-05T17:59:59.999999+00:0]>) |
|
""" |
|
if interval < 1: |
|
raise ValueError("interval has to be a positive integer") |
|
|
|
spanRange = iter( |
|
cls.span_range(frame, start, end, tz, bounds=bounds, exact=exact) |
|
) |
|
while True: |
|
try: |
|
intvlStart, intvlEnd = next(spanRange) |
|
for _ in range(interval - 1): |
|
try: |
|
_, intvlEnd = next(spanRange) |
|
except StopIteration: |
|
continue |
|
yield intvlStart, intvlEnd |
|
except StopIteration: |
|
return |
|
|
|
|
|
|
|
def __repr__(self) -> str: |
|
return f"<{self.__class__.__name__} [{self.__str__()}]>" |
|
|
|
def __str__(self) -> str: |
|
return self._datetime.isoformat() |
|
|
|
def __format__(self, formatstr: str) -> str: |
|
if len(formatstr) > 0: |
|
return self.format(formatstr) |
|
|
|
return str(self) |
|
|
|
def __hash__(self) -> int: |
|
return self._datetime.__hash__() |
|
|
|
|
|
|
|
def __getattr__(self, name: str) -> int: |
|
if name == "week": |
|
return self.isocalendar()[1] |
|
|
|
if name == "quarter": |
|
return int((self.month - 1) / self._MONTHS_PER_QUARTER) + 1 |
|
|
|
if not name.startswith("_"): |
|
value: Optional[int] = getattr(self._datetime, name, None) |
|
|
|
if value is not None: |
|
return value |
|
|
|
return cast(int, object.__getattribute__(self, name)) |
|
|
|
@property |
|
def tzinfo(self) -> dt_tzinfo: |
|
"""Gets the ``tzinfo`` of the :class:`Arrow <arrow.arrow.Arrow>` object. |
|
|
|
Usage:: |
|
|
|
>>> arw=arrow.utcnow() |
|
>>> arw.tzinfo |
|
tzutc() |
|
|
|
""" |
|
|
|
|
|
return cast(dt_tzinfo, self._datetime.tzinfo) |
|
|
|
@property |
|
def datetime(self) -> dt_datetime: |
|
"""Returns a datetime representation of the :class:`Arrow <arrow.arrow.Arrow>` object. |
|
|
|
Usage:: |
|
|
|
>>> arw=arrow.utcnow() |
|
>>> arw.datetime |
|
datetime.datetime(2019, 1, 24, 16, 35, 27, 276649, tzinfo=tzutc()) |
|
|
|
""" |
|
|
|
return self._datetime |
|
|
|
@property |
|
def naive(self) -> dt_datetime: |
|
"""Returns a naive datetime representation of the :class:`Arrow <arrow.arrow.Arrow>` |
|
object. |
|
|
|
Usage:: |
|
|
|
>>> nairobi = arrow.now('Africa/Nairobi') |
|
>>> nairobi |
|
<Arrow [2019-01-23T19:27:12.297999+03:00]> |
|
>>> nairobi.naive |
|
datetime.datetime(2019, 1, 23, 19, 27, 12, 297999) |
|
|
|
""" |
|
|
|
return self._datetime.replace(tzinfo=None) |
|
|
|
def timestamp(self) -> float: |
|
"""Returns a timestamp representation of the :class:`Arrow <arrow.arrow.Arrow>` object, in |
|
UTC time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().timestamp() |
|
1616882340.256501 |
|
|
|
""" |
|
|
|
return self._datetime.timestamp() |
|
|
|
@property |
|
def int_timestamp(self) -> int: |
|
"""Returns an integer timestamp representation of the :class:`Arrow <arrow.arrow.Arrow>` object, in |
|
UTC time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().int_timestamp |
|
1548260567 |
|
|
|
""" |
|
|
|
return int(self.timestamp()) |
|
|
|
@property |
|
def float_timestamp(self) -> float: |
|
"""Returns a floating-point timestamp representation of the :class:`Arrow <arrow.arrow.Arrow>` |
|
object, in UTC time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().float_timestamp |
|
1548260516.830896 |
|
|
|
""" |
|
|
|
return self.timestamp() |
|
|
|
@property |
|
def fold(self) -> int: |
|
"""Returns the ``fold`` value of the :class:`Arrow <arrow.arrow.Arrow>` object.""" |
|
|
|
return self._datetime.fold |
|
|
|
@property |
|
def ambiguous(self) -> bool: |
|
"""Indicates whether the :class:`Arrow <arrow.arrow.Arrow>` object is a repeated wall time in the current |
|
timezone. |
|
|
|
""" |
|
|
|
return dateutil_tz.datetime_ambiguous(self._datetime) |
|
|
|
@property |
|
def imaginary(self) -> bool: |
|
"""Indicates whether the :class: `Arrow <arrow.arrow.Arrow>` object exists in the current timezone.""" |
|
|
|
return not dateutil_tz.datetime_exists(self._datetime) |
|
|
|
|
|
|
|
def clone(self) -> "Arrow": |
|
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, cloned from the current one. |
|
|
|
Usage: |
|
|
|
>>> arw = arrow.utcnow() |
|
>>> cloned = arw.clone() |
|
|
|
""" |
|
|
|
return self.fromdatetime(self._datetime) |
|
|
|
def replace(self, **kwargs: Any) -> "Arrow": |
|
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object with attributes updated |
|
according to inputs. |
|
|
|
Use property names to set their value absolutely:: |
|
|
|
>>> import arrow |
|
>>> arw = arrow.utcnow() |
|
>>> arw |
|
<Arrow [2013-05-11T22:27:34.787885+00:00]> |
|
>>> arw.replace(year=2014, month=6) |
|
<Arrow [2014-06-11T22:27:34.787885+00:00]> |
|
|
|
You can also replace the timezone without conversion, using a |
|
:ref:`timezone expression <tz-expr>`:: |
|
|
|
>>> arw.replace(tzinfo=tz.tzlocal()) |
|
<Arrow [2013-05-11T22:27:34.787885-07:00]> |
|
|
|
""" |
|
|
|
absolute_kwargs = {} |
|
|
|
for key, value in kwargs.items(): |
|
if key in self._ATTRS: |
|
absolute_kwargs[key] = value |
|
elif key in ["week", "quarter"]: |
|
raise ValueError(f"Setting absolute {key} is not supported.") |
|
elif key not in ["tzinfo", "fold"]: |
|
raise ValueError(f"Unknown attribute: {key!r}.") |
|
|
|
current = self._datetime.replace(**absolute_kwargs) |
|
|
|
tzinfo = kwargs.get("tzinfo") |
|
|
|
if tzinfo is not None: |
|
tzinfo = self._get_tzinfo(tzinfo) |
|
current = current.replace(tzinfo=tzinfo) |
|
|
|
fold = kwargs.get("fold") |
|
|
|
if fold is not None: |
|
current = current.replace(fold=fold) |
|
|
|
return self.fromdatetime(current) |
|
|
|
def shift(self, **kwargs: Any) -> "Arrow": |
|
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object with attributes updated |
|
according to inputs. |
|
|
|
Use pluralized property names to relatively shift their current value: |
|
|
|
>>> import arrow |
|
>>> arw = arrow.utcnow() |
|
>>> arw |
|
<Arrow [2013-05-11T22:27:34.787885+00:00]> |
|
>>> arw.shift(years=1, months=-1) |
|
<Arrow [2014-04-11T22:27:34.787885+00:00]> |
|
|
|
Day-of-the-week relative shifting can use either Python's weekday numbers |
|
(Monday = 0, Tuesday = 1 .. Sunday = 6) or using dateutil.relativedelta's |
|
day instances (MO, TU .. SU). When using weekday numbers, the returned |
|
date will always be greater than or equal to the starting date. |
|
|
|
Using the above code (which is a Saturday) and asking it to shift to Saturday: |
|
|
|
>>> arw.shift(weekday=5) |
|
<Arrow [2013-05-11T22:27:34.787885+00:00]> |
|
|
|
While asking for a Monday: |
|
|
|
>>> arw.shift(weekday=0) |
|
<Arrow [2013-05-13T22:27:34.787885+00:00]> |
|
|
|
""" |
|
|
|
relative_kwargs = {} |
|
additional_attrs = ["weeks", "quarters", "weekday"] |
|
|
|
for key, value in kwargs.items(): |
|
if key in self._ATTRS_PLURAL or key in additional_attrs: |
|
relative_kwargs[key] = value |
|
else: |
|
supported_attr = ", ".join(self._ATTRS_PLURAL + additional_attrs) |
|
raise ValueError( |
|
f"Invalid shift time frame. Please select one of the following: {supported_attr}." |
|
) |
|
|
|
|
|
relative_kwargs.setdefault("months", 0) |
|
relative_kwargs["months"] += ( |
|
relative_kwargs.pop("quarters", 0) * self._MONTHS_PER_QUARTER |
|
) |
|
|
|
current = self._datetime + relativedelta(**relative_kwargs) |
|
|
|
if not dateutil_tz.datetime_exists(current): |
|
current = dateutil_tz.resolve_imaginary(current) |
|
|
|
return self.fromdatetime(current) |
|
|
|
def to(self, tz: TZ_EXPR) -> "Arrow": |
|
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, converted |
|
to the target timezone. |
|
|
|
:param tz: A :ref:`timezone expression <tz-expr>`. |
|
|
|
Usage:: |
|
|
|
>>> utc = arrow.utcnow() |
|
>>> utc |
|
<Arrow [2013-05-09T03:49:12.311072+00:00]> |
|
|
|
>>> utc.to('US/Pacific') |
|
<Arrow [2013-05-08T20:49:12.311072-07:00]> |
|
|
|
>>> utc.to(tz.tzlocal()) |
|
<Arrow [2013-05-08T20:49:12.311072-07:00]> |
|
|
|
>>> utc.to('-07:00') |
|
<Arrow [2013-05-08T20:49:12.311072-07:00]> |
|
|
|
>>> utc.to('local') |
|
<Arrow [2013-05-08T20:49:12.311072-07:00]> |
|
|
|
>>> utc.to('local').to('utc') |
|
<Arrow [2013-05-09T03:49:12.311072+00:00]> |
|
|
|
""" |
|
|
|
if not isinstance(tz, dt_tzinfo): |
|
tz = parser.TzinfoParser.parse(tz) |
|
|
|
dt = self._datetime.astimezone(tz) |
|
|
|
return self.__class__( |
|
dt.year, |
|
dt.month, |
|
dt.day, |
|
dt.hour, |
|
dt.minute, |
|
dt.second, |
|
dt.microsecond, |
|
dt.tzinfo, |
|
fold=getattr(dt, "fold", 0), |
|
) |
|
|
|
|
|
|
|
def format( |
|
self, fmt: str = "YYYY-MM-DD HH:mm:ssZZ", locale: str = DEFAULT_LOCALE |
|
) -> str: |
|
"""Returns a string representation of the :class:`Arrow <arrow.arrow.Arrow>` object, |
|
formatted according to the provided format string. |
|
|
|
:param fmt: the format string. |
|
:param locale: the locale to format. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().format('YYYY-MM-DD HH:mm:ss ZZ') |
|
'2013-05-09 03:56:47 -00:00' |
|
|
|
>>> arrow.utcnow().format('X') |
|
'1368071882' |
|
|
|
>>> arrow.utcnow().format('MMMM DD, YYYY') |
|
'May 09, 2013' |
|
|
|
>>> arrow.utcnow().format() |
|
'2013-05-09 03:56:47 -00:00' |
|
|
|
""" |
|
|
|
return formatter.DateTimeFormatter(locale).format(self._datetime, fmt) |
|
|
|
def humanize( |
|
self, |
|
other: Union["Arrow", dt_datetime, None] = None, |
|
locale: str = DEFAULT_LOCALE, |
|
only_distance: bool = False, |
|
granularity: Union[_GRANULARITY, List[_GRANULARITY]] = "auto", |
|
) -> str: |
|
"""Returns a localized, humanized representation of a relative difference in time. |
|
|
|
:param other: (optional) an :class:`Arrow <arrow.arrow.Arrow>` or ``datetime`` object. |
|
Defaults to now in the current :class:`Arrow <arrow.arrow.Arrow>` object's timezone. |
|
:param locale: (optional) a ``str`` specifying a locale. Defaults to 'en-us'. |
|
:param only_distance: (optional) returns only time difference eg: "11 seconds" without "in" or "ago" part. |
|
:param granularity: (optional) defines the precision of the output. Set it to strings 'second', 'minute', |
|
'hour', 'day', 'week', 'month' or 'year' or a list of any combination of these strings |
|
|
|
Usage:: |
|
|
|
>>> earlier = arrow.utcnow().shift(hours=-2) |
|
>>> earlier.humanize() |
|
'2 hours ago' |
|
|
|
>>> later = earlier.shift(hours=4) |
|
>>> later.humanize(earlier) |
|
'in 4 hours' |
|
|
|
""" |
|
|
|
locale_name = locale |
|
locale = locales.get_locale(locale) |
|
|
|
if other is None: |
|
utc = dt_datetime.utcnow().replace(tzinfo=dateutil_tz.tzutc()) |
|
dt = utc.astimezone(self._datetime.tzinfo) |
|
|
|
elif isinstance(other, Arrow): |
|
dt = other._datetime |
|
|
|
elif isinstance(other, dt_datetime): |
|
if other.tzinfo is None: |
|
dt = other.replace(tzinfo=self._datetime.tzinfo) |
|
else: |
|
dt = other.astimezone(self._datetime.tzinfo) |
|
|
|
else: |
|
raise TypeError( |
|
f"Invalid 'other' argument of type {type(other).__name__!r}. " |
|
"Argument must be of type None, Arrow, or datetime." |
|
) |
|
|
|
if isinstance(granularity, list) and len(granularity) == 1: |
|
granularity = granularity[0] |
|
|
|
_delta = int(round((self._datetime - dt).total_seconds())) |
|
sign = -1 if _delta < 0 else 1 |
|
delta_second = diff = abs(_delta) |
|
|
|
try: |
|
if granularity == "auto": |
|
if diff < 10: |
|
return locale.describe("now", only_distance=only_distance) |
|
|
|
if diff < self._SECS_PER_MINUTE: |
|
seconds = sign * delta_second |
|
return locale.describe( |
|
"seconds", seconds, only_distance=only_distance |
|
) |
|
|
|
elif diff < self._SECS_PER_MINUTE * 2: |
|
return locale.describe("minute", sign, only_distance=only_distance) |
|
elif diff < self._SECS_PER_HOUR: |
|
minutes = sign * max(delta_second // self._SECS_PER_MINUTE, 2) |
|
return locale.describe( |
|
"minutes", minutes, only_distance=only_distance |
|
) |
|
|
|
elif diff < self._SECS_PER_HOUR * 2: |
|
return locale.describe("hour", sign, only_distance=only_distance) |
|
elif diff < self._SECS_PER_DAY: |
|
hours = sign * max(delta_second // self._SECS_PER_HOUR, 2) |
|
return locale.describe("hours", hours, only_distance=only_distance) |
|
elif diff < self._SECS_PER_DAY * 2: |
|
return locale.describe("day", sign, only_distance=only_distance) |
|
elif diff < self._SECS_PER_WEEK: |
|
days = sign * max(delta_second // self._SECS_PER_DAY, 2) |
|
return locale.describe("days", days, only_distance=only_distance) |
|
|
|
elif diff < self._SECS_PER_WEEK * 2: |
|
return locale.describe("week", sign, only_distance=only_distance) |
|
elif diff < self._SECS_PER_MONTH: |
|
weeks = sign * max(delta_second // self._SECS_PER_WEEK, 2) |
|
return locale.describe("weeks", weeks, only_distance=only_distance) |
|
|
|
elif diff < self._SECS_PER_MONTH * 2: |
|
return locale.describe("month", sign, only_distance=only_distance) |
|
elif diff < self._SECS_PER_YEAR: |
|
|
|
self_months = self._datetime.year * 12 + self._datetime.month |
|
other_months = dt.year * 12 + dt.month |
|
|
|
months = sign * max(abs(other_months - self_months), 2) |
|
|
|
return locale.describe( |
|
"months", months, only_distance=only_distance |
|
) |
|
|
|
elif diff < self._SECS_PER_YEAR * 2: |
|
return locale.describe("year", sign, only_distance=only_distance) |
|
else: |
|
years = sign * max(delta_second // self._SECS_PER_YEAR, 2) |
|
return locale.describe("years", years, only_distance=only_distance) |
|
|
|
elif isinstance(granularity, str): |
|
granularity = cast(TimeFrameLiteral, granularity) |
|
|
|
if granularity == "second": |
|
delta = sign * float(delta_second) |
|
if abs(delta) < 2: |
|
return locale.describe("now", only_distance=only_distance) |
|
elif granularity == "minute": |
|
delta = sign * delta_second / self._SECS_PER_MINUTE |
|
elif granularity == "hour": |
|
delta = sign * delta_second / self._SECS_PER_HOUR |
|
elif granularity == "day": |
|
delta = sign * delta_second / self._SECS_PER_DAY |
|
elif granularity == "week": |
|
delta = sign * delta_second / self._SECS_PER_WEEK |
|
elif granularity == "month": |
|
delta = sign * delta_second / self._SECS_PER_MONTH |
|
elif granularity == "quarter": |
|
delta = sign * delta_second / self._SECS_PER_QUARTER |
|
elif granularity == "year": |
|
delta = sign * delta_second / self._SECS_PER_YEAR |
|
else: |
|
raise ValueError( |
|
"Invalid level of granularity. " |
|
"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' or 'year'." |
|
) |
|
|
|
if trunc(abs(delta)) != 1: |
|
granularity += "s" |
|
return locale.describe(granularity, delta, only_distance=only_distance) |
|
|
|
else: |
|
if not granularity: |
|
raise ValueError( |
|
"Empty granularity list provided. " |
|
"Please select one or more from 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter', 'year'." |
|
) |
|
|
|
timeframes: List[Tuple[TimeFrameLiteral, float]] = [] |
|
|
|
def gather_timeframes(_delta: float, _frame: TimeFrameLiteral) -> float: |
|
if _frame in granularity: |
|
value = sign * _delta / self._SECS_MAP[_frame] |
|
_delta %= self._SECS_MAP[_frame] |
|
if trunc(abs(value)) != 1: |
|
timeframes.append( |
|
(cast(TimeFrameLiteral, _frame + "s"), value) |
|
) |
|
else: |
|
timeframes.append((_frame, value)) |
|
return _delta |
|
|
|
delta = float(delta_second) |
|
frames: Tuple[TimeFrameLiteral, ...] = ( |
|
"year", |
|
"quarter", |
|
"month", |
|
"week", |
|
"day", |
|
"hour", |
|
"minute", |
|
"second", |
|
) |
|
for frame in frames: |
|
delta = gather_timeframes(delta, frame) |
|
|
|
if len(timeframes) < len(granularity): |
|
raise ValueError( |
|
"Invalid level of granularity. " |
|
"Please select between 'second', 'minute', 'hour', 'day', 'week', 'month', 'quarter' or 'year'." |
|
) |
|
|
|
return locale.describe_multi(timeframes, only_distance=only_distance) |
|
|
|
except KeyError as e: |
|
raise ValueError( |
|
f"Humanization of the {e} granularity is not currently translated in the {locale_name!r} locale. " |
|
"Please consider making a contribution to this locale." |
|
) |
|
|
|
def dehumanize(self, input_string: str, locale: str = "en_us") -> "Arrow": |
|
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, that represents |
|
the time difference relative to the attributes of the |
|
:class:`Arrow <arrow.arrow.Arrow>` object. |
|
|
|
:param timestring: a ``str`` representing a humanized relative time. |
|
:param locale: (optional) a ``str`` specifying a locale. Defaults to 'en-us'. |
|
|
|
Usage:: |
|
|
|
>>> arw = arrow.utcnow() |
|
>>> arw |
|
<Arrow [2021-04-20T22:27:34.787885+00:00]> |
|
>>> earlier = arw.dehumanize("2 days ago") |
|
>>> earlier |
|
<Arrow [2021-04-18T22:27:34.787885+00:00]> |
|
|
|
>>> arw = arrow.utcnow() |
|
>>> arw |
|
<Arrow [2021-04-20T22:27:34.787885+00:00]> |
|
>>> later = arw.dehumanize("in a month") |
|
>>> later |
|
<Arrow [2021-05-18T22:27:34.787885+00:00]> |
|
|
|
""" |
|
|
|
|
|
locale_obj = locales.get_locale(locale) |
|
|
|
|
|
normalized_locale_name = locale.lower().replace("_", "-") |
|
|
|
if normalized_locale_name not in DEHUMANIZE_LOCALES: |
|
raise ValueError( |
|
f"Dehumanize does not currently support the {locale} locale, please consider making a contribution to add support for this locale." |
|
) |
|
|
|
current_time = self.fromdatetime(self._datetime) |
|
|
|
|
|
time_object_info = dict.fromkeys( |
|
["seconds", "minutes", "hours", "days", "weeks", "months", "years"], 0 |
|
) |
|
|
|
|
|
unit_visited = dict.fromkeys( |
|
["now", "seconds", "minutes", "hours", "days", "weeks", "months", "years"], |
|
False, |
|
) |
|
|
|
|
|
num_pattern = re.compile(r"\d+") |
|
|
|
|
|
for unit, unit_object in locale_obj.timeframes.items(): |
|
|
|
if isinstance(unit_object, Mapping): |
|
strings_to_search = unit_object |
|
else: |
|
strings_to_search = {unit: str(unit_object)} |
|
|
|
|
|
|
|
|
|
for time_delta, time_string in strings_to_search.items(): |
|
|
|
search_string = str(time_string) |
|
search_string = search_string.format(r"\d+") |
|
|
|
|
|
pattern = re.compile(rf"(^|\b|\d){search_string}") |
|
match = pattern.search(input_string) |
|
|
|
|
|
if not match: |
|
continue |
|
|
|
match_string = match.group() |
|
num_match = num_pattern.search(match_string) |
|
|
|
|
|
|
|
if not num_match: |
|
change_value = ( |
|
1 if not time_delta.isnumeric() else abs(int(time_delta)) |
|
) |
|
else: |
|
change_value = int(num_match.group()) |
|
|
|
|
|
if unit == "now": |
|
unit_visited[unit] = True |
|
continue |
|
|
|
|
|
time_unit_to_change = str(unit) |
|
time_unit_to_change += ( |
|
"s" if (str(time_unit_to_change)[-1] != "s") else "" |
|
) |
|
time_object_info[time_unit_to_change] = change_value |
|
unit_visited[time_unit_to_change] = True |
|
|
|
|
|
if not any([True for k, v in unit_visited.items() if v]): |
|
raise ValueError( |
|
"Input string not valid. Note: Some locales do not support the week granularity in Arrow. " |
|
"If you are attempting to use the week granularity on an unsupported locale, this could be the cause of this error." |
|
) |
|
|
|
|
|
future_string = locale_obj.future |
|
future_string = future_string.format(".*") |
|
future_pattern = re.compile(rf"^{future_string}$") |
|
future_pattern_match = future_pattern.findall(input_string) |
|
|
|
past_string = locale_obj.past |
|
past_string = past_string.format(".*") |
|
past_pattern = re.compile(rf"^{past_string}$") |
|
past_pattern_match = past_pattern.findall(input_string) |
|
|
|
|
|
|
|
if past_pattern_match: |
|
sign_val = -1 |
|
elif future_pattern_match: |
|
sign_val = 1 |
|
elif unit_visited["now"]: |
|
sign_val = 0 |
|
else: |
|
raise ValueError( |
|
"Invalid input String. String does not contain any relative time information. " |
|
"String should either represent a time in the future or a time in the past. " |
|
"Ex: 'in 5 seconds' or '5 seconds ago'." |
|
) |
|
|
|
time_changes = {k: sign_val * v for k, v in time_object_info.items()} |
|
|
|
return current_time.shift(**time_changes) |
|
|
|
|
|
|
|
def is_between( |
|
self, |
|
start: "Arrow", |
|
end: "Arrow", |
|
bounds: _BOUNDS = "()", |
|
) -> bool: |
|
"""Returns a boolean denoting whether the :class:`Arrow <arrow.arrow.Arrow>` object is between |
|
the start and end limits. |
|
|
|
:param start: an :class:`Arrow <arrow.arrow.Arrow>` object. |
|
:param end: an :class:`Arrow <arrow.arrow.Arrow>` object. |
|
:param bounds: (optional) a ``str`` of either '()', '(]', '[)', or '[]' that specifies |
|
whether to include or exclude the start and end values in the range. '(' excludes |
|
the start, '[' includes the start, ')' excludes the end, and ']' includes the end. |
|
If the bounds are not specified, the default bound '()' is used. |
|
|
|
Usage:: |
|
|
|
>>> start = arrow.get(datetime(2013, 5, 5, 12, 30, 10)) |
|
>>> end = arrow.get(datetime(2013, 5, 5, 12, 30, 36)) |
|
>>> arrow.get(datetime(2013, 5, 5, 12, 30, 27)).is_between(start, end) |
|
True |
|
|
|
>>> start = arrow.get(datetime(2013, 5, 5)) |
|
>>> end = arrow.get(datetime(2013, 5, 8)) |
|
>>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[]') |
|
True |
|
|
|
>>> start = arrow.get(datetime(2013, 5, 5)) |
|
>>> end = arrow.get(datetime(2013, 5, 8)) |
|
>>> arrow.get(datetime(2013, 5, 8)).is_between(start, end, '[)') |
|
False |
|
|
|
""" |
|
|
|
util.validate_bounds(bounds) |
|
|
|
if not isinstance(start, Arrow): |
|
raise TypeError( |
|
f"Cannot parse start date argument type of {type(start)!r}." |
|
) |
|
|
|
if not isinstance(end, Arrow): |
|
raise TypeError(f"Cannot parse end date argument type of {type(start)!r}.") |
|
|
|
include_start = bounds[0] == "[" |
|
include_end = bounds[1] == "]" |
|
|
|
target_ts = self.float_timestamp |
|
start_ts = start.float_timestamp |
|
end_ts = end.float_timestamp |
|
|
|
return ( |
|
(start_ts <= target_ts <= end_ts) |
|
and (include_start or start_ts < target_ts) |
|
and (include_end or target_ts < end_ts) |
|
) |
|
|
|
|
|
|
|
def date(self) -> date: |
|
"""Returns a ``date`` object with the same year, month and day. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().date() |
|
datetime.date(2019, 1, 23) |
|
|
|
""" |
|
|
|
return self._datetime.date() |
|
|
|
def time(self) -> dt_time: |
|
"""Returns a ``time`` object with the same hour, minute, second, microsecond. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().time() |
|
datetime.time(12, 15, 34, 68352) |
|
|
|
""" |
|
|
|
return self._datetime.time() |
|
|
|
def timetz(self) -> dt_time: |
|
"""Returns a ``time`` object with the same hour, minute, second, microsecond and |
|
tzinfo. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().timetz() |
|
datetime.time(12, 5, 18, 298893, tzinfo=tzutc()) |
|
|
|
""" |
|
|
|
return self._datetime.timetz() |
|
|
|
def astimezone(self, tz: Optional[dt_tzinfo]) -> dt_datetime: |
|
"""Returns a ``datetime`` object, converted to the specified timezone. |
|
|
|
:param tz: a ``tzinfo`` object. |
|
|
|
Usage:: |
|
|
|
>>> pacific=arrow.now('US/Pacific') |
|
>>> nyc=arrow.now('America/New_York').tzinfo |
|
>>> pacific.astimezone(nyc) |
|
datetime.datetime(2019, 1, 20, 10, 24, 22, 328172, tzinfo=tzfile('/usr/share/zoneinfo/America/New_York')) |
|
|
|
""" |
|
|
|
return self._datetime.astimezone(tz) |
|
|
|
def utcoffset(self) -> Optional[timedelta]: |
|
"""Returns a ``timedelta`` object representing the whole number of minutes difference from |
|
UTC time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.now('US/Pacific').utcoffset() |
|
datetime.timedelta(-1, 57600) |
|
|
|
""" |
|
|
|
return self._datetime.utcoffset() |
|
|
|
def dst(self) -> Optional[timedelta]: |
|
"""Returns the daylight savings time adjustment. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().dst() |
|
datetime.timedelta(0) |
|
|
|
""" |
|
|
|
return self._datetime.dst() |
|
|
|
def timetuple(self) -> struct_time: |
|
"""Returns a ``time.struct_time``, in the current timezone. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().timetuple() |
|
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=20, tm_hour=15, tm_min=17, tm_sec=8, tm_wday=6, tm_yday=20, tm_isdst=0) |
|
|
|
""" |
|
|
|
return self._datetime.timetuple() |
|
|
|
def utctimetuple(self) -> struct_time: |
|
"""Returns a ``time.struct_time``, in UTC time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().utctimetuple() |
|
time.struct_time(tm_year=2019, tm_mon=1, tm_mday=19, tm_hour=21, tm_min=41, tm_sec=7, tm_wday=5, tm_yday=19, tm_isdst=0) |
|
|
|
""" |
|
|
|
return self._datetime.utctimetuple() |
|
|
|
def toordinal(self) -> int: |
|
"""Returns the proleptic Gregorian ordinal of the date. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().toordinal() |
|
737078 |
|
|
|
""" |
|
|
|
return self._datetime.toordinal() |
|
|
|
def weekday(self) -> int: |
|
"""Returns the day of the week as an integer (0-6). |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().weekday() |
|
5 |
|
|
|
""" |
|
|
|
return self._datetime.weekday() |
|
|
|
def isoweekday(self) -> int: |
|
"""Returns the ISO day of the week as an integer (1-7). |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().isoweekday() |
|
6 |
|
|
|
""" |
|
|
|
return self._datetime.isoweekday() |
|
|
|
def isocalendar(self) -> Tuple[int, int, int]: |
|
"""Returns a 3-tuple, (ISO year, ISO week number, ISO weekday). |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().isocalendar() |
|
(2019, 3, 6) |
|
|
|
""" |
|
|
|
return self._datetime.isocalendar() |
|
|
|
def isoformat(self, sep: str = "T", timespec: str = "auto") -> str: |
|
"""Returns an ISO 8601 formatted representation of the date and time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().isoformat() |
|
'2019-01-19T18:30:52.442118+00:00' |
|
|
|
""" |
|
|
|
return self._datetime.isoformat(sep, timespec) |
|
|
|
def ctime(self) -> str: |
|
"""Returns a ctime formatted representation of the date and time. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().ctime() |
|
'Sat Jan 19 18:26:50 2019' |
|
|
|
""" |
|
|
|
return self._datetime.ctime() |
|
|
|
def strftime(self, format: str) -> str: |
|
"""Formats in the style of ``datetime.strftime``. |
|
|
|
:param format: the format string. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().strftime('%d-%m-%Y %H:%M:%S') |
|
'23-01-2019 12:28:17' |
|
|
|
""" |
|
|
|
return self._datetime.strftime(format) |
|
|
|
def for_json(self) -> str: |
|
"""Serializes for the ``for_json`` protocol of simplejson. |
|
|
|
Usage:: |
|
|
|
>>> arrow.utcnow().for_json() |
|
'2019-01-19T18:25:36.760079+00:00' |
|
|
|
""" |
|
|
|
return self.isoformat() |
|
|
|
|
|
|
|
def __add__(self, other: Any) -> "Arrow": |
|
if isinstance(other, (timedelta, relativedelta)): |
|
return self.fromdatetime(self._datetime + other, self._datetime.tzinfo) |
|
|
|
return NotImplemented |
|
|
|
def __radd__(self, other: Union[timedelta, relativedelta]) -> "Arrow": |
|
return self.__add__(other) |
|
|
|
@overload |
|
def __sub__(self, other: Union[timedelta, relativedelta]) -> "Arrow": |
|
pass |
|
|
|
@overload |
|
def __sub__(self, other: Union[dt_datetime, "Arrow"]) -> timedelta: |
|
pass |
|
|
|
def __sub__(self, other: Any) -> Union[timedelta, "Arrow"]: |
|
if isinstance(other, (timedelta, relativedelta)): |
|
return self.fromdatetime(self._datetime - other, self._datetime.tzinfo) |
|
|
|
elif isinstance(other, dt_datetime): |
|
return self._datetime - other |
|
|
|
elif isinstance(other, Arrow): |
|
return self._datetime - other._datetime |
|
|
|
return NotImplemented |
|
|
|
def __rsub__(self, other: Any) -> timedelta: |
|
if isinstance(other, dt_datetime): |
|
return other - self._datetime |
|
|
|
return NotImplemented |
|
|
|
|
|
|
|
def __eq__(self, other: Any) -> bool: |
|
if not isinstance(other, (Arrow, dt_datetime)): |
|
return False |
|
|
|
return self._datetime == self._get_datetime(other) |
|
|
|
def __ne__(self, other: Any) -> bool: |
|
if not isinstance(other, (Arrow, dt_datetime)): |
|
return True |
|
|
|
return not self.__eq__(other) |
|
|
|
def __gt__(self, other: Any) -> bool: |
|
if not isinstance(other, (Arrow, dt_datetime)): |
|
return NotImplemented |
|
|
|
return self._datetime > self._get_datetime(other) |
|
|
|
def __ge__(self, other: Any) -> bool: |
|
if not isinstance(other, (Arrow, dt_datetime)): |
|
return NotImplemented |
|
|
|
return self._datetime >= self._get_datetime(other) |
|
|
|
def __lt__(self, other: Any) -> bool: |
|
if not isinstance(other, (Arrow, dt_datetime)): |
|
return NotImplemented |
|
|
|
return self._datetime < self._get_datetime(other) |
|
|
|
def __le__(self, other: Any) -> bool: |
|
if not isinstance(other, (Arrow, dt_datetime)): |
|
return NotImplemented |
|
|
|
return self._datetime <= self._get_datetime(other) |
|
|
|
|
|
@staticmethod |
|
def _get_tzinfo(tz_expr: Optional[TZ_EXPR]) -> dt_tzinfo: |
|
"""Get normalized tzinfo object from various inputs.""" |
|
if tz_expr is None: |
|
return dateutil_tz.tzutc() |
|
if isinstance(tz_expr, dt_tzinfo): |
|
return tz_expr |
|
else: |
|
try: |
|
return parser.TzinfoParser.parse(tz_expr) |
|
except parser.ParserError: |
|
raise ValueError(f"{tz_expr!r} not recognized as a timezone.") |
|
|
|
@classmethod |
|
def _get_datetime( |
|
cls, expr: Union["Arrow", dt_datetime, int, float, str] |
|
) -> dt_datetime: |
|
"""Get datetime object from a specified expression.""" |
|
if isinstance(expr, Arrow): |
|
return expr.datetime |
|
elif isinstance(expr, dt_datetime): |
|
return expr |
|
elif util.is_timestamp(expr): |
|
timestamp = float(expr) |
|
return cls.utcfromtimestamp(timestamp).datetime |
|
else: |
|
raise ValueError(f"{expr!r} not recognized as a datetime or timestamp.") |
|
|
|
@classmethod |
|
def _get_frames(cls, name: _T_FRAMES) -> Tuple[str, str, int]: |
|
"""Finds relevant timeframe and steps for use in range and span methods. |
|
|
|
Returns a 3 element tuple in the form (frame, plural frame, step), for example ("day", "days", 1) |
|
|
|
""" |
|
if name in cls._ATTRS: |
|
return name, f"{name}s", 1 |
|
elif name[-1] == "s" and name[:-1] in cls._ATTRS: |
|
return name[:-1], name, 1 |
|
elif name in ["week", "weeks"]: |
|
return "week", "weeks", 1 |
|
elif name in ["quarter", "quarters"]: |
|
return "quarter", "months", 3 |
|
else: |
|
supported = ", ".join( |
|
[ |
|
"year(s)", |
|
"month(s)", |
|
"day(s)", |
|
"hour(s)", |
|
"minute(s)", |
|
"second(s)", |
|
"microsecond(s)", |
|
"week(s)", |
|
"quarter(s)", |
|
] |
|
) |
|
raise ValueError( |
|
f"Range or span over frame {name} not supported. Supported frames: {supported}." |
|
) |
|
|
|
@classmethod |
|
def _get_iteration_params(cls, end: Any, limit: Optional[int]) -> Tuple[Any, int]: |
|
"""Sets default end and limit values for range method.""" |
|
if end is None: |
|
if limit is None: |
|
raise ValueError("One of 'end' or 'limit' is required.") |
|
|
|
return cls.max, limit |
|
|
|
else: |
|
if limit is None: |
|
return end, sys.maxsize |
|
return end, limit |
|
|
|
@staticmethod |
|
def _is_last_day_of_month(date: "Arrow") -> bool: |
|
"""Returns a boolean indicating whether the datetime is the last day of the month.""" |
|
return date.day == calendar.monthrange(date.year, date.month)[1] |
|
|
|
|
|
Arrow.min = Arrow.fromdatetime(dt_datetime.min) |
|
Arrow.max = Arrow.fromdatetime(dt_datetime.max) |
|
|