SkillLayer.slots#

Turns a Pool of workers and a list of Requirements into the Slots that Project#suggestAssignments takes. Existing active assignments are bound to the slots, so repeated runs stay stable after a commit. Needs a solved project with no unsolved edits.

Signature#

public static Vec<Slot> slots(Project project, Pool pool, Vec<Requirement> requirements)

Synchronous and CPU-only. Reads the project's accepted state through Project#roster and never modifies the project. Not synchronized: don't call it while another thread uses the same Project.

Parameters#

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

Returns#

A new Vec<Slot>: for each requirement, count slots, in requirement order, then instance order. Slot fields:

Field Value
id Id.str("<requirement id>#<n>"), n = 1..count (for example be#1, be#2), built by slotId
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 null

On the next call, an assignment created from a slot binds back to that slot by id. Keep each requirement id on the same task; see Requirement.

suggestAssignments takes the result 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 non-null 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. An empty list leaves every slot empty.
  3. Matching. A deterministic maximum bipartite matching binds the remaining assignments to the empty slots of requirements whose assignments is null. 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#

All exceptions are unchecked. The first failing check wins: the project state, then the pool, then each requirement's fields in input order, then the slot limit, then binding per task.

Error When
MatchException.Project The project is unsolved or has unsolved edits, for example after apply. code() returns ProjectError.NO_BASELINE; error holds the ProjectException
MatchException.Invalid The pool or a requirement is invalid. path names the item, detail the reason
InputException (VALIDATION, path null) The summed count of all requirements reaches Integer.MAX_VALUE
OutOfMemoryError The summed count is below that limit but too large to allocate
NullPointerException project, pool or requirements is null

MatchException.Invalid paths:

Path When
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 detail names the other requirement when it already holds the assignment)

There's no STALE_BASELINE: the call takes no baseline token.

MatchException isn't a PlenException; toPlenException() converts it to the matching InputException or ProjectException. Its simple name clashes with java.lang.MatchException, so code that catches it needs import org.dhtmlx.plen.MatchException; next to import org.dhtmlx.plen.*;.

Collection overload#

public static Vec<Slot> slots(Project project, Pool pool, java.util.Collection<Requirement> requirements)

Copies requirements into a Vec and calls the main form, so List.of(...) works. Same behavior and errors.

SkillLayer.slotId#

public static String slotId(Id requirement, long n)

Returns the id of a requirement's n-th slot: the requirement id's key(), #, then n (slotId(Id.str("be"), 2) is "be#2"). slots uses it for every slot id, and an assignment created from a slot carries the same id. n isn't checked.

SkillLayer.validatePool#

public static void validatePool(Roster roster, Pool pool)

Runs the pool checks of slots alone against a Roster from Project#roster. Returns normally when the pool is valid.

Throws MatchException.Invalid with the pool.workers[...] paths listed under Errors, and NullPointerException when roster or pool is null.

Example#

import java.util.List;
import org.dhtmlx.plen.*;

public class SlotsExample {
  public static void main(String[] args) throws Exception {
    ProjectData data = new ProjectData()
        .withTasks(List.of(new TaskData(Id.str("B"))
            .withStartDate(Moment.fromYmdHm(2026, 8, 10, 8, 0))
            .withDuration(2.0)))
        .withResources(List.of(
            new ResourceData().withId(Id.str("ann")),
            new ResourceData().withId(Id.str("bob"))));

    Project project = new Project();
    project.solve(data, new SolveOptions());

    Pool pool = new Pool().withWorkers(List.of(
        new Worker().withResource(Id.str("ann"))
            .withSkills(List.of(new Skill().withId(Id.str("backend")).withValue(1.0))),
        new Worker().withResource(Id.str("bob"))
            .withSkills(List.of(new Skill().withId(Id.str("backend")).withValue(2.0)))));
    Requirement requirement = new Requirement(Id.str("be"), Id.str("B"),
        List.of(SkillMatch.atLeast(Id.str("backend"), 2.0)));

    Vec<Slot> slots = SkillLayer.slots(project, pool, List.of(requirement));
    Slot slot = slots.get(0);
    System.out.println(slot.id + " on " + slot.task + ", allowed " + slot.allowed);
    // be#1 on B, allowed [bob]

    SuggestResult proposal = project.suggestAssignments(slots, new SuggestOptions());
    project.apply(proposal.changes);
  }
}

For the full workflow, see Skills and requirements.