From 1f5f4af22ab4f72e00c6bde52946cda4687982cf Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Tue, 21 Nov 2023 14:09:04 +0100 Subject: [PATCH 01/17] GST-4 Timefold Solver --- pom.xml | 2 + timefold-solver/README.md | 7 +++ timefold-solver/pom.xml | 44 ++++++++++++++ .../com/baeldung/timefoldsolver/Employee.java | 32 +++++++++++ .../com/baeldung/timefoldsolver/Shift.java | 57 +++++++++++++++++++ .../timefoldsolver/ShiftSchedule.java | 47 +++++++++++++++ .../timefoldsolver/ShiftScheduleApp.java | 54 ++++++++++++++++++ .../ShiftScheduleConstraintProvider.java | 37 ++++++++++++ .../ShiftScheduleConstraintProviderTest.java | 50 ++++++++++++++++ 9 files changed, 330 insertions(+) create mode 100644 timefold-solver/README.md create mode 100644 timefold-solver/pom.xml create mode 100644 timefold-solver/src/main/java/com/baeldung/timefoldsolver/Employee.java create mode 100644 timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java create mode 100644 timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java create mode 100644 timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java create mode 100644 timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProvider.java create mode 100644 timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java diff --git a/pom.xml b/pom.xml index 7099dd8cbc..8729aa8f5c 100644 --- a/pom.xml +++ b/pom.xml @@ -894,6 +894,7 @@ tensorflow-java testing-modules testing-modules/mockito-simple + timefold-solver vaadin vavr-modules vertx-modules @@ -1139,6 +1140,7 @@ tensorflow-java testing-modules testing-modules/mockito-simple + timefold-solver vaadin vavr-modules vertx-modules diff --git a/timefold-solver/README.md b/timefold-solver/README.md new file mode 100644 index 0000000000..b7a2dbe14b --- /dev/null +++ b/timefold-solver/README.md @@ -0,0 +1,7 @@ +## OptaPlanner + +This module contains articles about OptaPlanner. + +### Relevant articles + +- [A Guide to OptaPlanner](https://www.baeldung.com/opta-planner) diff --git a/timefold-solver/pom.xml b/timefold-solver/pom.xml new file mode 100644 index 0000000000..2a219a593c --- /dev/null +++ b/timefold-solver/pom.xml @@ -0,0 +1,44 @@ + + + 4.0.0 + timefold-solver + timefold-solver + + + com.baeldung + parent-modules + 1.0.0-SNAPSHOT + + + + + + ai.timefold.solver + timefold-solver-bom + ${version.ai.timefold.solver} + pom + import + + + + + + ai.timefold.solver + timefold-solver-core + + + ai.timefold.solver + timefold-solver-test + test + + + + + 17 + 17 + 1.4.0 + + + diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Employee.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Employee.java new file mode 100644 index 0000000000..c755896201 --- /dev/null +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Employee.java @@ -0,0 +1,32 @@ +package com.baeldung.timefoldsolver; + +import java.util.Set; + +public class Employee { + + private String name; + private Set skills; + + public Employee(String name, Set skills) { + this.name = name; + this.skills = skills; + } + + @Override + public String toString() { + return name; + } + + // ************************************************************************ + // Getters and setters + // ************************************************************************ + + public String getName() { + return name; + } + + public Set getSkills() { + return skills; + } + +} diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java new file mode 100644 index 0000000000..04087134c6 --- /dev/null +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java @@ -0,0 +1,57 @@ +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; + } + + // ************************************************************************ + // Getters and setters + // ************************************************************************ + + public LocalDateTime getStart() { + return start; + } + + public LocalDateTime getEnd() { + return end; + } + + public String getRequiredSkill() { + return requiredSkill; + } + + public Employee getEmployee() { + return employee; + } + +} diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java new file mode 100644 index 0000000000..85abc8eb66 --- /dev/null +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java @@ -0,0 +1,47 @@ +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 employees; + @PlanningEntityCollectionProperty + private List shifts; + + @PlanningScore + private HardSoftScore score = null; + + // A no-arg constructor is required for @PlanningSolution annotated classes + public ShiftSchedule() { + } + + public ShiftSchedule(List employees, List shifts) { + this.employees = employees; + this.shifts = shifts; + } + + // ************************************************************************ + // Getters and setters + // ************************************************************************ + + public List getEmployees() { + return employees; + } + + public List getShifts() { + return shifts; + } + + public HardSoftScore getScore() { + return score; + } + +} diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java new file mode 100644 index 0000000000..9c25922dea --- /dev/null +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java @@ -0,0 +1,54 @@ +package com.baeldung.timefoldsolver; + +import java.time.Duration; +import java.time.LocalDate; +import java.util.List; +import java.util.Set; + +import ai.timefold.solver.core.api.solver.Solver; +import ai.timefold.solver.core.api.solver.SolverFactory; +import ai.timefold.solver.core.config.solver.SolverConfig; + +public class ShiftScheduleApp { + + public static void main(String[] args) { + SolverFactory solverFactory = SolverFactory.create(new SolverConfig() + .withSolutionClass(ShiftSchedule.class) + .withEntityClasses(Shift.class) + .withConstraintProviderClass(ShiftScheduleConstraintProvider.class) + // The solver runs only for 5 seconds on this small dataset. + // It's recommended to run for at least 5 minutes ("5m") otherwise. + .withTerminationSpentLimit(Duration.ofSeconds(5))); + Solver solver = solverFactory.buildSolver(); + + ShiftSchedule problem = loadProblem(); + ShiftSchedule solution = solver.solve(problem); + printSolution(solution); + } + + private static 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 static void printSolution(ShiftSchedule solution) { + System.out.println("Shift assignments"); + for (Shift shift : solution.getShifts()) { + System.out.println(" " + shift.getStart().toLocalDate() + + " " + shift.getStart().toLocalTime() + " - " + shift.getEnd().toLocalTime() + + ": " + shift.getEmployee().getName()); + } + } + +} diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProvider.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProvider.java new file mode 100644 index 0000000000..0846429460 --- /dev/null +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProvider.java @@ -0,0 +1,37 @@ +package com.baeldung.timefoldsolver; + +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; + +import static ai.timefold.solver.core.api.score.stream.Joiners.equal; + +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"); + } + +} diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java new file mode 100644 index 0000000000..9979f5d5e0 --- /dev/null +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java @@ -0,0 +1,50 @@ +package com.baeldung.timefoldsolver; + +import java.time.LocalDate; +import java.util.Set; + +import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; +import org.junit.jupiter.api.Test; + +class ShiftScheduleConstraintProviderTest { + + private static final LocalDate MONDAY = LocalDate.of(2030, 4, 1); + private static final LocalDate TUESDAY = LocalDate.of(2030, 4, 2); + + ConstraintVerifier constraintVerifier = ConstraintVerifier.build( + new ShiftScheduleConstraintProvider(), ShiftSchedule.class, Shift.class); + + @Test + void atMostOneShiftPerDay() { + 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 both A-B and B-A. To avoid that, use forEachUniquePair() in the constraint instead. + .penalizesBy(2); + 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 requiredSkill() { + 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); + constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::requiredSkill) + .given( + ann, + new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Waiter", ann)) + .penalizesBy(0); + } + +} From 39a7c066059b7db29fb75b98a5cc03a6d7843b5b Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Fri, 24 Nov 2023 15:08:56 +0100 Subject: [PATCH 02/17] Remove commetn blocks Co-authored-by: Liam Williams --- .../src/main/java/com/baeldung/timefoldsolver/Shift.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java index 04087134c6..9c7b663ecd 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java @@ -34,10 +34,6 @@ public class Shift { return start + " - " + end; } - // ************************************************************************ - // Getters and setters - // ************************************************************************ - public LocalDateTime getStart() { return start; } From 8e8331376ef326e5efc059a804373beb9b5311de Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Fri, 24 Nov 2023 15:09:03 +0100 Subject: [PATCH 03/17] Remove commetn blocks Co-authored-by: Liam Williams --- .../src/main/java/com/baeldung/timefoldsolver/Employee.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Employee.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Employee.java index c755896201..ef4752a8c6 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Employee.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Employee.java @@ -17,10 +17,6 @@ public class Employee { return name; } - // ************************************************************************ - // Getters and setters - // ************************************************************************ - public String getName() { return name; } From 2478475ceae6da380542501956604ff65fc88cd6 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Fri, 24 Nov 2023 15:09:14 +0100 Subject: [PATCH 04/17] Remove comment blocks Co-authored-by: Liam Williams --- .../main/java/com/baeldung/timefoldsolver/ShiftSchedule.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java index 85abc8eb66..7794d46a28 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java @@ -28,10 +28,6 @@ public class ShiftSchedule { this.shifts = shifts; } - // ************************************************************************ - // Getters and setters - // ************************************************************************ - public List getEmployees() { return employees; } From 464bf4cda6c028505aaa357afa0d8c5454b9e093 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Fri, 24 Nov 2023 15:12:23 +0100 Subject: [PATCH 05/17] Fields default to null. Co-authored-by: Liam Williams --- .../main/java/com/baeldung/timefoldsolver/ShiftSchedule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java index 7794d46a28..794c31e5b7 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftSchedule.java @@ -17,7 +17,7 @@ public class ShiftSchedule { private List shifts; @PlanningScore - private HardSoftScore score = null; + private HardSoftScore score; // A no-arg constructor is required for @PlanningSolution annotated classes public ShiftSchedule() { From 783d9dbd0189b6f427190b28db22e5ebe5b123bb Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Fri, 24 Nov 2023 21:01:28 +0100 Subject: [PATCH 06/17] Use slf4j --- .../com/baeldung/timefoldsolver/ShiftScheduleApp.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java index 9c25922dea..9170e2363f 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java @@ -8,9 +8,13 @@ import java.util.Set; 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 org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class ShiftScheduleApp { + private static final Logger logger = LoggerFactory.getLogger(ShiftScheduleApp.class); + public static void main(String[] args) { SolverFactory solverFactory = SolverFactory.create(new SolverConfig() .withSolutionClass(ShiftSchedule.class) @@ -43,9 +47,9 @@ public class ShiftScheduleApp { } private static void printSolution(ShiftSchedule solution) { - System.out.println("Shift assignments"); + logger.info("Shift assignments"); for (Shift shift : solution.getShifts()) { - System.out.println(" " + shift.getStart().toLocalDate() + logger.info(" " + shift.getStart().toLocalDate() + " " + shift.getStart().toLocalTime() + " - " + shift.getEnd().toLocalTime() + ": " + shift.getEmployee().getName()); } From 3df51484f27a8ee07507d827df34f17c8e80af6f Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Fri, 24 Nov 2023 21:17:46 +0100 Subject: [PATCH 07/17] improve comment --- .../timefoldsolver/ShiftScheduleConstraintProviderTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java index 9979f5d5e0..9477e892eb 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java @@ -22,7 +22,8 @@ class ShiftScheduleConstraintProviderTest { 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 both A-B and B-A. To avoid that, use forEachUniquePair() in the constraint instead. + // Penalizes by 2 because both {shiftA, shiftB} and {shiftB, shiftA} match. + // To avoid that, use forEachUniquePair() in the constraint instead. .penalizesBy(2); constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::atMostOneShiftPerDay) .given( From 07a6999e56bd27aa2d0fc6b1b779245c814663a6 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Fri, 24 Nov 2023 21:40:29 +0100 Subject: [PATCH 08/17] format with baeldung style --- .../com/baeldung/timefoldsolver/Shift.java | 3 +- .../timefoldsolver/ShiftScheduleApp.java | 42 +++++++++---------- .../ShiftScheduleConstraintProvider.java | 28 ++++++------- .../ShiftScheduleConstraintProviderTest.java | 37 +++++++--------- 4 files changed, 48 insertions(+), 62 deletions(-) diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java index 9c7b663ecd..2ef2f2a1b9 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/Shift.java @@ -16,7 +16,8 @@ public class Shift { private Employee employee; // A no-arg constructor is required for @PlanningEntity annotated classes - public Shift() {} + public Shift() { + } public Shift(LocalDateTime start, LocalDateTime end, String requiredSkill) { this(start, end, requiredSkill, null); diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java index 9170e2363f..24666ac6aa 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java @@ -5,24 +5,24 @@ import java.time.LocalDate; import java.util.List; import java.util.Set; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + 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 org.slf4j.Logger; -import org.slf4j.LoggerFactory; public class ShiftScheduleApp { private static final Logger logger = LoggerFactory.getLogger(ShiftScheduleApp.class); public static void main(String[] args) { - SolverFactory solverFactory = SolverFactory.create(new SolverConfig() - .withSolutionClass(ShiftSchedule.class) - .withEntityClasses(Shift.class) - .withConstraintProviderClass(ShiftScheduleConstraintProvider.class) - // The solver runs only for 5 seconds on this small dataset. - // It's recommended to run for at least 5 minutes ("5m") otherwise. - .withTerminationSpentLimit(Duration.ofSeconds(5))); + SolverFactory solverFactory = SolverFactory.create(new SolverConfig().withSolutionClass(ShiftSchedule.class) + .withEntityClasses(Shift.class) + .withConstraintProviderClass(ShiftScheduleConstraintProvider.class) + // The solver runs only for 5 seconds on this small dataset. + // It's recommended to run for at least 5 minutes ("5m") otherwise. + .withTerminationSpentLimit(Duration.ofSeconds(5))); Solver solver = solverFactory.buildSolver(); ShiftSchedule problem = loadProblem(); @@ -33,25 +33,21 @@ public class ShiftScheduleApp { private static 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") - )); + 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 static 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()); + logger.info(" " + shift.getStart() + .toLocalDate() + " " + shift.getStart() + .toLocalTime() + " - " + shift.getEnd() + .toLocalTime() + ": " + shift.getEmployee() + .getName()); } } diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProvider.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProvider.java index 0846429460..bd308dacbf 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProvider.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProvider.java @@ -1,37 +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; -import static ai.timefold.solver.core.api.score.stream.Joiners.equal; - public class ShiftScheduleConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { - return new Constraint[] { - atMostOneShiftPerDay(constraintFactory), - requiredSkill(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"); + .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"); + .filter(shift -> !shift.getEmployee() + .getSkills() + .contains(shift.getRequiredSkill())) + .penalize(HardSoftScore.ONE_HARD) + .asConstraint("Required skill"); } } diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java index 9477e892eb..c4741a831b 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java @@ -3,49 +3,40 @@ package com.baeldung.timefoldsolver; import java.time.LocalDate; import java.util.Set; -import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; import org.junit.jupiter.api.Test; +import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; + class ShiftScheduleConstraintProviderTest { private static final LocalDate MONDAY = LocalDate.of(2030, 4, 1); private static final LocalDate TUESDAY = LocalDate.of(2030, 4, 2); - ConstraintVerifier constraintVerifier = ConstraintVerifier.build( - new ShiftScheduleConstraintProvider(), ShiftSchedule.class, Shift.class); + ConstraintVerifier constraintVerifier = ConstraintVerifier.build(new ShiftScheduleConstraintProvider(), + ShiftSchedule.class, Shift.class); @Test void atMostOneShiftPerDay() { 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() in the constraint instead. - .penalizesBy(2); + .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() in the constraint instead. + .penalizesBy(2); 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); + .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 requiredSkill() { 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); + .given(ann, new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Cook", ann)) + .penalizesBy(1); constraintVerifier.verifyThat(ShiftScheduleConstraintProvider::requiredSkill) - .given( - ann, - new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Waiter", ann)) - .penalizesBy(0); + .given(ann, new Shift(MONDAY.atTime(6, 0), MONDAY.atTime(14, 0), "Waiter", ann)) + .penalizesBy(0); } } From dfd736183f64d84efdd29ecbbc5f1795cb21ca75 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Fri, 24 Nov 2023 21:44:10 +0100 Subject: [PATCH 09/17] Adjust test names --- .../ShiftScheduleConstraintProviderTest.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java index c4741a831b..0832cd7bdd 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java @@ -16,24 +16,33 @@ class ShiftScheduleConstraintProviderTest { ShiftSchedule.class, Shift.class); @Test - void atMostOneShiftPerDay() { + void whenTwoShiftsOnOneDay_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() in the constraint instead. .penalizesBy(2); + } + @Test + void whenTwoShiftsOnDifferentDays_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 requiredSkill() { + void whenEmployeeLacksRequiredSkill_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 whenEmployeeHasRequiredSkill_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); From 25bfddf417ea1ce8feb994101ccf771f38c4ef02 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Sat, 25 Nov 2023 14:15:27 +0100 Subject: [PATCH 10/17] Limit solving duration to 2 seconds --- .../java/com/baeldung/timefoldsolver/ShiftScheduleApp.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java index 24666ac6aa..e8dc02e2b3 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java +++ b/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java @@ -20,9 +20,9 @@ public class ShiftScheduleApp { SolverFactory solverFactory = SolverFactory.create(new SolverConfig().withSolutionClass(ShiftSchedule.class) .withEntityClasses(Shift.class) .withConstraintProviderClass(ShiftScheduleConstraintProvider.class) - // The solver runs only for 5 seconds on this small dataset. - // It's recommended to run for at least 5 minutes ("5m") otherwise. - .withTerminationSpentLimit(Duration.ofSeconds(5))); + // The solver runs only for 2 seconds on this tiny dataset. + // It's recommended to run for at least 5 minutes ("5m") on large datasets. + .withTerminationSpentLimit(Duration.ofSeconds(2))); Solver solver = solverFactory.buildSolver(); ShiftSchedule problem = loadProblem(); From 5df3c5711ada202a685a1f193f0bb2a32986c806 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Sat, 25 Nov 2023 14:35:43 +0100 Subject: [PATCH 11/17] Make PMD happy --- ...erTest.java => ShiftScheduleConstraintProviderUnitTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename timefold-solver/src/test/java/com/baeldung/timefoldsolver/{ShiftScheduleConstraintProviderTest.java => ShiftScheduleConstraintProviderUnitTest.java} (97%) diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java similarity index 97% rename from timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java rename to timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java index 0832cd7bdd..3a3f4dc032 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.Test; import ai.timefold.solver.test.api.score.stream.ConstraintVerifier; -class ShiftScheduleConstraintProviderTest { +class ShiftScheduleConstraintProviderUnitTest { private static final LocalDate MONDAY = LocalDate.of(2030, 4, 1); private static final LocalDate TUESDAY = LocalDate.of(2030, 4, 2); From 2e60ba50079fbe296f326a281cda547ffc7bd2de Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Sat, 25 Nov 2023 21:09:32 +0100 Subject: [PATCH 12/17] Turn into a unit test to avoid main() --- timefold-solver/pom.xml | 4 ++-- .../ShiftScheduleConstraintProviderUnitTest.java | 1 + .../ShiftScheduleSolverUnitTest.java} | 13 ++++++++----- 3 files changed, 11 insertions(+), 7 deletions(-) rename timefold-solver/src/{main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java => test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java} (86%) diff --git a/timefold-solver/pom.xml b/timefold-solver/pom.xml index 2a219a593c..a16afb9e54 100644 --- a/timefold-solver/pom.xml +++ b/timefold-solver/pom.xml @@ -1,7 +1,7 @@ + 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"> 4.0.0 timefold-solver timefold-solver diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java index 3a3f4dc032..3f68ef701e 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java @@ -24,6 +24,7 @@ class ShiftScheduleConstraintProviderUnitTest { // To avoid that, use forEachUniquePair() in the constraint instead. .penalizesBy(2); } + @Test void whenTwoShiftsOnDifferentDays_thenDoNotPenalize() { Employee ann = new Employee("Ann", null); diff --git a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java similarity index 86% rename from timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java rename to timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java index e8dc02e2b3..45a84b40da 100644 --- a/timefold-solver/src/main/java/com/baeldung/timefoldsolver/ShiftScheduleApp.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java @@ -5,18 +5,21 @@ 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.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 ShiftScheduleApp { +public class ShiftScheduleSolverUnitTest { - private static final Logger logger = LoggerFactory.getLogger(ShiftScheduleApp.class); + private static final Logger logger = LoggerFactory.getLogger(ShiftScheduleSolverUnitTest.class); - public static void main(String[] args) { + @Test + public void solve() { SolverFactory solverFactory = SolverFactory.create(new SolverConfig().withSolutionClass(ShiftSchedule.class) .withEntityClasses(Shift.class) .withConstraintProviderClass(ShiftScheduleConstraintProvider.class) @@ -30,7 +33,7 @@ public class ShiftScheduleApp { printSolution(solution); } - private static ShiftSchedule loadProblem() { + private ShiftSchedule loadProblem() { LocalDate monday = LocalDate.of(2030, 4, 1); LocalDate tuesday = LocalDate.of(2030, 4, 2); return new ShiftSchedule( @@ -40,7 +43,7 @@ public class ShiftScheduleApp { new Shift(tuesday.atTime(14, 0), tuesday.atTime(22, 0), "Bartender"))); } - private static void printSolution(ShiftSchedule solution) { + private void printSolution(ShiftSchedule solution) { logger.info("Shift assignments"); for (Shift shift : solution.getShifts()) { logger.info(" " + shift.getStart() From 367e9e6c2a0c332b774320bef8ee8818a3fce200 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Mon, 27 Nov 2023 08:45:03 +0100 Subject: [PATCH 13/17] Assert the solution --- .../ShiftScheduleConstraintProviderUnitTest.java | 2 +- .../timefoldsolver/ShiftScheduleSolverUnitTest.java | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java index 3f68ef701e..fbe6ebccaf 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java @@ -21,7 +21,7 @@ class ShiftScheduleConstraintProviderUnitTest { 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() in the constraint instead. + // To avoid that, use forEachUniquePair() in the constraint instead of forEach().join() in the implementation. .penalizesBy(2); } diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java index 45a84b40da..8b57fb42b6 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java @@ -1,5 +1,7 @@ package com.baeldung.timefoldsolver; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + import java.time.Duration; import java.time.LocalDate; import java.util.List; @@ -23,13 +25,17 @@ public class ShiftScheduleSolverUnitTest { SolverFactory solverFactory = SolverFactory.create(new SolverConfig().withSolutionClass(ShiftSchedule.class) .withEntityClasses(Shift.class) .withConstraintProviderClass(ShiftScheduleConstraintProvider.class) - // The solver runs only for 2 seconds on this tiny dataset. - // It's recommended to run for at least 5 minutes ("5m") on large datasets. - .withTerminationSpentLimit(Duration.ofSeconds(2))); + // 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 solver = solverFactory.buildSolver(); ShiftSchedule problem = loadProblem(); ShiftSchedule solution = solver.solve(problem); + assertThat(solution.getShifts().size()).isNotZero(); + for (Shift shift : solution.getShifts()) { + assertThat(shift.getEmployee()).isNotNull(); + } printSolution(solution); } From cf033c3934545e5327e0e40e06a58c836b5985e8 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Sat, 2 Dec 2023 12:01:10 +0100 Subject: [PATCH 14/17] rename and improve test whenSolve_thenScoreIsOptimalAndAllShiftsAssigned --- .../baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java index 8b57fb42b6..f0cf126236 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java @@ -11,6 +11,7 @@ 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; @@ -21,7 +22,7 @@ public class ShiftScheduleSolverUnitTest { private static final Logger logger = LoggerFactory.getLogger(ShiftScheduleSolverUnitTest.class); @Test - public void solve() { + public void whenSolve_thenScoreIsOptimalAndAllShiftsAssigned() { SolverFactory solverFactory = SolverFactory.create(new SolverConfig().withSolutionClass(ShiftSchedule.class) .withEntityClasses(Shift.class) .withConstraintProviderClass(ShiftScheduleConstraintProvider.class) @@ -32,6 +33,7 @@ public class ShiftScheduleSolverUnitTest { 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(); From 9e8f67ad03ba65f772f390ff6b1035353742091f Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Sat, 2 Dec 2023 12:03:19 +0100 Subject: [PATCH 15/17] Improve forEachUniquePair comment further --- .../timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java index fbe6ebccaf..2f929ace34 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java @@ -21,7 +21,7 @@ class ShiftScheduleConstraintProviderUnitTest { 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() in the constraint instead of forEach().join() in the implementation. + // To avoid that, use forEachUniquePair(Shift) instead of forEach(Shift).join(Shift) in ShiftScheduleConstraintProvider.atMostOneShiftPerDay(). .penalizesBy(2); } From f2adeb59566c6e9660004e2afc0bba8b9108d40b Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Sat, 2 Dec 2023 20:20:40 +0100 Subject: [PATCH 16/17] BDD style --- .../ShiftScheduleConstraintProviderUnitTest.java | 8 ++++---- .../timefoldsolver/ShiftScheduleSolverUnitTest.java | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java index 2f929ace34..c8ec5b53c4 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleConstraintProviderUnitTest.java @@ -16,7 +16,7 @@ class ShiftScheduleConstraintProviderUnitTest { ShiftSchedule.class, Shift.class); @Test - void whenTwoShiftsOnOneDay_thenPenalize() { + 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)) @@ -26,7 +26,7 @@ class ShiftScheduleConstraintProviderUnitTest { } @Test - void whenTwoShiftsOnDifferentDays_thenDoNotPenalize() { + 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)) @@ -34,7 +34,7 @@ class ShiftScheduleConstraintProviderUnitTest { } @Test - void whenEmployeeLacksRequiredSkill_thenPenalize() { + 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)) @@ -42,7 +42,7 @@ class ShiftScheduleConstraintProviderUnitTest { } @Test - void whenEmployeeHasRequiredSkill_thenDoNotPenalize() { + 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)) diff --git a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java index f0cf126236..873ce1a853 100644 --- a/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java +++ b/timefold-solver/src/test/java/com/baeldung/timefoldsolver/ShiftScheduleSolverUnitTest.java @@ -22,7 +22,7 @@ public class ShiftScheduleSolverUnitTest { private static final Logger logger = LoggerFactory.getLogger(ShiftScheduleSolverUnitTest.class); @Test - public void whenSolve_thenScoreIsOptimalAndAllShiftsAssigned() { + public void given3Employees5Shifts_whenSolve_thenScoreIsOptimalAndAllShiftsAssigned() { SolverFactory solverFactory = SolverFactory.create(new SolverConfig().withSolutionClass(ShiftSchedule.class) .withEntityClasses(Shift.class) .withConstraintProviderClass(ShiftScheduleConstraintProvider.class) From 97aaeed2c0a9dcf4b72664db6957c3ba8005d324 Mon Sep 17 00:00:00 2001 From: Geoffrey De Smet Date: Thu, 7 Dec 2023 08:21:13 +0100 Subject: [PATCH 17/17] Fix Timefold Solver readme + remove unpublished article --- timefold-solver/README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/timefold-solver/README.md b/timefold-solver/README.md index b7a2dbe14b..1abc4d4ca0 100644 --- a/timefold-solver/README.md +++ b/timefold-solver/README.md @@ -1,7 +1,6 @@ -## OptaPlanner +## Timefold Solver -This module contains articles about OptaPlanner. +This module contains articles about (Timefold Solver)[https://timefold.ai]. ### Relevant articles -- [A Guide to OptaPlanner](https://www.baeldung.com/opta-planner)