Skip to content

Commit

Permalink
Merge pull request #181 from octoenergy/ranges/dt-date-range-method
Browse files Browse the repository at this point in the history
Encapsulate date_range... fn in FiniteDatetimeRange
  • Loading branch information
benthorner authored Dec 19, 2024
2 parents 7f34d7c + 2786ddb commit 8ef5cfa
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 79 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

- Add `FiniteDatetimeRange.as_date_range` [#181](https://github.com/octoenergy/xocto/pull/181)
- [Breaking] Remove `ranges.date_range_for_midnight_range` (replaced by FiniteDatetimeRange.as_date_range)[#181] (https://github.com/octoenergy/xocto/pull/181)

## v6.2.0 - 2024-12-11

- Add `ranges.date_range_for_midnight_range` [#178] (https://github.com/octoenergy/xocto/pull/178)
Expand Down
95 changes: 47 additions & 48 deletions tests/test_ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -1028,6 +1028,53 @@ def test_errors_if_naive(self):

assert "naive" in str(exc_info.value)

class TestAsDateRange:
def test_returns_date_range(self):
dt_range = ranges.FiniteDatetimeRange(
datetime.datetime(2020, 1, 1),
datetime.datetime(2020, 1, 10),
)

assert dt_range.as_date_range() == ranges.FiniteDateRange(
datetime.date(2020, 1, 1),
datetime.date(2020, 1, 9),
)

def test_errors_if_different_timezones(self):
dt_range = ranges.FiniteDatetimeRange(
datetime.datetime(2020, 1, 1, tzinfo=zoneinfo.ZoneInfo("Asia/Dubai")),
datetime.datetime(
2020, 1, 10, tzinfo=zoneinfo.ZoneInfo("Australia/Sydney")
),
)

with pytest.raises(ValueError) as exc_info:
dt_range.as_date_range()

assert "Start and end in different timezones" in str(exc_info.value)

def test_errors_if_start_not_midnight(self):
dt_range = ranges.FiniteDatetimeRange(
datetime.datetime(2020, 1, 1, hour=1),
datetime.datetime(2020, 1, 10),
)

with pytest.raises(ValueError) as exc_info:
dt_range.as_date_range()

assert "Start of range is not midnight-aligned" in str(exc_info.value)

def test_errors_if_end_not_midnight(self):
dt_range = ranges.FiniteDatetimeRange(
datetime.datetime(2020, 1, 1),
datetime.datetime(2020, 1, 10, hour=1),
)

with pytest.raises(ValueError) as exc_info:
dt_range.as_date_range()

assert "End of range is not midnight-aligned" in str(exc_info.value)


class TestAsFiniteDatetimePeriods:
def test_converts(self):
Expand Down Expand Up @@ -1187,54 +1234,6 @@ def test_yields_correct_ranges(self, row):
assert result == row["expected"]


class TestDateRangeForMidnightRange:
def test_returns_date_range(self):
dt_range = ranges.FiniteDatetimeRange(
datetime.datetime(2020, 1, 1),
datetime.datetime(2020, 1, 10),
)

assert ranges.date_range_for_midnight_range(dt_range) == ranges.FiniteDateRange(
datetime.date(2020, 1, 1),
datetime.date(2020, 1, 9),
)

def test_errors_if_different_timezones(self):
dt_range = ranges.FiniteDatetimeRange(
datetime.datetime(2020, 1, 1, tzinfo=zoneinfo.ZoneInfo("Asia/Dubai")),
datetime.datetime(
2020, 1, 10, tzinfo=zoneinfo.ZoneInfo("Australia/Sydney")
),
)

with pytest.raises(ValueError) as exc_info:
ranges.date_range_for_midnight_range(dt_range)

assert "Start and end in different timezones" in str(exc_info.value)

def test_errors_if_start_not_midnight(self):
dt_range = ranges.FiniteDatetimeRange(
datetime.datetime(2020, 1, 1, hour=1),
datetime.datetime(2020, 1, 10),
)

with pytest.raises(ValueError) as exc_info:
ranges.date_range_for_midnight_range(dt_range)

assert "Start of range is not midnight-aligned" in str(exc_info.value)

def test_errors_if_end_not_midnight(self):
dt_range = ranges.FiniteDatetimeRange(
datetime.datetime(2020, 1, 1),
datetime.datetime(2020, 1, 10, hour=1),
)

with pytest.raises(ValueError) as exc_info:
ranges.date_range_for_midnight_range(dt_range)

assert "End of range is not midnight-aligned" in str(exc_info.value)


def _rangeset_from_string(rangeset_str: str) -> ranges.RangeSet[int]:
"""
Convenience method to make test declarations clearer.
Expand Down
61 changes: 30 additions & 31 deletions xocto/ranges.py
Original file line number Diff line number Diff line change
Expand Up @@ -904,6 +904,36 @@ def localize(self, tz: datetime.tzinfo) -> FiniteDatetimeRange:
self.end.astimezone(tz),
)

def as_date_range(
self: FiniteDatetimeRange,
) -> FiniteDateRange:
"""
Returns the date range covered by this range (if midnight-aligned).
This can be useful where a range is available at datetime granularity,
but is used in functions that operate at date granularity.
Raises:
ValueError:
If the range boundaries are in different timezeones.
If the range boundaries are not midnight-aligned.
"""
# First check range timezone is uniform.
if self.start.tzinfo != self.end.tzinfo:
raise ValueError("Start and end in different timezones")

# Check datetimes are both midnight-aligned.
if self.start.time() != datetime.time(0, 0):
raise ValueError("Start of range is not midnight-aligned")

if self.end.time() != datetime.time(0, 0):
raise ValueError("End of range is not midnight-aligned")

return FiniteDateRange(
self.start.date(),
self.end.date() - datetime.timedelta(days=1),
)


class FiniteDateRange(FiniteRange[datetime.date]):
"""
Expand Down Expand Up @@ -1085,34 +1115,3 @@ def iterate_over_months(

yield FiniteDatetimeRange(start_at, this_end)
start_at = next_start


def date_range_for_midnight_range(
range: FiniteDatetimeRange,
) -> FiniteDateRange:
"""
Returns the date range of a midnight-aligned datetime range.
This can be useful where a range is available at datetime granularity,
but is used in functions that operate at date granularity.
Raises:
ValueError:
If the range boundaries are in different timezeones.
If the range boundaries are not midnight-aligned.
"""
# First check range timezone is uniform.
if range.start.tzinfo != range.end.tzinfo:
raise ValueError("Start and end in different timezones")

# Check datetimes are both midnight-aligned.
if range.start.time() != datetime.time(0, 0):
raise ValueError("Start of range is not midnight-aligned")

if range.end.time() != datetime.time(0, 0):
raise ValueError("End of range is not midnight-aligned")

return FiniteDateRange(
range.start.date(),
range.end.date() - datetime.timedelta(days=1),
)

0 comments on commit 8ef5cfa

Please sign in to comment.