In a simple rostering problem, I want OptaPlanner to return "holes" in my schedule so certain shifts are uncovered if no employees are available with the required skill.
Assume 3 basic classes and that I want to penalize uncovered shifts only with HardMediumSoftScore.ONE_SOFT
.
How do I write such a constraint?
Employee.java
public class Employee {
private long id = 0L;
private List<Skill> skills;
Shift.java
@PlanningEntity
public class Shift {
private RequiredSkills
@PlanningVariable(valueRangeProviderRefs = "employeeRange")
private Employee employee;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
private OffsetDateTime start;
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssXXX")
private OffsetDateTime end;
Schedule.java
@PlanningSolution
public class Schedule {
@PlanningEntityCollectionProperty
private List<Shift> shifts;
@ProblemFactCollectionProperty
@ValueRangeProvider(id = "employeeRange")
private List<Employee> employees;
Assume further a simple ConstraintProvider
public class ScheduleConstraints implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{
requiredSkill(constraintFactory),
};
}
private Constraint requiredSkill(ConstraintFactory constraintFactory) {
return constraintFactory.from(Shift.class)
.filter(shift -> {
return !shift.getEmployee().hasSkills(shift.getSkills());
}).penalize("Required skill", HardMediumSoftScore.ONE_HARD);
}
Here is my attempt. Is it good?
private Constraint allShiftsMustBeCovered(ConstraintFactory constraintFactory) {
return constraintFactory.fromUnfiltered(Shift.class)
.filter(shift-> {
return shift.getEmployee().getId() == 0L;
}).penalize("All shifts must be covered", HardMediumSoftScore.ONE_SOFT);
}
Unless I'm mistaken, your code will throw NullPointerException
s. If a shift is not covered, Shift.employee
is null
. Your constraints assume that Employee
is never null
- you'll have to fix that, perhaps like this:
// I added a null check as from() only excludes uninitialized entities.
// If employee is nullable, from() will still return Shifts with null employees.
private Constraint requiredSkill(ConstraintFactory constraintFactory) {
return constraintFactory.from(Shift.class)
.filter(shift -> shift.getEmployee() != null)
.filter(shift -> {
return !shift.getEmployee().hasSkills(shift.getSkills());
}).penalize("Required skill", HardMediumSoftScore.ONE_HARD);
}
private Constraint allShiftsMustBeCovered(ConstraintFactory constraintFactory) {
return constraintFactory.fromUnfiltered(Shift.class)
.filter(shift-> shift.getEmployee() == null ||
shift.getEmployee().getId() == 0L
).penalize("All shifts must be covered", HardMediumSoftScore.ONE_SOFT);
}
As null
is a valid value here, make sure that Shift.employee
variable is nullable. This approach is known in the docs as overconstrained planning.