Using the employee roster example found here, I am trying to setup another constraint/rule that would to try to assign employees to shifts based on some shift pattern (Let's assume this is fixed).
E.g.
For the Spot/zone Airport, we have a day shift (6am-18pm) and night shift (18pm- 6am). What I am trying to accomplish is to be able to specify a shift pattern that employees will work. e.g.
Day-Shift, Day Shift, Day-Shift, Night-Shift,Night-Shift,Night-Shift, Off, Off, Off
I tried to break the problem down into separate constraints as follows:
Constraint conformToShiftPattern(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(SlotDAO.class)
.join(SlotDAO.class, equal(SlotDAO::getProvisionalEmployee))
.filter((s1, s2) -> !Objects.equals(s1, s2))
.join(SlotDAO.class,
equal((s1, s2) -> s2.getProvisionalEmployee(), SlotDAO::getProvisionalEmployee))
.filter((s1, s2, s3) -> !Objects.equals(s3, s2))
.filter((s1, s2, s3) -> !ContractualRequirementUtil.isPatternMatch(s1, s2, s3))
.penalizeConfigurableLong(CONSTRAINT_DUTY_PATTERN_MATCH,
ContractualRequirementUtil::getPatternMatchPenalty);
}
public static boolean isSlotFromSameShifts(SlotDAO slot1, SlotDAO slot2, SlotDAO slot3) {
return slot1.getClientShiftId() == slot2.getClientShiftId() && slot2.getClientShiftId() == slot3.getClientShiftId();
}
Constraint noMoreThan3OfSameShifts(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(SlotDAO.class)
.join(SlotDAO.class, equal(SlotDAO::getProvisionalEmployee))
.filter((s1, s2) -> !Objects.equals(s1, s2))
.join(SlotDAO.class,
equal((s1, s2) -> s2.getProvisionalEmployee(), SlotDAO::getProvisionalEmployee))
.filter((s1, s2, s3) -> !Objects.equals(s3, s2))
.filter(ContractualRequirementUtil::isPatternMatch)
.join(SlotDAO.class,
equal((s1, s2, s3) -> s3.getProvisionalEmployee(), SlotDAO::getProvisionalEmployee))
.filter((s1, s2, s3, s4) -> Objects.equals(s4.getClientShiftId(), s3.getClientShiftId()))
.penalizeConfigurableLong(NO_MORE_THAN_3_OF_SAME_SHIFT,
(s1, s2, s3, s4) -> 8);
}
public static boolean isSlotFromSameShifts(SlotDAO slot1, SlotDAO slot2, SlotDAO slot3) {
return slot1.getClientShiftId() == slot2.getClientShiftId() && slot2.getClientShiftId() == slot3.getClientShiftId();
}
but this just doesn't even come close the output is still very random, please advise or point me in a direction.
Also not sure how to then enforce 3 days off, not sure if this is even possible or maybe to much for OptaPlanner.
Thank you in advance. I have read that maybe shadow variables could help here, but I don't see how.
The n-consecutive pattern is indeed difficult to implement in Constraint Streams. In some of the examples where we need to use it (such as Nurse Rostering), we ended up writing our own custom Consecutive Constraint Collector.
return constraintFactory.forEach(MinMaxContractLine.class)
...
.join(ShiftAssignment.class,
Joiners.equal(ContractLine::getContract, ShiftAssignment::getContract))
.groupBy((contract, shift) -> shift.getEmployee(),
(contract, shift) -> contract,
ExperimentalConstraintCollectors.consecutive((contract, shift) -> shift.getShiftDate(),
ShiftDate::getDayIndex))
.flattenLast(ConsecutiveInfo::getConsecutiveSequences)
...
.penalize("consecutiveWorkingDays", HardSoftScore.ONE_SOFT,
(employee, contract, shiftList) -> ...);
This collector then handles both sequences of shifts, and sequences of days off; with no limits on sequence length. Its source code is far too complex to paste here, you should look in the repo directly.
As a side note, OptaWeb Employee Rostering example application is abandoned and no longer maintained. I suggest you move to one of our other examples or quickstarts.