optaplannertimefold

Timefold consecutive collectors constraint


I'm trying to learn how to use the different types of building blocks used for constraint streams in Timefold. To do this, I'm using the Employee Scheduling example as a base and modifying it. I want to add a constraint that limits the number of consecutive days an employee can work, i.e. be assigned to a shift, considering that a shift belongs to the day in which its start time is located. The limit of consecutive working days is set to 4.

I wrote this code for the constraint, following the example of consecutive matches in section 4.5.7 of the Score Calculation section of the timefold documentation

    Constraint maxConseutiveDays(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Shift.class)
                .filter(shift -> shift.getEmployee() != null)
                .groupBy(Shift::getEmployee, 
                        ConstraintCollectors.toConsecutiveSequences(Shift::getStart, Shift::getId))
                .flattenLast(SequenceChain::getConsecutiveSequences)
                .filter((employee, shifts) -> shifts.getCount() > 4)
                .penalize(HardSoftScore.ONE_HARD)
                .asConstraint("Maximum consecutive working days exceeded");
    }

However, I keep getting build errors, particularly one that says no suitable method found for toConsecutiveSequences(Shift::getStart,Shift::getId). Could someone give me more details about the arguments expected by the toConsecutiveSequences function or something that will help me find the error?


Solution

  • The compiler is telling you that there is no method toConsecutiveSequences on the class ConstraintCollectors which would accept these parameters. If we look at the class, we see that it is true; your filter produces a UniConstraintStream and for that, the only available method is:

    <A> UniConstraintCollector<A, ?, SequenceChain<A, Integer>> toConsecutiveSequences(ToIntFunction<A> indexMap) {
        ...
    }
    

    As you can see, this method only accepts one argument. You are giving it two. Your groupBy shoud look like this instead:

    .groupBy(Shift::getEmployee,  ConstraintCollectors.toConsecutiveSequences(Shift::getId))
    

    As I reviewed your constraint, I also discovered a score trap in your definition. You always want to penalize more for larger infringements, to help the solver distinguish solutions from one another. Having fixed that, your constraint would look like so:

    Constraint maxConseutiveDays(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Shift.class)
                .filter(shift -> shift.getEmployee() != null)
                .groupBy(Shift::getEmployee,
                         ConstraintCollectors.toConsecutiveSequences(Shift::getId))
                .flattenLast(SequenceChain::getConsecutiveSequences)
                .filter((employee, shifts) -> shifts.getCount() > 4)
                .penalize(HardSoftScore.ONE_HARD, (employee,  shifts) -> shifts.getCount())
                .asConstraint("Maximum consecutive working days exceeded");
    }