slots#

Turns a Pool of workers and a list of Requirements into the Slots that Project::suggest_assignments takes. Existing active assignments are bound to the slots, so repeated runs stay stable after a commit. Part of the plen-assign crate.

cargo add plen-assign

Signature#

pub fn slots(
    project: &Project,
    pool: &Pool,
    requirements: &[Requirement],
) -> Result<Vec<Slot>, MatchError>

Synchronous. Reads the project's accepted state through Project::roster and never modifies the project.

Parameters#

Name Type Description
project &Project A solved project with no unsolved edits. Tasks, resources and assignments are checked against its accepted state.
pool &Pool Workers that may be assigned. Pool order sets the order of each slot's allowed.
requirements &[Requirement] What each task needs. An empty slice returns an empty Vec.

Returns#

Ok(Vec<Slot>): for each requirement, count slots, in requirement order, then instance order. Each slot is filled in as follows:

Field Value
id "<requirement id>#<n>", n = 1..=count (for example be#1, be#2)
task The requirement's task
allowed Pool workers for which every SkillMatch of the requirement holds, in pool order. No matches allows every worker.
value The requirement's units
assignment The existing active assignment bound to this slot, or None

On the next call, an assignment created from a slot binds back to that slot by id; see Requirement for why a requirement id must stay on its task.

The result can be passed to suggest_assignments as is.

Binding#

The task's existing active assignments are bound per task, across all requirements on that task, in three steps:

  1. Slot id. An assignment whose id is one of the requirement's slot ids binds to that slot, even when its resource isn't in allowed. The slot is then broken (AssignTarget::Broken). This step runs for every requirement of the task before any explicit list.
  2. Explicit list. Requirement::assignments: Some(list) replaces automatic binding for that requirement. A listed slot-id assignment keeps its slot, the other entries take the remaining slots in order, and leftover slots stay empty. Some(vec![]) leaves every slot empty.
  3. Matching. A deterministic maximum bipartite matching binds the remaining assignments to the empty slots of requirements without a list. An assignment can take a slot only when its resource is in the slot's allowed. Input order decides between equal matchings, and an earlier requirement can't take the only worker a later one fits.

Assignments left unbound stay on the task without a slot.

Errors#

Returns MatchError. The first failing check wins: the project state, then the pool, then each requirement in input order.

Error When it occurs
MatchError::Project(ProjectError::NoBaseline) The project is unsolved or has unsolved edits (after apply).
MatchError::Invalid The pool or a requirement is invalid. path names the item.

MatchError::Invalid paths:

Path When it occurs
pool.workers[i].resource Unknown resource, or the same resource listed as two workers
pool.workers[i].skills[j].id The same skill listed twice on one worker
pool.workers[i].skills[j].value Not finite
requirements[i].id Duplicate requirement id, or one of its slot ids is an active assignment on another task
requirements[i].task Unknown or unscheduled task
requirements[i].count 0
requirements[i].units Not finite or <= 0
requirements[i].matches[j].value Not finite
requirements[i].assignments More entries than count, or an assignment with one of this requirement's slot ids left out of the list
requirements[i].assignments[k] Unknown or inactive assignment, an assignment on another task, or one bound twice (the message names the other requirement when it already holds it)

There's no StaleBaseline: the function takes no baseline token.

Example#

use plen_assign::{slots, Pool, Requirement, Skill, SkillMatch, Worker};
use plen_core::{
    DateTime, Project, ProjectData, ResourceData, SolveOptions, SuggestOptions, TaskData,
};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut task = TaskData::new("B");
    task.start_date = DateTime::from_ymd_hm(2026, 8, 10, 8, 0);
    task.duration = Some(2.0);

    let resource = |id: &str| ResourceData { id: id.into(), capacity: None, calendar_id: None };
    let data = ProjectData {
        tasks: vec![task],
        resources: vec![resource("ann"), resource("bob")],
        ..Default::default()
    };

    let mut project = Project::default();
    project.solve(Some(data), &SolveOptions::default())?;

    let pool = Pool {
        workers: vec![
            Worker { resource: "ann".into(), skills: vec![Skill { id: "backend".into(), value: 1.0 }] },
            Worker { resource: "bob".into(), skills: vec![Skill { id: "backend".into(), value: 2.0 }] },
        ],
    };
    let requirements = [Requirement::new("be", "B", vec![SkillMatch::at_least("backend", 2.0)])];

    let slots = slots(&project, &pool, &requirements)?;
    // One slot "be#1" on task "B", allowed = ["bob"]

    let proposal = project.suggest_assignments(&slots, &SuggestOptions::default())?;
    project.apply(&proposal.changes)?;
    Ok(())
}

For the full workflow, see Skills and requirements.