javaoptaplannertimefold

How to sum up the shift durations for a week


I have a problem with fact collection DaySetting.

public class DaySetting {
    String businessDate;
    Integer weekNo;
    Integer monthNo;
}

I have another class shift:

public class Shift {
    @PlanningId
    private String id;

    private LocalDateTime start;
    private LocalDateTime end;

     public Long getShiftDuration() {
        long minutes = ChronoUnit.MINUTES.between(start, end);
        
        return minutes;
    }
    public String getStartDate() {
     DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
     String dateStr = getStart().toLocalDate().format(formatter);
     return dateStr;
   }
}

The day setting can be taken the values like:

public class DaySetting {
    String businessDate = 2024-06-01;
    Integer weekNo = 1; // first week of June
    Integer monthNo = 6;//June
}
public class DaySetting {
    String businessDate = 2024-06-02;
    Integer weekNo = 1; // first week of June
    Integer monthNo = 6;//June
}
public class DaySetting {
    String businessDate = 2024-06-09;
    Integer weekNo = 2; // Second week of June
    Integer monthNo = 6;//June
}

public class DaySetting {
    String businessDate = 2024-06-10;
    Integer weekNo = 2; // second week of June
    Integer monthNo = 6;//June
}

I have to sum up the duration of the work based on weekNo value. for WeekNo 1 there are 2 shifts, it will be summed up. The first two shift durations will be summed up for week 1,

The later two shifts will be summed up for the week 2.

So, far what I tried below:

Constraint workingHoursLessThanMaximumInContractPerWeek(ConstraintFactory constraintFactory) {
        return  constraintFactory.forEach(DaySetting.class)
                .join(Shift.class)
                .groupBy((daySetting, shift) -> day.getWeekNo(),
                         ConstraintCollectors.sumLong((daySetting, shift) -> shift.getShiftDuration()))
//Shift don't have weekNo field. It only have getStartDate() to get the business date, 
//so we have to take the date value from DaySettings class and match with the date of Shift class. How to achieve this with the constraints.
                .penalize(HardMediumSoftScore.ONE_SOFT, ((weekNo, sumValue))->{
                    return sumValue;
                })
                .asConstraint("Working Hours Greater Than Maximum In Contract Per Week");
    }

Solution

  • I would recommend putting DaySetting in Shift to avoid ambiguities arising from Shifts spanning multiple days (such as night shifts). Additionally, year should also be included, otherwise issues can arise when planning schedules across year boundaries. I would create a WeekIdentifier record that maps a LocalDate to a Year + Week of year:

    public record WeekIdentifier(long year, long weekInYear) {
        private final static WeekFields WEEK_DEFINITION =   WeekFields.of(DayOfWeek.MONDAY, 7);
        
        public static WeekIdentifier forDate(LocalDate date) {
            return new WeekIdentifier(WEEK_DEFINITION.weekBasedYear().getFrom(date),
                                      WEEK_DEFINITION.weekOfWeekBasedYear().getFrom(date));
        }
    } 
    

    Then I would add a method for getting the WeekIdentifer of a Shift:

    // Assumption, Shift is a @PlanningEntity with a @PlanningVariable employee
    @PlanningEntity
    public class Shift {
        @PlanningId
        private String id;
    
        private LocalDateTime start;
        private LocalDateTime end;
        
        @PlanningVariable
        private Employee employee;
    
         public Long getShiftDuration() {
            long minutes = ChronoUnit.MINUTES.between(start, end);
            
            return minutes;
        }
        
        public WeekIdentifier getWeekIdentifier() {
            return WeekIdentifier.forDate(getStart().toLocalDate());
        }
        
        public Employee getEmployee() {
            return employee;
        }
    }
    

    Then I would write the Constriant like this:

    Constraint workingHoursLessThanMaximumInContractPerWeek(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Shift.class)
                   .groupBy(Shift::getEmployee,
                            Shift::getWeekIdentifier,
                            ConstraintCollectors.sumLong(Shift::getShiftDuration))
                   .filter((employee, week, minutes) -> minutes > employee.getMaximumMinutesPerWeek())
                   .penalize(HardMediumSoftScore.ONE_SOFT, (employee, week, minutes) -> {
                       return minutes - employee.getMaximumMinutesPerWeek();
                   })
                   .asConstraint("Working Hours Greater Than Maximum In Contract Per Week");
    }