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
5 changes: 0 additions & 5 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,6 @@

## Features

- Support negative slice bounds (`start`/`stop`) for zero-indexed finite
sequences, mirroring Python's own list-slicing semantics. Currently only
single-element negative indexing (`seq[-1]`) is translated;
`_process_range()` treats slice bounds as literal index values.

- Add a `sum()` utility method to `NumericSequence`, after careful design.

- Add a `round()` utility method to `NumericSequence`, and replace direct
Expand Down
21 changes: 17 additions & 4 deletions calculus/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ def __getitem__(self, subscript: int | slice) -> T | Sequence[T]:

If subscript is an integer, the corresponding element is
returned. If subscript is a slice, the corresponding
subsequence is returned.
subsequence is returned. Zero-indexed finite sequences also
support negative indexing and slicing.

Args:
subscript (int | slice): The index or slice specifying the
Expand Down Expand Up @@ -400,9 +401,21 @@ def _index_sequence(self, index: int) -> T:
def _slice_sequence(self, slice_: slice) -> Sequence[T]:
# Return the subsequence specified by the given slice.

start, step, size = self._process_range(
slice_.start, slice_.stop, slice_.step,
)
# Allow Python-style negative indexing for finite sequences
# starting at index 0.
start, stop, step = slice_.start, slice_.stop, slice_.step
if self.last_index is not INFINITY and self.first_index == 0:
effective_first_index = -(self.last_index + 1)
def adjust_negative_index(index: int) -> int:
index = max(index, effective_first_index)
index = index - effective_first_index
return index
if start is not None and start < 0:
start = adjust_negative_index(start)
if stop is not None and stop < 0:
stop = adjust_negative_index(stop)

start, step, size = self._process_range(start, stop, step)

def subrule(k: int) -> int:
return start + (k - self.first_index)*step
Expand Down
36 changes: 36 additions & 0 deletions tests/test_sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,42 @@ def test_negative_step_slice_on_infinite_sequence_with_stop_is_empty() -> None:
assert list(seq[:5:-1]) == []


def test_slice_negative_start_on_zero_indexed_finite_sequence() -> None:
seq = Sequence(lambda n: n, size=10, first_index=0)
assert list(seq[-3:]) == [7, 8, 9]


def test_slice_negative_stop_on_zero_indexed_finite_sequence() -> None:
seq = Sequence(lambda n: n, size=10, first_index=0)
assert list(seq[:-1]) == [0, 1, 2, 3, 4, 5, 6, 7, 8]


def test_slice_negative_bounds_on_zero_indexed_finite_sequence() -> None:
seq = Sequence(lambda n: n, size=10, first_index=0)
assert list(seq[-5:-1]) == [5, 6, 7, 8]


def test_slice_negative_bounds_out_of_range_clamp() -> None:
seq = Sequence(lambda n: n, size=10, first_index=0)
assert list(seq[-100:]) == [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
assert list(seq[:-100]) == []


def test_slice_negative_start_with_negative_step() -> None:
seq = Sequence(lambda n: n, size=10, first_index=0)
assert list(seq[-1::-1]) == [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]


def test_slice_negative_bounds_ignored_for_infinite_sequence() -> None:
seq = Sequence(lambda n: n, first_index=1)
assert list(seq[-1:].head(3)) == [1, 2, 3]


def test_slice_negative_bounds_ignored_for_one_indexed_sequence() -> None:
seq = Sequence(lambda n: n, size=5, first_index=1)
assert list(seq[-1:]) == [1, 2, 3, 4, 5]


# -- UTILITY

def test_bool_true_for_nonempty_finite_sequence() -> None:
Expand Down