Skip to content

Commit 51a4e0d

Browse files
feat(table) - Add fancy indexing
1 parent 1888d7e commit 51a4e0d

4 files changed

Lines changed: 433 additions & 6 deletions

File tree

docs/docs/content/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ All notable changes to Rayforce-Py will be documented in this file.
55
!!! note ""
66
You can also subscribe for release notifications by joining our [:simple-zulip: Zulip](https://rayforcedb.zulipchat.com/#narrow/channel/549008-Discuss)!
77

8+
## **`0.6.1`**
9+
10+
### New Features
11+
12+
- **Fancy indexing for Tables**: `Table.__getitem__` now supports multiple indexing modes beyond column access:
13+
14+
- **Expression filter**: `table[Column("age") > 35]` — filter rows by condition. Supports `&` (and) and `|` (or) for combining expressions.
15+
- **Integer row access**: `table[0]`, `table[-1]` — access a single row by index, returns a Dict.
16+
- **Slicing**: `table[1:3]`, `table[:5]`, `table[-2:]` — row slicing backed by the C-level `TAKE` operation.
17+
- **Index list**: `table[[0, 2, 5]]` — select specific rows by position.
18+
19+
2026-02-23 | **[🔗 PyPI](https://pypi.org/project/rayforce-py/0.6.1/)** | **[🔗 GitHub](https://github.com/RayforceDB/rayforce-py/releases/tag/0.6.1)**
20+
21+
822
## **`0.6.0`**
923

1024
### New Features

docs/docs/content/documentation/data-types/table/access-values.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,47 @@ Vector([I64(29), I64(34), I64(41)])
3535

3636
```python
3737
>>> table[["id", "name"]]
38-
Table(columns=['id', 'name'])
38+
Table[Symbol('id'), Symbol('name')]
39+
```
40+
41+
**Single row by index** - returns a [:material-code-braces: Dict](../dict.md):
42+
43+
```python
44+
>>> table[0]
45+
Dict({'id': '001', 'name': 'alice', 'age': 29})
46+
47+
>>> table[-1] # last row
48+
Dict({'id': '003', 'name': 'charlie', 'age': 41})
49+
```
50+
51+
**Row slicing** - returns a new [:octicons-table-24: Table](overview.md). Uses the C-level `TAKE` operation:
52+
53+
```python
54+
>>> table[1:3] # rows at index 1 and 2
55+
>>> table[:2] # first 2 rows
56+
>>> table[-2:] # last 2 rows
57+
>>> table[2:] # from index 2 to end
58+
```
59+
60+
**Filter by expression** - returns a new [:octicons-table-24: Table](overview.md) with matching rows:
61+
62+
```python
63+
>>> from rayforce import Column
64+
65+
>>> table[Column("age") > 35]
66+
```
67+
68+
Expressions can be combined with `&` (and) and `|` (or):
69+
70+
```python
71+
>>> table[(Column("age") > 30) & (Column("name") == "charlie")]
72+
>>> table[(Column("age") < 30) | (Column("age") > 40)]
73+
```
74+
75+
**Select rows by index list** - returns a new [:octicons-table-24: Table](overview.md):
76+
77+
```python
78+
>>> table[[0, 2]] # rows at index 0 and 2
3979
```
4080

4181
## Access a Specific Column

rayforce/types/table.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,13 +403,70 @@ def __len__(self) -> int:
403403
).value
404404

405405
@DestructiveOperationHandler()
406-
def __getitem__(self, key: str | list[str]) -> Vector | List | Table:
406+
def __getitem__(
407+
self,
408+
key: str | int | slice | list | Expression | Column,
409+
) -> Vector | List | Table | Dict:
407410
if isinstance(key, str):
408411
return self.at_column(key)
412+
413+
if isinstance(key, Expression):
414+
return t.cast("Table", self).select("*").where(key).execute()
415+
416+
if isinstance(key, int):
417+
return self.at_row(key)
418+
419+
if isinstance(key, slice):
420+
if key.step is not None:
421+
raise errors.RayforceIndexError("Slice step is not supported")
422+
423+
start, stop = key.start, key.stop
424+
425+
# table[-n:] → tail
426+
if start is not None and start < 0 and stop is None:
427+
return t.cast("Table", self).take(start)
428+
429+
# table[:n] → head
430+
if start in (None, 0) and stop is not None and stop > 0:
431+
return t.cast("Table", self).take(stop)
432+
433+
# table[a:b] → take with offset
434+
if start is None:
435+
start = 0
436+
if stop is None:
437+
stop = len(self)
438+
if start < 0 or stop < 0:
439+
length = len(self)
440+
if start < 0:
441+
start = max(length + start, 0)
442+
if stop < 0:
443+
stop = max(length + stop, 0)
444+
445+
return t.cast("Table", self).take(max(stop - start, 0), offset=start)
446+
447+
# List indexing: list[str] → column select, list[int] → row select
409448
if isinstance(key, list):
410-
return t.cast("Table", self).select(*key).execute()
411-
raise errors.RayforceConversionError(
412-
f"Key must be a string or list of strings, got {type(key).__name__}"
449+
if not key:
450+
raise errors.RayforceIndexError("Cannot index with an empty list")
451+
if isinstance(key[0], str):
452+
return t.cast("Table", self).select(*key).execute()
453+
if isinstance(key[0], int):
454+
col_names = self.columns()
455+
col_values = self.values()
456+
new_data: dict[str, Vector] = {}
457+
for i, name_sym in enumerate(col_names):
458+
name = name_sym.value if hasattr(name_sym, "value") else str(name_sym)
459+
new_data[name] = utils.eval_obj(
460+
List([Operation.AT, col_values[i], Vector(items=key, ray_type=I64)])
461+
)
462+
return Table(new_data)
463+
raise errors.RayforceTypeError(
464+
f"List index elements must be str or int, got {type(key[0]).__name__}"
465+
)
466+
467+
raise errors.RayforceTypeError(
468+
f"Invalid index type: {type(key).__name__}. "
469+
f"Use str, int, slice, list, Expression, Column, or boolean Vector."
413470
)
414471

415472
@DestructiveOperationHandler()

0 commit comments

Comments
 (0)