Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions arrow/arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -550,14 +550,14 @@ def span(
(<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":
if not 1 <= week_start <= 7:
raise ValueError("week_start argument must be between 1 and 7.")
attr = "day"
elif frame_absolute == "quarter":
attr = "month"
Expand Down Expand Up @@ -595,39 +595,49 @@ def span(

return floor, ceil

def floor(self, frame: _T_FRAMES) -> "Arrow":
def floor(self, frame: _T_FRAMES, **kwargs: Any) -> "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...).
:param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where
Monday is 1 and Sunday is 7.

Usage::

>>> arrow.utcnow().floor('hour')
<Arrow [2013-05-09T03:00:00+00:00]>

>>> arrow.utcnow().floor('week', week_start=7)
<Arrow [2021-02-21T00:00:00+00:00]>

"""

return self.span(frame)[0]
return self.span(frame, **kwargs)[0]

def ceil(self, frame: _T_FRAMES) -> "Arrow":
def ceil(self, frame: _T_FRAMES, **kwargs: Any) -> "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...).
:param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where
Monday is 1 and Sunday is 7.

Usage::

>>> arrow.utcnow().ceil('hour')
<Arrow [2013-05-09T03:59:59.999999+00:00]>

>>> arrow.utcnow().ceil('week', week_start=7)
<Arrow [2021-02-27T23:59:59.999999+00:00]>

"""

return self.span(frame)[1]
return self.span(frame, **kwargs)[1]

@classmethod
def span_range(
Expand Down
6 changes: 6 additions & 0 deletions docs/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,12 @@ Or just get the floor and ceiling:
>>> arrow.utcnow().ceil('hour')
<Arrow [2013-05-07T05:59:59.999999+00:00]>

>>> arrow.utcnow().floor('week', week_start=7)
<Arrow [2013-05-05T00:00:00+00:00]>

>>> arrow.utcnow().ceil('week', week_start=7)
<Arrow [2013-05-11T23:59:59.999999+00:00]>

You can also get a range of time spans:

.. code-block:: python
Expand Down
156 changes: 156 additions & 0 deletions tests/test_arrow.py
Original file line number Diff line number Diff line change
Expand Up @@ -1727,6 +1727,162 @@ def test_floor(self):
assert floor == self.arrow.floor("month")
assert ceil == self.arrow.ceil("month")

def test_floor_week_start(self):
"""
Test floor method with week_start parameter for different week starts.
"""
# Test with default week_start=1 (Monday)
floor_default = self.arrow.floor("week")
floor_span_default, _ = self.arrow.span("week")
assert floor_default == floor_span_default

# Test with week_start=1 (Monday) - explicit
floor_monday = self.arrow.floor("week", week_start=1)
floor_span_monday, _ = self.arrow.span("week", week_start=1)
assert floor_monday == floor_span_monday

# Test with week_start=7 (Sunday)
floor_sunday = self.arrow.floor("week", week_start=7)
floor_span_sunday, _ = self.arrow.span("week", week_start=7)
assert floor_sunday == floor_span_sunday

# Test with week_start=6 (Saturday)
floor_saturday = self.arrow.floor("week", week_start=6)
floor_span_saturday, _ = self.arrow.span("week", week_start=6)
assert floor_saturday == floor_span_saturday

# Test with week_start=2 (Tuesday)
floor_tuesday = self.arrow.floor("week", week_start=2)
floor_span_tuesday, _ = self.arrow.span("week", week_start=2)
assert floor_tuesday == floor_span_tuesday

def test_ceil_week_start(self):
"""
Test ceil method with week_start parameter for different week starts.
"""
# Test with default week_start=1 (Monday)
ceil_default = self.arrow.ceil("week")
_, ceil_span_default = self.arrow.span("week")
assert ceil_default == ceil_span_default

# Test with week_start=1 (Monday) - explicit
ceil_monday = self.arrow.ceil("week", week_start=1)
_, ceil_span_monday = self.arrow.span("week", week_start=1)
assert ceil_monday == ceil_span_monday

# Test with week_start=7 (Sunday)
ceil_sunday = self.arrow.ceil("week", week_start=7)
_, ceil_span_sunday = self.arrow.span("week", week_start=7)
assert ceil_sunday == ceil_span_sunday

# Test with week_start=6 (Saturday)
ceil_saturday = self.arrow.ceil("week", week_start=6)
_, ceil_span_saturday = self.arrow.span("week", week_start=6)
assert ceil_saturday == ceil_span_saturday

# Test with week_start=2 (Tuesday)
ceil_tuesday = self.arrow.ceil("week", week_start=2)
_, ceil_span_tuesday = self.arrow.span("week", week_start=2)
assert ceil_tuesday == ceil_span_tuesday

def test_floor_ceil_week_start_values(self):
"""
Test specific date values for floor and ceil with different week_start values.
The test arrow is 2013-02-15 (Friday, isoweekday=5).
"""
# Test Monday start (week_start=1)
# Friday should floor to previous Monday (2013-02-11)
floor_mon = self.arrow.floor("week", week_start=1)
assert floor_mon == datetime(2013, 2, 11, tzinfo=tz.tzutc())
# Friday should ceil to next Sunday (2013-02-17)
ceil_mon = self.arrow.ceil("week", week_start=1)
assert ceil_mon == datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc())

# Test Sunday start (week_start=7)
# Friday should floor to previous Sunday (2013-02-10)
floor_sun = self.arrow.floor("week", week_start=7)
assert floor_sun == datetime(2013, 2, 10, tzinfo=tz.tzutc())
# Friday should ceil to next Saturday (2013-02-16)
ceil_sun = self.arrow.ceil("week", week_start=7)
assert ceil_sun == datetime(2013, 2, 16, 23, 59, 59, 999999, tzinfo=tz.tzutc())

# Test Saturday start (week_start=6)
# Friday should floor to previous Saturday (2013-02-09)
floor_sat = self.arrow.floor("week", week_start=6)
assert floor_sat == datetime(2013, 2, 9, tzinfo=tz.tzutc())
# Friday should ceil to next Friday (2013-02-15)
ceil_sat = self.arrow.ceil("week", week_start=6)
assert ceil_sat == datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc())

def test_floor_ceil_week_start_backward_compatibility(self):
"""
Test that floor and ceil methods maintain backward compatibility
when called without the week_start parameter.
"""
# Test that calling floor/ceil without parameters works the same as before
floor_old = self.arrow.floor("week")
floor_new = self.arrow.floor("week", week_start=1) # default value
assert floor_old == floor_new

ceil_old = self.arrow.ceil("week")
ceil_new = self.arrow.ceil("week", week_start=1) # default value
assert ceil_old == ceil_new

def test_floor_ceil_week_start_ignored_for_non_week_frames(self):
"""
Test that week_start parameter is ignored for non-week frames.
"""
# Test that week_start parameter is ignored for different frames
for frame in ["hour", "day", "month", "year"]:
# floor should work the same with or without week_start for non-week frames
floor_without = self.arrow.floor(frame)
floor_with = self.arrow.floor(frame, week_start=7) # should be ignored
assert floor_without == floor_with

# ceil should work the same with or without week_start for non-week frames
ceil_without = self.arrow.ceil(frame)
ceil_with = self.arrow.ceil(frame, week_start=7) # should be ignored
assert ceil_without == ceil_with

def test_floor_ceil_week_start_validation(self):
"""
Test that week_start parameter validation works correctly for week frames.
"""
# Valid values should work for week frames
for week_start in range(1, 8):
self.arrow.floor("week", week_start=week_start)
self.arrow.ceil("week", week_start=week_start)

# Invalid values should raise ValueError for week frames
with pytest.raises(
ValueError, match="week_start argument must be between 1 and 7"
):
self.arrow.floor("week", week_start=0)

with pytest.raises(
ValueError, match="week_start argument must be between 1 and 7"
):
self.arrow.floor("week", week_start=8)

with pytest.raises(
ValueError, match="week_start argument must be between 1 and 7"
):
self.arrow.ceil("week", week_start=0)

with pytest.raises(
ValueError, match="week_start argument must be between 1 and 7"
):
self.arrow.ceil("week", week_start=8)

# Invalid week_start values should be ignored for non-week frames (no validation)
# This ensures the parameter doesn't cause errors for other frames
for frame in ["hour", "day", "month", "year"]:
# These should not raise errors even though week_start is invalid
self.arrow.floor(frame, week_start=0)
self.arrow.floor(frame, week_start=8)
self.arrow.ceil(frame, week_start=0)
self.arrow.ceil(frame, week_start=8)

def test_span_inclusive_inclusive(self):
floor, ceil = self.arrow.span("hour", bounds="[]")

Expand Down
Loading