diff --git a/java/conference-scheduling/src/main/java/org/acme/conferencescheduling/domain/Talk.java b/java/conference-scheduling/src/main/java/org/acme/conferencescheduling/domain/Talk.java index 46cd9b2709..c8ad68b9db 100644 --- a/java/conference-scheduling/src/main/java/org/acme/conferencescheduling/domain/Talk.java +++ b/java/conference-scheduling/src/main/java/org/acme/conferencescheduling/domain/Talk.java @@ -1,8 +1,5 @@ package org.acme.conferencescheduling.domain; -import static java.util.Collections.emptyList; -import static java.util.Collections.emptySet; - import java.util.List; import java.util.Objects; import java.util.Set; @@ -11,9 +8,11 @@ import ai.timefold.solver.core.api.domain.lookup.PlanningId; import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider; import ai.timefold.solver.core.api.domain.variable.PlanningVariable; - import com.fasterxml.jackson.annotation.JsonIgnore; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; + @PlanningEntity public class Talk { @@ -108,10 +107,6 @@ public Set getRoomRange() { return talkType.getCompatibleRooms(); } - public boolean hasSpeaker(Speaker speaker) { - return speakers.contains(speaker); - } - public int overlappingThemeTrackCount(Talk other) { return overlappingCount(themeTrackTags, other.themeTrackTags); } @@ -317,24 +312,11 @@ public int overlappingMutuallyExclusiveTalksTagCount(Talk other) { return overlappingCount(mutuallyExclusiveTalksTags, other.mutuallyExclusiveTalksTags); } - public boolean hasMutualSpeaker(Talk other) { - for (Speaker speaker : speakers) { - if (other.hasSpeaker(speaker)) { - return true; - } - } - return false; - } - @JsonIgnore public Integer getDurationInMinutes() { return timeslot == null ? null : timeslot.getDurationInMinutes(); } - public boolean overlapsTime(Talk other) { - return timeslot != null && other.getTimeslot() != null && timeslot.overlapsTime(other.getTimeslot()); - } - public int overlappingDurationInMinutes(Talk other) { if (timeslot == null) { return 0; diff --git a/java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/ConferenceSchedulingConstraintProvider.java b/java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/ConferenceSchedulingConstraintProvider.java index 3d3eb95373..9081b03625 100644 --- a/java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/ConferenceSchedulingConstraintProvider.java +++ b/java/conference-scheduling/src/main/java/org/acme/conferencescheduling/solver/ConferenceSchedulingConstraintProvider.java @@ -1,9 +1,33 @@ package org.acme.conferencescheduling.solver; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +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 ai.timefold.solver.core.api.score.stream.Joiners; +import org.acme.conferencescheduling.domain.ConferenceConstraintProperties; +import org.acme.conferencescheduling.domain.Speaker; +import org.acme.conferencescheduling.domain.Talk; +import org.acme.conferencescheduling.solver.justifications.ConferenceSchedulingJustification; +import org.acme.conferencescheduling.solver.justifications.ConflictTalkJustification; +import org.acme.conferencescheduling.solver.justifications.DiversityTalkJustification; +import org.acme.conferencescheduling.solver.justifications.PreferredTagsJustification; +import org.acme.conferencescheduling.solver.justifications.ProhibitedTagsJustification; +import org.acme.conferencescheduling.solver.justifications.RequiredTagsJustification; +import org.acme.conferencescheduling.solver.justifications.UnavailableTimeslotJustification; +import org.acme.conferencescheduling.solver.justifications.UndesiredTagsJustification; + import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.compose; import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.countBi; import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.max; import static ai.timefold.solver.core.api.score.stream.ConstraintCollectors.min; +import static ai.timefold.solver.core.api.score.stream.Joiners.containedIn; import static ai.timefold.solver.core.api.score.stream.Joiners.equal; import static ai.timefold.solver.core.api.score.stream.Joiners.filtering; import static ai.timefold.solver.core.api.score.stream.Joiners.greaterThan; @@ -47,29 +71,6 @@ import static org.acme.conferencescheduling.domain.ConferenceConstraintProperties.THEME_TRACK_CONFLICT; import static org.acme.conferencescheduling.domain.ConferenceConstraintProperties.THEME_TRACK_ROOM_STABILITY; -import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.Collections; -import java.util.List; -import java.util.Objects; - -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 org.acme.conferencescheduling.domain.ConferenceConstraintProperties; -import org.acme.conferencescheduling.domain.Speaker; -import org.acme.conferencescheduling.domain.Talk; -import org.acme.conferencescheduling.solver.justifications.ConferenceSchedulingJustification; -import org.acme.conferencescheduling.solver.justifications.ConflictTalkJustification; -import org.acme.conferencescheduling.solver.justifications.DiversityTalkJustification; -import org.acme.conferencescheduling.solver.justifications.PreferredTagsJustification; -import org.acme.conferencescheduling.solver.justifications.ProhibitedTagsJustification; -import org.acme.conferencescheduling.solver.justifications.RequiredTagsJustification; -import org.acme.conferencescheduling.solver.justifications.UnavailableTimeslotJustification; -import org.acme.conferencescheduling.solver.justifications.UndesiredTagsJustification; - /** * Provides the constraints for the conference scheduling problem. *

@@ -151,8 +152,8 @@ Constraint speakerUnavailableTimeslot(ConstraintFactory factory) { return factory.forEachIncludingUnassigned(Talk.class) .filter(talk -> talk.getTimeslot() != null) .join(Speaker.class, - filtering((talk, speaker) -> talk.hasSpeaker(speaker) - && speaker.getUnavailableTimeslots().contains(talk.getTimeslot()))) + Joiners.containing(Talk::getSpeakers, speaker -> speaker), + containedIn(Talk::getTimeslot, Speaker::getUnavailableTimeslots)) .penalize(HardSoftScore.ofHard(100), (talk, speaker) -> talk.getDurationInMinutes()) .justifyWith( (talk, speaker, score) -> new UnavailableTimeslotJustification(talk, speaker)) @@ -163,7 +164,8 @@ Constraint speakerConflict(ConstraintFactory factory) { return factory.forEachUniquePair(Talk.class, overlapping(t -> t.getTimeslot().getStartDateTime(), t -> t.getTimeslot().getEndDateTime())) .join(Speaker.class, - filtering((talk1, talk2, speaker) -> talk1.hasSpeaker(speaker) && talk2.hasSpeaker(speaker))) + Joiners.containing((talk1, talk2) -> talk1.getSpeakers(), speaker -> speaker), + Joiners.containing((talk1, talk2) -> talk2.getSpeakers(), speaker -> speaker)) .penalize(HardSoftScore.ofHard(10), (talk1, talk2, speaker) -> talk2.overlappingDurationInMinutes(talk1)) .justifyWith((talk, talk2, speaker, score) -> new ConflictTalkJustification(talk, talk2, speaker)) .asConstraint(SPEAKER_CONFLICT); @@ -173,7 +175,7 @@ Constraint talkPrerequisiteTalks(ConstraintFactory factory) { return factory.forEach(Talk.class) .join(Talk.class, greaterThan(t -> t.getTimeslot().getEndDateTime(), t -> t.getTimeslot().getStartDateTime()), - filtering((talk1, talk2) -> talk2.getPrerequisiteTalks().contains(talk1))) + containedIn(talk -> talk, Talk::getPrerequisiteTalks)) .penalize(HardSoftScore.ofHard(10), Talk::combinedDurationInMinutes) .justifyWith( (talk, talk2, score) -> new ConferenceSchedulingJustification( @@ -184,7 +186,7 @@ Constraint talkPrerequisiteTalks(ConstraintFactory factory) { Constraint talkMutuallyExclusiveTalksTags(ConstraintFactory factory) { return factory.forEachUniquePair(Talk.class, overlapping(t -> t.getTimeslot().getStartDateTime(), t -> t.getTimeslot().getEndDateTime()), - filtering((talk1, talk2) -> talk2.overlappingMutuallyExclusiveTalksTagCount(talk1) > 0)) + Joiners.intersecting(Talk::getMutuallyExclusiveTalksTags)) .penalize(HardSoftScore.ofHard(1), (talk1, talk2) -> talk1.overlappingMutuallyExclusiveTalksTagCount(talk2) * talk1.overlappingDurationInMinutes(talk2)) .justifyWith((talk, talk2, score) -> new ConflictTalkJustification("mutually-exclusive-talks tags", talk, @@ -193,8 +195,7 @@ Constraint talkMutuallyExclusiveTalksTags(ConstraintFactory factory) { } Constraint consecutiveTalksPause(ConstraintFactory factory) { - return factory.forEachUniquePair(Talk.class, - filtering((talk1, talk2) -> talk2.hasMutualSpeaker(talk1))) + return factory.forEachUniquePair(Talk.class, Joiners.intersecting(Talk::getSpeakers)) .ifExists(ConferenceConstraintProperties.class, filtering((talk1, talk2, config) -> !talk1.getTimeslot().pauseExists(talk2.getTimeslot(), config.getMinimumConsecutiveTalksPauseInMinutes()))) @@ -341,7 +342,7 @@ Constraint talkProhibitedRoomTags(ConstraintFactory factory) { Constraint themeTrackConflict(ConstraintFactory factory) { return factory.forEachUniquePair(Talk.class, overlapping(t -> t.getTimeslot().getStartDateTime(), t -> t.getTimeslot().getEndDateTime()), - filtering((talk1, talk2) -> talk2.overlappingThemeTrackCount(talk1) > 0)) + Joiners.intersecting(Talk::getThemeTrackTags)) .penalize(HardSoftScore.ofSoft(10), (talk1, talk2) -> talk1.overlappingThemeTrackCount(talk2) * talk1.overlappingDurationInMinutes(talk2)) .justifyWith( @@ -353,7 +354,7 @@ Constraint themeTrackConflict(ConstraintFactory factory) { Constraint themeTrackRoomStability(ConstraintFactory factory) { return factory.forEachUniquePair(Talk.class, equal(talk -> talk.getTimeslot().getStartDateTime().toLocalDate()), - filtering((talk1, talk2) -> talk2.overlappingThemeTrackCount(talk1) > 0)) + Joiners.intersecting(Talk::getThemeTrackTags)) .filter((talk1, talk2) -> !talk1.getRoom().equals(talk2.getRoom())) .penalize(HardSoftScore.ofSoft(10), (talk1, talk2) -> talk1.overlappingThemeTrackCount(talk2) * talk1.combinedDurationInMinutes(talk2)) @@ -371,7 +372,7 @@ Constraint themeTrackRoomStability(ConstraintFactory factory) { Constraint sectorConflict(ConstraintFactory factory) { return factory.forEachUniquePair(Talk.class, overlapping(t -> t.getTimeslot().getStartDateTime(), t -> t.getTimeslot().getEndDateTime()), - filtering((talk1, talk2) -> talk2.overlappingSectorCount(talk1) > 0)) + Joiners.intersecting(Talk::getSectorTags)) .penalize(HardSoftScore.ofSoft(10), (talk1, talk2) -> talk1.overlappingSectorCount(talk2) * talk1.overlappingDurationInMinutes(talk2)) .justifyWith((talk, talk2, score) -> new ConflictTalkJustification("sector", talk, talk.getSectorTags(), talk2, @@ -382,7 +383,7 @@ Constraint sectorConflict(ConstraintFactory factory) { Constraint audienceTypeDiversity(ConstraintFactory factory) { return factory.forEachUniquePair(Talk.class, equal(Talk::getTimeslot), - filtering((talk1, talk2) -> talk2.overlappingAudienceTypeCount(talk1) > 0)) + Joiners.intersecting(Talk::getAudienceTypes)) .reward(HardSoftScore.ofSoft(1), (talk1, talk2) -> talk1.overlappingAudienceTypeCount(talk2) * talk1.getTimeslot().getDurationInMinutes()) .justifyWith((talk, talk2, score) -> new DiversityTalkJustification("audience types", talk, @@ -393,8 +394,8 @@ Constraint audienceTypeDiversity(ConstraintFactory factory) { Constraint audienceTypeThemeTrackConflict(ConstraintFactory factory) { return factory.forEachUniquePair(Talk.class, overlapping(t -> t.getTimeslot().getStartDateTime(), t -> t.getTimeslot().getEndDateTime()), - filtering((talk1, talk2) -> talk2.overlappingThemeTrackCount(talk1) > 0), - filtering((talk1, talk2) -> talk2.overlappingAudienceTypeCount(talk1) > 0)) + Joiners.intersecting(Talk::getThemeTrackTags), + Joiners.intersecting(Talk::getAudienceTypes)) .penalize(HardSoftScore.ofSoft(1), (talk1, talk2) -> talk1.overlappingThemeTrackCount(talk2) * talk1.overlappingAudienceTypeCount(talk2) * talk1.overlappingDurationInMinutes(talk2)) @@ -420,7 +421,7 @@ Constraint contentAudienceLevelFlowViolation(ConstraintFactory factory) { lessThan(Talk::getAudienceLevel), greaterThan(talk1 -> talk1.getTimeslot().getEndDateTime(), talk2 -> talk2.getTimeslot().getStartDateTime()), - filtering((talk1, talk2) -> talk2.overlappingContentCount(talk1) > 0)) + Joiners.intersecting(Talk::getContentTags)) .penalize(HardSoftScore.ofSoft(10), (talk1, talk2) -> talk1.overlappingContentCount(talk2) * talk1.combinedDurationInMinutes(talk2)) .justifyWith((talk, talk2, score) -> new ConferenceSchedulingJustification( @@ -435,7 +436,7 @@ Constraint contentAudienceLevelFlowViolation(ConstraintFactory factory) { Constraint contentConflict(ConstraintFactory factory) { return factory.forEachUniquePair(Talk.class, overlapping(t -> t.getTimeslot().getStartDateTime(), t -> t.getTimeslot().getEndDateTime()), - filtering((talk1, talk2) -> talk2.overlappingContentCount(talk1) > 0)) + Joiners.intersecting(Talk::getContentTags)) .penalize(HardSoftScore.ofSoft(100), (talk1, talk2) -> talk1.overlappingContentCount(talk2) * talk1.overlappingDurationInMinutes(talk2)) .justifyWith( @@ -601,7 +602,7 @@ Constraint talkUndesiredRoomTags(ConstraintFactory factory) { Constraint speakerMakespan(ConstraintFactory factory) { return factory.forEach(Speaker.class) .join(Talk.class, - filtering((speaker, talk) -> talk.hasSpeaker(speaker))) + containedIn(speaker -> speaker, Talk::getSpeakers)) .groupBy((speaker, talk) -> speaker, compose( min((Speaker speaker, Talk talk) -> talk, talk -> talk.getTimeslot().getStartDateTime()), diff --git a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/solver/MaintenanceScheduleConstraintProvider.java b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/solver/MaintenanceScheduleConstraintProvider.java index ddcb0dc318..56565dc740 100644 --- a/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/solver/MaintenanceScheduleConstraintProvider.java +++ b/java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/solver/MaintenanceScheduleConstraintProvider.java @@ -1,19 +1,18 @@ package org.acme.maintenancescheduling.solver; -import java.util.Collections; import java.util.HashSet; import java.util.Set; -import static java.time.temporal.ChronoUnit.DAYS; -import static ai.timefold.solver.core.api.score.stream.Joiners.equal; -import static ai.timefold.solver.core.api.score.stream.Joiners.filtering; -import static ai.timefold.solver.core.api.score.stream.Joiners.overlapping; - -import org.acme.maintenancescheduling.domain.Job; import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore; 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 ai.timefold.solver.core.api.score.stream.Joiners; +import org.acme.maintenancescheduling.domain.Job; + +import static ai.timefold.solver.core.api.score.stream.Joiners.equal; +import static ai.timefold.solver.core.api.score.stream.Joiners.overlapping; +import static java.time.temporal.ChronoUnit.DAYS; public class MaintenanceScheduleConstraintProvider implements ConstraintProvider { @@ -99,9 +98,7 @@ public Constraint tagConflict(ConstraintFactory constraintFactory) { return constraintFactory .forEachUniquePair(Job.class, overlapping(Job::getStartDate, Job::getEndDate), - // TODO Use intersecting() when available https://github.com/TimefoldAI/timefold-solver/issues/8 - filtering((job1, job2) -> !Collections.disjoint( - job1.getTags(), job2.getTags()))) + Joiners.intersecting(Job::getTags)) .penalizeLong(HardSoftLongScore.ofSoft(1_000), (job1, job2) -> { Set intersection = new HashSet<>(job1.getTags());