Skip to content

Commit 4c8c44a

Browse files
krisfremenjadchaar
andauthored
Add week_start parameter to floor() and ceil() (#1222)
* add kwargs to ceil and floor. pass it through to span. * add to guide.rst --------- Co-authored-by: Jad Chaar <jadchaar@users.noreply.github.com>
1 parent 7ccbe66 commit 4c8c44a

File tree

3 files changed

+178
-6
lines changed

3 files changed

+178
-6
lines changed

arrow/arrow.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -550,14 +550,14 @@ def span(
550550
(<Arrow [2021-02-20T00:00:00+00:00]>, <Arrow [2021-02-26T23:59:59.999999+00:00]>)
551551
552552
"""
553-
if not 1 <= week_start <= 7:
554-
raise ValueError("week_start argument must be between 1 and 7.")
555553

556554
util.validate_bounds(bounds)
557555

558556
frame_absolute, frame_relative, relative_steps = self._get_frames(frame)
559557

560558
if frame_absolute == "week":
559+
if not 1 <= week_start <= 7:
560+
raise ValueError("week_start argument must be between 1 and 7.")
561561
attr = "day"
562562
elif frame_absolute == "quarter":
563563
attr = "month"
@@ -595,39 +595,49 @@ def span(
595595

596596
return floor, ceil
597597

598-
def floor(self, frame: _T_FRAMES) -> "Arrow":
598+
def floor(self, frame: _T_FRAMES, **kwargs: Any) -> "Arrow":
599599
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the "floor"
600600
of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.
601601
Equivalent to the first element in the 2-tuple returned by
602602
:func:`span <arrow.arrow.Arrow.span>`.
603603
604604
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
605+
:param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where
606+
Monday is 1 and Sunday is 7.
605607
606608
Usage::
607609
608610
>>> arrow.utcnow().floor('hour')
609611
<Arrow [2013-05-09T03:00:00+00:00]>
610612
613+
>>> arrow.utcnow().floor('week', week_start=7)
614+
<Arrow [2021-02-21T00:00:00+00:00]>
615+
611616
"""
612617

613-
return self.span(frame)[0]
618+
return self.span(frame, **kwargs)[0]
614619

615-
def ceil(self, frame: _T_FRAMES) -> "Arrow":
620+
def ceil(self, frame: _T_FRAMES, **kwargs: Any) -> "Arrow":
616621
"""Returns a new :class:`Arrow <arrow.arrow.Arrow>` object, representing the "ceiling"
617622
of the timespan of the :class:`Arrow <arrow.arrow.Arrow>` object in a given timeframe.
618623
Equivalent to the second element in the 2-tuple returned by
619624
:func:`span <arrow.arrow.Arrow.span>`.
620625
621626
:param frame: the timeframe. Can be any ``datetime`` property (day, hour, minute...).
627+
:param week_start: (optional) only used in combination with the week timeframe. Follows isoweekday() where
628+
Monday is 1 and Sunday is 7.
622629
623630
Usage::
624631
625632
>>> arrow.utcnow().ceil('hour')
626633
<Arrow [2013-05-09T03:59:59.999999+00:00]>
627634
635+
>>> arrow.utcnow().ceil('week', week_start=7)
636+
<Arrow [2021-02-27T23:59:59.999999+00:00]>
637+
628638
"""
629639

630-
return self.span(frame)[1]
640+
return self.span(frame, **kwargs)[1]
631641

632642
@classmethod
633643
def span_range(

docs/guide.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,12 @@ Or just get the floor and ceiling:
303303
>>> arrow.utcnow().ceil('hour')
304304
<Arrow [2013-05-07T05:59:59.999999+00:00]>
305305
306+
>>> arrow.utcnow().floor('week', week_start=7)
307+
<Arrow [2013-05-05T00:00:00+00:00]>
308+
309+
>>> arrow.utcnow().ceil('week', week_start=7)
310+
<Arrow [2013-05-11T23:59:59.999999+00:00]>
311+
306312
You can also get a range of time spans:
307313

308314
.. code-block:: python

tests/test_arrow.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1727,6 +1727,162 @@ def test_floor(self):
17271727
assert floor == self.arrow.floor("month")
17281728
assert ceil == self.arrow.ceil("month")
17291729

1730+
def test_floor_week_start(self):
1731+
"""
1732+
Test floor method with week_start parameter for different week starts.
1733+
"""
1734+
# Test with default week_start=1 (Monday)
1735+
floor_default = self.arrow.floor("week")
1736+
floor_span_default, _ = self.arrow.span("week")
1737+
assert floor_default == floor_span_default
1738+
1739+
# Test with week_start=1 (Monday) - explicit
1740+
floor_monday = self.arrow.floor("week", week_start=1)
1741+
floor_span_monday, _ = self.arrow.span("week", week_start=1)
1742+
assert floor_monday == floor_span_monday
1743+
1744+
# Test with week_start=7 (Sunday)
1745+
floor_sunday = self.arrow.floor("week", week_start=7)
1746+
floor_span_sunday, _ = self.arrow.span("week", week_start=7)
1747+
assert floor_sunday == floor_span_sunday
1748+
1749+
# Test with week_start=6 (Saturday)
1750+
floor_saturday = self.arrow.floor("week", week_start=6)
1751+
floor_span_saturday, _ = self.arrow.span("week", week_start=6)
1752+
assert floor_saturday == floor_span_saturday
1753+
1754+
# Test with week_start=2 (Tuesday)
1755+
floor_tuesday = self.arrow.floor("week", week_start=2)
1756+
floor_span_tuesday, _ = self.arrow.span("week", week_start=2)
1757+
assert floor_tuesday == floor_span_tuesday
1758+
1759+
def test_ceil_week_start(self):
1760+
"""
1761+
Test ceil method with week_start parameter for different week starts.
1762+
"""
1763+
# Test with default week_start=1 (Monday)
1764+
ceil_default = self.arrow.ceil("week")
1765+
_, ceil_span_default = self.arrow.span("week")
1766+
assert ceil_default == ceil_span_default
1767+
1768+
# Test with week_start=1 (Monday) - explicit
1769+
ceil_monday = self.arrow.ceil("week", week_start=1)
1770+
_, ceil_span_monday = self.arrow.span("week", week_start=1)
1771+
assert ceil_monday == ceil_span_monday
1772+
1773+
# Test with week_start=7 (Sunday)
1774+
ceil_sunday = self.arrow.ceil("week", week_start=7)
1775+
_, ceil_span_sunday = self.arrow.span("week", week_start=7)
1776+
assert ceil_sunday == ceil_span_sunday
1777+
1778+
# Test with week_start=6 (Saturday)
1779+
ceil_saturday = self.arrow.ceil("week", week_start=6)
1780+
_, ceil_span_saturday = self.arrow.span("week", week_start=6)
1781+
assert ceil_saturday == ceil_span_saturday
1782+
1783+
# Test with week_start=2 (Tuesday)
1784+
ceil_tuesday = self.arrow.ceil("week", week_start=2)
1785+
_, ceil_span_tuesday = self.arrow.span("week", week_start=2)
1786+
assert ceil_tuesday == ceil_span_tuesday
1787+
1788+
def test_floor_ceil_week_start_values(self):
1789+
"""
1790+
Test specific date values for floor and ceil with different week_start values.
1791+
The test arrow is 2013-02-15 (Friday, isoweekday=5).
1792+
"""
1793+
# Test Monday start (week_start=1)
1794+
# Friday should floor to previous Monday (2013-02-11)
1795+
floor_mon = self.arrow.floor("week", week_start=1)
1796+
assert floor_mon == datetime(2013, 2, 11, tzinfo=tz.tzutc())
1797+
# Friday should ceil to next Sunday (2013-02-17)
1798+
ceil_mon = self.arrow.ceil("week", week_start=1)
1799+
assert ceil_mon == datetime(2013, 2, 17, 23, 59, 59, 999999, tzinfo=tz.tzutc())
1800+
1801+
# Test Sunday start (week_start=7)
1802+
# Friday should floor to previous Sunday (2013-02-10)
1803+
floor_sun = self.arrow.floor("week", week_start=7)
1804+
assert floor_sun == datetime(2013, 2, 10, tzinfo=tz.tzutc())
1805+
# Friday should ceil to next Saturday (2013-02-16)
1806+
ceil_sun = self.arrow.ceil("week", week_start=7)
1807+
assert ceil_sun == datetime(2013, 2, 16, 23, 59, 59, 999999, tzinfo=tz.tzutc())
1808+
1809+
# Test Saturday start (week_start=6)
1810+
# Friday should floor to previous Saturday (2013-02-09)
1811+
floor_sat = self.arrow.floor("week", week_start=6)
1812+
assert floor_sat == datetime(2013, 2, 9, tzinfo=tz.tzutc())
1813+
# Friday should ceil to next Friday (2013-02-15)
1814+
ceil_sat = self.arrow.ceil("week", week_start=6)
1815+
assert ceil_sat == datetime(2013, 2, 15, 23, 59, 59, 999999, tzinfo=tz.tzutc())
1816+
1817+
def test_floor_ceil_week_start_backward_compatibility(self):
1818+
"""
1819+
Test that floor and ceil methods maintain backward compatibility
1820+
when called without the week_start parameter.
1821+
"""
1822+
# Test that calling floor/ceil without parameters works the same as before
1823+
floor_old = self.arrow.floor("week")
1824+
floor_new = self.arrow.floor("week", week_start=1) # default value
1825+
assert floor_old == floor_new
1826+
1827+
ceil_old = self.arrow.ceil("week")
1828+
ceil_new = self.arrow.ceil("week", week_start=1) # default value
1829+
assert ceil_old == ceil_new
1830+
1831+
def test_floor_ceil_week_start_ignored_for_non_week_frames(self):
1832+
"""
1833+
Test that week_start parameter is ignored for non-week frames.
1834+
"""
1835+
# Test that week_start parameter is ignored for different frames
1836+
for frame in ["hour", "day", "month", "year"]:
1837+
# floor should work the same with or without week_start for non-week frames
1838+
floor_without = self.arrow.floor(frame)
1839+
floor_with = self.arrow.floor(frame, week_start=7) # should be ignored
1840+
assert floor_without == floor_with
1841+
1842+
# ceil should work the same with or without week_start for non-week frames
1843+
ceil_without = self.arrow.ceil(frame)
1844+
ceil_with = self.arrow.ceil(frame, week_start=7) # should be ignored
1845+
assert ceil_without == ceil_with
1846+
1847+
def test_floor_ceil_week_start_validation(self):
1848+
"""
1849+
Test that week_start parameter validation works correctly for week frames.
1850+
"""
1851+
# Valid values should work for week frames
1852+
for week_start in range(1, 8):
1853+
self.arrow.floor("week", week_start=week_start)
1854+
self.arrow.ceil("week", week_start=week_start)
1855+
1856+
# Invalid values should raise ValueError for week frames
1857+
with pytest.raises(
1858+
ValueError, match="week_start argument must be between 1 and 7"
1859+
):
1860+
self.arrow.floor("week", week_start=0)
1861+
1862+
with pytest.raises(
1863+
ValueError, match="week_start argument must be between 1 and 7"
1864+
):
1865+
self.arrow.floor("week", week_start=8)
1866+
1867+
with pytest.raises(
1868+
ValueError, match="week_start argument must be between 1 and 7"
1869+
):
1870+
self.arrow.ceil("week", week_start=0)
1871+
1872+
with pytest.raises(
1873+
ValueError, match="week_start argument must be between 1 and 7"
1874+
):
1875+
self.arrow.ceil("week", week_start=8)
1876+
1877+
# Invalid week_start values should be ignored for non-week frames (no validation)
1878+
# This ensures the parameter doesn't cause errors for other frames
1879+
for frame in ["hour", "day", "month", "year"]:
1880+
# These should not raise errors even though week_start is invalid
1881+
self.arrow.floor(frame, week_start=0)
1882+
self.arrow.floor(frame, week_start=8)
1883+
self.arrow.ceil(frame, week_start=0)
1884+
self.arrow.ceil(frame, week_start=8)
1885+
17301886
def test_span_inclusive_inclusive(self):
17311887
floor, ceil = self.arrow.span("hour", bounds="[]")
17321888

0 commit comments

Comments
 (0)