Documentation

Joiners

Control which tuples are created when joining constraint streams.

Joiners define the matching criteria when two streams are joined. Without joiners, a join produces every possible pair; joiners filter this down to only relevant combinations.

Available Joiners

equal

For symmetric self-joins, takes a single key extractor:

equal(|shift: &Shift| shift.employee_idx)

equal_bi

For cross-joins or directed projected self-joins, takes separate left and right key extractors:

equal_bi(|shift: &Shift| shift.employee_idx, |u: &Unavailability| u.employee_idx)

less_than

Matches when the left value is less than the right. Takes two extractors.

less_than(|a: &Shift| a.id, |b: &Shift| b.id)

greater_than

Matches when the left value is greater than the right. Takes two extractors.

greater_than(|a: &Shift| a.priority, |b: &Shift| b.priority)

overlapping

Matches when two ranges overlap. Takes four extractors: start and end for each side.

overlapping(
    |a: &Shift| a.start_time, |a: &Shift| a.end_time,
    |b: &Shift| b.start_time, |b: &Shift| b.end_time,
)

filtering

A general-purpose joiner that uses a predicate over both elements.

filtering(|a: &Shift, b: &Shift| a.location.distance_to(&b.location) < 50.0)

Using Joiners with join

Self-join - join the same generated source on the right side:

type Streams = ConstraintFactory<Schedule, HardSoftScore>;

Streams::new()
    .for_each(Schedule::shifts())
    .join((
        Streams::new().for_each(Schedule::shifts()),
        equal_bi(
            |left: &Shift| left.employee_idx,
            |right: &Shift| right.employee_idx,
        ),
    ))

Cross-join - pass a tuple of (stream, joiner):

type Streams = ConstraintFactory<Schedule, HardSoftScore>;

Streams::new()
    .for_each(Schedule::shifts())
    .join((
        Streams::new().for_each(Schedule::unavailability()),
        equal_bi(|shift: &Shift| shift.date, |u: &Unavailability| u.date),
    ))

After a cross join, the stream can score pairs directly, group the joined pairs with .group_by(|left, right| key, collector), or project each pair into a retained scoring row with .project(|left, right| row).

After a projected stream, join(equal(|row| key)) creates a symmetric projected self-join. join(equal_bi(left_key, right_key)) creates a directed projected self-join where row orientation is part of the rule.

Performance Note

Indexed joiners (equal, equal_bi, less_than, greater_than, overlapping) are much faster than filtering because they use index lookups instead of iterating all pairs. Prefer indexed joiners where possible and only use filtering for conditions that can’t be expressed with indexed joiners.

Low-level joined filters receive semantic source indexes in the current runtime. The fluent filtering(|left, right| ...) and .filter(|left, right| ...) closures remain value-oriented.

See Also