Requirement#

What one task needs: Count slots on Task, each for a worker that satisfies every SkillMatch in Matches. Pass a list of requirements to SkillLayer.Slots, which turns each one into Slots. Create one with the constructor and set the other properties in an object initializer.

Declaration#

namespace Plen.Assign;

public sealed class Requirement : IDeepCloneable<Requirement>, IEquatable<Requirement>

Constructors#

Requirement(Id, Id, List<SkillMatch>)#

public Requirement(Id id, Id task, List<SkillMatch> matches)

Creates a requirement for one slot at Units = 1.0 with automatic binding (Count = 1, Assignments = null). It's the only constructor, so new Requirement { … } without arguments doesn't compile.

Name Type Description
id Id Requirement id.
task Id The task the slots belong to.
matches List<SkillMatch> The skill matches every candidate must satisfy. The list is stored as passed, not copied.

Properties#

Property Type Default Description
Id Id set by the constructor Requirement id; unique within one Slots call. Slot ids are "<Id>#<n>", n = 1..Count.
Task Id set by the constructor A scheduled leaf task of the project.
Matches List<SkillMatch> set by the constructor Every match must hold for a worker to be a candidate. An empty list allows every pool worker.
Count uint 1 Number of slots. At least 1.
Units double 1.0 Rate of a new assignment (1 = 100 %); becomes each slot's Value. Finite and > 0.
Assignments List<Id>? null Explicit binding of existing assignments. null = automatic binding; [] = every slot empty.

Id#

public required Id Id { get; set; }

The requirement's slot ids become the ids of the assignments that fill them on Task. After those assignments are committed, the id stays tied to that task: reusing it for a requirement on another task throws.

Assignments#

public List<Id>? Assignments { get; set; }

A non-null list replaces automatic binding for this requirement. An assignment whose id is one of this requirement's slot ids keeps its slot. The other listed assignments take the remaining slots in list order, and slots left over stay empty. The list can't hold more entries than Count. Each entry must be an active assignment on Task, and a slot-id assignment of this requirement must be listed. For how binding works across all requirements of a task, see Binding existing assignments.

Methods#

DeepClone#

public Requirement DeepClone()

Returns a copy that shares no list or SkillMatch with this one.

Equals#

public bool Equals(Requirement? other)
public override bool Equals(object? obj)

Value equality over all six properties: lists element by element, double by ==. A null Assignments equals only another null, not []. GetHashCode is consistent with it.

ToString#

public override string ToString()

Returns a readable rendering of all properties, for logging.

SkillMatch#

One condition on a worker's skill: worker value <Op> Value. A skill the worker doesn't list counts as 0, so SkillMatch.NoMore("qa", 0) holds for every worker without the qa skill.

namespace Plen.Assign;

public sealed class SkillMatch : IDeepCloneable<SkillMatch>, IEquatable<SkillMatch>
{
    public required Id Skill { get; set; }
    public SkillOp Op { get; set; }
    public double Value { get; set; }

    public static SkillMatch AtLeast(Id skill, double value);
    public static SkillMatch NoMore(Id skill, double value);
    public static SkillMatch Equal(Id skill, double value);
}
Member Type Default Description
Skill Id required Skill id, compared with Skill.Id.
Op SkillOp SkillOp.AtLeast The comparison.
Value double 1.0 The value to compare with. Must be finite.
AtLeast(skill, value) SkillMatch A match with Op = SkillOp.AtLeast.
NoMore(skill, value) SkillMatch A match with Op = SkillOp.NoMore.
Equal(skill, value) SkillMatch A match with Op = SkillOp.Equal.

The class has a parameterless constructor, so new SkillMatch { Skill = "backend" } gives worker value >= 1. DeepClone, Equals (all three properties, double by ==), GetHashCode and ToString work as on Requirement.

SkillOp#

The comparison a SkillMatch applies. All three are inclusive.

namespace Plen.Assign;

public enum SkillOp { AtLeast, NoMore, Equal }
Value Description
AtLeast Worker value >= match value. The default.
NoMore Worker value <= match value.
Equal Worker value == match value.

Errors#

Requirements are validated by SkillLayer.Slots. It throws InputException with Kind = InputErrorKind.Validation and one of these paths for requirement i:

Path When
requirements[i].id The id repeats an earlier requirement's id, or one of its 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 is NaN, infinite or <= 0.
requirements[i].matches[j].value A match Value is NaN or infinite.
requirements[i].assignments The list has more entries than Count, or leaves out an assignment that has one of this requirement's slot ids.
requirements[i].assignments[k] The entry is unknown or inactive, is on another task, or is bound twice (also when another requirement already bound it).

Example#

Two half-time slots for workers with backend grade 1 or higher:

using Plen;
using Plen.Assign;

var monday = Moment.FromYmdHm(2026, 8, 10, 8, 0) ?? throw new InvalidOperationException("invalid date");

var project = new Project();
project.Solve(new ProjectData
{
    Tasks = [new TaskData("api") { StartDate = monday, Duration = 2 }],
    Resources = [new ResourceData { Id = "ann" }, new ResourceData { Id = "bob" }, new ResourceData { Id = "cat" }],
}, new SolveOptions());

var pool = new Pool
{
    Workers =
    [
        new Worker { Resource = "ann", Skills = [new Skill { Id = "backend", Value = 2 }] },
        new Worker { Resource = "bob", Skills = [new Skill { Id = "backend" }] },
        new Worker { Resource = "cat" },
    ],
};

var requirement = new Requirement("be", "api", [SkillMatch.AtLeast("backend", 1)])
{
    Count = 2,
    Units = 0.5,
};

foreach (var slot in SkillLayer.Slots(project, pool, [requirement]))
{
    Console.WriteLine($"{slot.Id}: allowed [{string.Join(", ", slot.Allowed)}], value {slot.Value}");
}

Output:

be#1: allowed [ann, bob], value 0.5
be#2: allowed [ann, bob], value 0.5