# `Latu.Window`
[🔗](https://github.com/zero-one-group/latu/blob/v0.4.0/lib/latu/window.ex#L1)

A window specification: how to partition, how to order, and which rows the frame covers.

    alias Latu.Window, as: W

    W.partition_by([:suburb])
    |> W.order_by([desc(:price)])
    |> W.rows_between(:unbounded_preceding, :current_row)

Alias it, as PySpark does. `order_by/2` would collide with `Latu.order_by/2` on import.

There is no window relation and no window message of its own: `Latu.Column.over/2` folds
this into an `Expression.Window` that rides inside an ordinary `Project` or `WithColumns`.
So this struct is client-side state, like `Latu.GroupedData` and `Latu.CaseWhen`, and
reaches the wire only through `Latu.Column.over/2`.

Not to be confused with **`Latu.Functions.window/2`**, Spark's tumbling *time* window, which is
an ordinary function over a timestamp column and has nothing to do with this.

## Frame boundaries

`:current_row`, `:unbounded_preceding` (lower only), `:unbounded_following` (upper only), or an
integer offset. Zero is `:current_row` — Spark has no other reading of it, and PySpark encodes
it that way, so `rows_between(0, 2)` and `rows_between(:current_row, 2)` are the same plan.

`rows_between/3` counts rows and `range_between/3` counts *values of the ordering column*, so
the latter needs exactly one `order_by` key to mean anything.

# `boundary`

```elixir
@type boundary() ::
  :current_row | :unbounded_preceding | :unbounded_following | integer()
```

The two unbounded atoms are positional; see `rows_between/3`.

# `t`

```elixir
@type t() :: %Latu.Window{
  frame: nil | {:rows | :range, boundary(), boundary()},
  orders: [Latu.Plan.sort_order()],
  partitions: [Latu.Plan.expression()] | nil
}
```

# `order_by`

```elixir
@spec order_by([term()] | term()) :: t()
```

Order within each partition. Takes column names or sort keys.

A bare name sorts ascending with nulls first, which is `Latu.Column.asc/1`'s rule and SQL's.

# `order_by`

```elixir
@spec order_by(t(), [term()] | term()) :: t()
```

Replace the ordering of an existing specification.

# `partition_by`

```elixir
@spec partition_by([term()] | term()) :: t()
```

Partition the rows. Starts a specification, or replaces the partitioning of one.

A window nobody partitioned moves every row to a single partition; `Latu.Column.over/2` says
so out loud, as PySpark does. `partition_by([])` is the explicit global window, and quiet.

A single column needs no list, as everywhere else.

# `partition_by`

```elixir
@spec partition_by(t(), [term()] | term()) :: t()
```

Replace the partitioning of an existing specification.

# `range_between`

```elixir
@spec range_between(t(), boundary(), boundary()) :: t()
```

A frame counted in values of the ordering column. `rows_between/3` counts rows instead.

# `rows_between`

```elixir
@spec rows_between(t(), boundary(), boundary()) :: t()
```

A frame counted in rows, relative to the current one.

    W.partition_by([:suburb]) |> W.rows_between(-1, 1)

Offsets are 32-bit here and 64-bit in `range_between/3`, which is Spark's asymmetry, not a
choice — see `docs/decisions.md`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
