# tals — Time-Aware List Slicer

`tals` makes time-based slicing as natural as ordinary indexing. It extends Python's `[start:end]` syntax with datetime and calendar-period bounds, and works on any list of objects — it only needs to know which attribute holds the timestamp.


```python
from tals import slice_objects

# Last 7 days
slice_objects(entries, "-7d:", "created_at", reference_dt)

# This calendar month, excluding the last entry
slice_objects(entries, "0M:-1", "created_at", reference_dt)

# February only
slice_objects(entries, "-1M:0M", "created_at", reference_dt)
```

## Installation

```
pip install tals
```

## Overview

`tals` extends Python's familiar `[start:end]` slice syntax with two additional bound types:

- **Time-delta bounds** — relative to a reference datetime: `-7d`, `+2h`, `-30m`
- **Calendar-period bounds** — snapped to period boundaries: `0M`, `-1W`, `0Y`

The library has no domain knowledge. It works on any list of objects and only needs the name of the attribute that holds each object's timestamp.

## API

```python
def slice_objects(
    objects: list[Any],
    position: str,
    timestamp_key: str,
    reference_dt: datetime,
    week_start: int = 0,
) -> Any | list[Any]:
```

| Parameter | Description |
|-----------|-------------|
| `objects` | List of arbitrary objects in any order |
| `position` | Slice expression string (see syntax below) |
| `timestamp_key` | Attribute name holding each object's `datetime` value |
| `reference_dt` | The "now" anchor for all relative expressions — the library never calls `datetime.now()` itself |
| `week_start` | First day of the week: `0` = Monday (default), `6` = Sunday |

**Return value:** a single-index expression returns one object (or `None` if out of bounds); a slice expression returns a list (possibly empty).

The list is stable-sorted ascending by `timestamp_key` before slicing. All indices operate on this sorted list.

## Syntax

```
[index]
[start:end]
[start:]
[:end]
[:]
```

### Bound types

| Form | Description | Example |
|------|-------------|---------|
| `0`, `1`, `-1` | Integer index — same semantics as Python | `[-1]` → last object |
| `-7d`, `+2h`, `-30m` | Time-delta — relative to `reference_dt` | `[-7d:]` → last 7 days |
| `0M`, `-1M`, `+1M` | Calendar month — start of the Nth month | `[0M:]` → this month onward |
| `0W`, `-1W` | Calendar week — start of the Nth week | `[-1W:0W]` → last week |
| `0Y`, `-1Y` | Calendar year — Jan 1 of the Nth year | `[0Y:]` → this year onward |

Period `0` is the period containing `reference_dt`; `-1` is the previous period; `+1` is the next.

Calendar bounds are not valid as a single index — they only make sense in a slice.

### Inclusivity

All bounds follow Python's convention: **start is inclusive, end is exclusive**.

| Expression | Meaning |
|------------|---------|
| `[-7d:]` | `timestamp >= reference_dt − 7 days` |
| `[:-7d]` | `timestamp < reference_dt − 7 days` |
| `[-1M:0M]` | `timestamp >= start of last month` and `< start of this month` |

### Mixed bounds

A slice can mix bound types. Time/calendar bounds are resolved to datetime thresholds first, the list is filtered, then integer bounds are applied to the filtered result.

```python
# March entries, excluding the last one
slice_objects(entries, "0M:-1", "created_at", reference_dt)
# → filter to [Mar 1, Apr 1), then apply [:-1]

# From 7 days ago, drop the trailing entry
slice_objects(entries, "-7d:-1", "created_at", reference_dt)
```

## Examples

Given four objects with a `start` attribute and `reference_dt = 2026-03-10`:

| Object | `start` |
|--------|---------|
| A | 2026-01-10 |
| B | 2026-02-05 |
| C | 2026-03-01 |
| D | 2026-03-10 |

```python
slice_objects(objs, "[-1]",     "start", ref)  # → D
slice_objects(objs, "[0]",      "start", ref)  # → A
slice_objects(objs, "[:]",      "start", ref)  # → [A, B, C, D]
slice_objects(objs, "[:-1]",    "start", ref)  # → [A, B, C]
slice_objects(objs, "[0M:]",    "start", ref)  # → [C, D]       (March)
slice_objects(objs, "[-1M:0M]", "start", ref)  # → [B]          (February)
slice_objects(objs, "[-1M:]",   "start", ref)  # → [B, C, D]    (Feb 1 onward)
slice_objects(objs, "[0Y:]",    "start", ref)  # → [A, B, C, D] (all of 2026)
slice_objects(objs, "[0M:-1]",  "start", ref)  # → [C]          (March, drop last)
```

## Timezone handling

`reference_dt` and all object timestamps must be either all timezone-aware or all timezone-naive — mixing the two raises `TypeError`. Objects in different (but both aware) timezones are compared correctly by Python and are fully supported.

Calendar boundaries are computed in the timezone of `reference_dt`, so `0M` on a `UTC+02:00` reference resolves to midnight of the 1st in that timezone.

## Out of scope

`tals` is a pure slicing primitive. The following are the caller's responsibility:

- **Pre-filtering** — pass only the subset of objects that should be considered
- **Field extraction** — read attributes from the returned objects
- **Fallback values** — convert `None` or `[]` to domain-specific defaults

## License

MIT
