Merge pull request #15270 from ge0ffrey/timefold-solver

[GST-4] Timefold Solver article
This commit is contained in:
Liam Williams 2023-12-08 22:11:31 +00:00 committed by GitHub
commit 5d61699e3e
9 changed files with 329 additions and 0 deletions

View File

@ -895,6 +895,7 @@
<module>tensorflow-java</module>
<module>testing-modules</module>
<module>testing-modules/mockito-simple</module>
<module>timefold-solver</module>
<module>vaadin</module>
<module>vavr-modules</module>
<module>vertx-modules</module>
@ -1139,6 +1140,7 @@
<module>tensorflow-java</module>
<module>testing-modules</module>
<module>testing-modules/mockito-simple</module>
<module>timefold-solver</module>
<module>vaadin</module>
<module>vavr-modules</module>
<module>vertx-modules</module>

View File

@ -0,0 +1,6 @@
## Timefold Solver
This module contains articles about (Timefold Solver)[https://timefold.ai].
### Relevant articles

44
timefold-solver/pom.xml Normal file
View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>timefold-solver</artifactId>
<name>timefold-solver</name>
<parent>
<groupId>com.baeldung</groupId>
<artifactId>parent-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-bom</artifactId>
<version>${version.ai.timefold.solver}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-core</artifactId>
</dependency>
<dependency>
<groupId>ai.timefold.solver</groupId>
<artifactId>timefold-solver-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<version.ai.timefold.solver>1.4.0</version.ai.timefold.solver>
</properties>
</project>

View File

@ -0,0 +1,28 @@
package com.baeldung.timefoldsolver;
import java.util.Set;
public class Employee {
private String name;
private Set<String> skills;
public Employee(String name, Set<String> skills) {
this.name = name;
this.skills = skills;
}
@Override
public String toString() {
return name;
}
public String getName() {
return name;
}
public Set<String> getSkills() {
return skills;
}
}

View File

@ -0,0 +1,54 @@
package com.baeldung.timefoldsolver;
import java.time.LocalDateTime;
import ai.timefold.solver.core.api.domain.entity.PlanningEntity;
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
@PlanningEntity
public class Shift {
private LocalDateTime start;
private LocalDateTime end;
private String requiredSkill;
@PlanningVariable
private Employee employee;
// A no-arg constructor is required for @PlanningEntity annotated classes
public Shift() {
}
public Shift(LocalDateTime start, LocalDateTime end, String requiredSkill) {
this(start, end, requiredSkill, null);
}
public Shift(LocalDateTime start, LocalDateTime end, String requiredSkill, Employee employee) {
this.start = start;
this.end = end;
this.requiredSkill = requiredSkill;
this.employee = employee;
}
@Override
public String toString() {
return start + " - " + end;
}
public LocalDateTime getStart() {
return start;
}
public LocalDateTime getEnd() {
return end;
}
public String getRequiredSkill() {
return requiredSkill;
}
public Employee getEmployee() {
return employee;
}
}

View File

@ -0,0 +1,43 @@
package com.baeldung.timefoldsolver;
import java.util.List;
import ai.timefold.solver.core.api.domain.solution.PlanningEntityCollectionProperty;
import ai.timefold.solver.core.api.domain.solution.PlanningScore;
import ai.timefold.solver.core.api.domain.solution.PlanningSolution;
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
@PlanningSolution
public class ShiftSchedule {
@ValueRangeProvider
private List<Employee> employees;
@PlanningEntityCollectionProperty
private List<Shift> shifts;
@PlanningScore
private HardSoftScore score;
// A no-arg constructor is required for @PlanningSolution annotated classes
public ShiftSchedule() {
}
public ShiftSchedule(List<Employee> employees, List<Shift> shifts) {
this.employees = employees;
this.shifts = shifts;
}
public List<Employee> getEmployees() {
return employees;
}
public List<Shift> getShifts() {
return shifts;
}
public HardSoftScore getScore() {
return score;
}
}

View File

@ -0,0 +1,35 @@
package com.baeldung.timefoldsolver;
import static ai.timefold.solver.core.api.score.stream.Joiners.equal;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.score.stream.Constraint;
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
public class ShiftScheduleConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] { atMostOneShiftPerDay(constraintFactory), requiredSkill(constraintFactory) };
}
public Constraint atMostOneShiftPerDay(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Shift.class)
.join(Shift.class, equal(shift -> shift.getStart()
.toLocalDate()), equal(Shift::getEmployee))
.filter((shift1, shift2) -> shift1 != shift2)
.penalize(HardSoftScore.ONE_HARD)
.asConstraint("At most one shift per day");
}
public Constraint requiredSkill(ConstraintFactory constraintFactory) {
return constraintFactory.forEach(Shift.class)
.filter(shift -> !shift.getEmployee()
.getSkills()
.contains(shift.getRequiredSkill()))
.penalize(HardSoftScore.ONE_HARD)
.asConstraint("Required skill");
}
}

View File

