Requirement#

The staffing need of one task: count slots, each filled by a worker who satisfies every SkillMatch in matches. Requirements are the third argument of slots. Create one with Requirement::new and change count, units or assignments on the returned value, or write a struct literal.

Requirements aren't stored in the project. Pass them again on every slots call.

Declaration#

#[derive(Clone, Debug, PartialEq)]
pub struct Requirement {
    pub id: Id,
    pub task: Id,
    pub matches: Vec<SkillMatch>,
    pub count: u32,
    pub units: f64,
    pub assignments: Option<Vec<Id>>,
}

Requirement has no Default and doesn't implement Serialize / Deserialize.

Constructors#

new#

pub fn new(id: impl Into<Id>, task: impl Into<Id>, matches: Vec<SkillMatch>) -> Requirement

Creates a requirement for one slot at units 1.0, with automatic binding.

Parameters

Name Type Description
id impl Into<Id> Requirement id. Slot ids are built from it.
task impl Into<Id> The task that needs the work.
matches Vec<SkillMatch> Skill conditions. All of them must hold.

Returns a Requirement with count: 1, units: 1.0 and assignments: None.

new doesn't validate anything. slots checks the fields.

Fields#

Name Type Default via new Description
id Id - Unique among the requirements of one slots call. The slot ids are "<id>#<n>", n = 1..=count.
task Id - A scheduled leaf task of the project.
matches Vec<SkillMatch> - Conditions a worker must meet, all of them. An empty list accepts every pool worker.
count u32 1 Number of slots. At least 1.
units f64 1.0 The value of each slot: the units of a new assignment that fills it. Finite and > 0.
assignments Option<Vec<Id>> None Which existing active assignments of task fill the slots. See below.

id stays on its task#

slots binds an assignment back to its slot by the slot id. Keep a requirement id on the same task across runs: if one of its slot ids is an active assignment on another task, slots fails at requirements[i].id.

assignments#

Value Effect
None Automatic binding: assignments with this requirement's slot ids, then matching of the task's other assignments by allowed.
Some(list) Explicit binding. Only the listed assignments fill slots, at most count of them. Any assignment with one of this requirement's slot ids must be in the list. Slots left over stay empty.
Some(vec![]) Every slot stays empty. Fails if an assignment with one of the slot ids exists.

The binding order across all requirements of a task is on slots.

Errors#

slots reports an invalid requirement as MatchError::Invalid at one of these paths.

Path When it occurs
requirements[i].id Another requirement in the call has the same id, or one of the slot ids is an active assignment on another task.
requirements[i].task The task is unknown or unscheduled.
requirements[i].count count is 0.
requirements[i].units units isn't finite, or is <= 0.
requirements[i].assignments The list has more entries than count, or leaves out an assignment with one of this requirement's slot ids.
requirements[i].assignments[k] The assignment is unknown or inactive, is on another task, or is bound twice. When another requirement already holds it, the message names that requirement, for example already bound to requirement "fe".

Ids compare by their Id key.

Example#

Two backend slots at half units on task api. bob already works on api, so his assignment fills the first slot.

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

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut task = TaskData::new("api");
    task.start_date = DateTime::from_ymd_hm(2026, 3, 2, 8, 0);
    task.duration = Some(4.0);
    let data = ProjectData {
        tasks: vec![task],
        resources: ["ann", "bob"]
            .map(|id| ResourceData { id: id.into(), capacity: None, calendar_id: None })
            .to_vec(),
        assignments: vec![AssignmentData {
            id: Some("a1".into()),
            task: "api".into(),
            resource: "bob".into(),
            value: 1.0,
            offset: None,
            duration: None,
        }],
        ..Default::default()
    };
    let mut project = Project::default();
    project.solve(Some(data), &SolveOptions::default())?;

    let backend = |resource: &str| Worker {
        resource: resource.into(),
        skills: vec![Skill { id: "backend".into(), value: 1.0 }],
    };
    let pool = Pool { workers: vec![backend("ann"), backend("bob")] };

    let mut requirement = Requirement::new("be", "api", vec![SkillMatch::at_least("backend", 1.0)]);
    requirement.count = 2;
    requirement.units = 0.5;

    for slot in slots(&project, &pool, &[requirement])? {
        let bound = slot.assignment.as_ref().map(|id| id.to_string());
        println!("{}: value {}, assignment {bound:?}", slot.id, slot.value);
    }
    Ok(())
}

Output:

be#1: value 0.5, assignment Some("a1")
be#2: value 0.5, assignment None
  • SkillMatch - one condition in matches
  • Pool - the workers a requirement's matches are checked against
  • slots - turns requirements into slots
  • Slot - what each of the count instances becomes
  • Skills and Requirements - shows requirements, matches and binding of existing assignments