@ -0,0 +1,52 @@
package com.baeldung.timefoldsolver;
import java.time.LocalDate;
import java.util.Set;
import org.junit.jupiter.api.Test;
import ai.timefold.solver.test.api.score.stream.ConstraintVerifier;
class ShiftScheduleConstraintProviderUnitTest {
private static final LocalDate MONDAY = LocalDate.of(2030, 4, 1);
private static final LocalDate TUESDAY = LocalDate.of(2030, 4, 2);
ConstraintVerifier<ShiftScheduleConstraintProvider, ShiftSchedule> constraintVerifier = ConstraintVerifier.build(new ShiftScheduleConstraintProvider(),
ShiftSchedule.class, Shift.class);
@Test
void givenTwoShiftsOnOneDay_whenApplyingAtMostOneShiftPerDayConstraint_thenPenalize() {
Employee ann = new Employee("Ann", null);
constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::atMostOneShiftPerDay)
.given(ann, new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), null, ann), new Shift(MONDAY.atTime(14, 0), MONDAY.atTime(22, 0), null, ann))
// Penalizes by 2 because both {shiftA, shiftB} and {shiftB, shiftA} match.
// To avoid that, use forEachUniquePair(Shift) instead of forEach(Shift).join(Shift) in ShiftScheduleConstraintProvider.atMostOneShiftPerDay().
.penalizesBy(2);
}
@Test
void givenTwoShiftsOnDifferentDays_whenApplyingAtMostOneShiftPerDayConstraint_thenDoNotPenalize() {
Employee ann = new Employee("Ann", null);
constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::atMostOneShiftPerDay)
.given(ann, new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), null, ann), new Shift(TUESDAY.atTime(14, 0), TUESDAY.atTime(22, 0), null, ann))
.penalizesBy(0);
}
@Test
void givenEmployeeLacksRequiredSkill_whenApplyingRequiredSkillConstraint_thenPenalize() {
Employee ann = new Employee("Ann", Set.of("Waiter"));
constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::requiredSkill)
.given(ann, new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Cook", ann))
.penalizesBy(1);
}
@Test
void givenEmployeeHasRequiredSkill_whenApplyingRequiredSkillConstraint_thenDoNotPenalize() {
Employee ann = new Employee("Ann", Set.of("Waiter"));
constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::requiredSkill)
.given(ann, new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Waiter", ann))
.penalizesBy(0);
}
}

View File

@ -0,0 +1,65 @@
package com.baeldung.timefoldsolver;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import java.time.Duration;
import java.time.LocalDate;
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import ai.timefold.solver.core.api.score.buildin.hardsoft.HardSoftScore;
import ai.timefold.solver.core.api.solver.Solver;
import ai.timefold.solver.core.api.solver.SolverFactory;
import ai.timefold.solver.core.config.solver.SolverConfig;
import ai.timefold.solver.core.config.solver.termination.TerminationConfig;
public class ShiftScheduleSolverUnitTest {
private static final Logger logger = LoggerFactory.getLogger(ShiftScheduleSolverUnitTest.class);
@Test
public void given3Employees5Shifts_whenSolve_thenScoreIsOptimalAndAllShiftsAssigned() {
SolverFactory<ShiftSchedule> solverFactory = SolverFactory.create(new SolverConfig().withSolutionClass(ShiftSchedule.class)
.withEntityClasses(Shift.class)
.withConstraintProviderClass(ShiftScheduleConstraintProvider.class)
// For this dataset, we know the optimal score in advance (which is normally not the case).
// So we can use .withBestScoreLimit() instead of .withTerminationSpentLimit().
.withTerminationConfig(new TerminationConfig().withBestScoreLimit("0hard/0soft")));
Solver<ShiftSchedule> solver = solverFactory.buildSolver();
ShiftSchedule problem = loadProblem();
ShiftSchedule solution = solver.solve(problem);
assertThat(solution.getScore()).isEqualTo(HardSoftScore.ZERO);
assertThat(solution.getShifts().size()).isNotZero();
for (Shift shift : solution.getShifts()) {
assertThat(shift.getEmployee()).isNotNull();
}
printSolution(solution);
}
private ShiftSchedule loadProblem() {
LocalDate monday = LocalDate.of(2030, 4, 1);
LocalDate tuesday = LocalDate.of(2030, 4, 2);
return new ShiftSchedule(
List.of(new Employee("Ann", Set.of("Bartender")), new Employee("Beth", Set.of("Waiter", "Bartender")), new Employee("Carl", Set.of("Waiter"))),
List.of(new Shift(monday.atTime(6, 0), monday.atTime(14, 0), "Waiter"), new Shift(monday.atTime(9, 0), monday.atTime(17, 0), "Bartender"),
new Shift(monday.atTime(14, 0), monday.atTime(22, 0), "Bartender"), new Shift(tuesday.atTime(6, 0), tuesday.atTime(14, 0), "Waiter"),
new Shift(tuesday.atTime(14, 0), tuesday.atTime(22, 0), "Bartender")));
}
private void printSolution(ShiftSchedule solution) {
logger.info("Shift assignments");
for (Shift shift : solution.getShifts()) {
logger.info(" " + shift.getStart()
.toLocalDate() + " " + shift.getStart()
.toLocalTime() + " - " + shift.getEnd()
.toLocalTime() + ": " + shift.getEmployee()
.getName());
}
}
}