Skip to content

Commit b327e87

Browse files
chore: migrate quickstarts to use declarative shadow variables
One food-packaging ShadowVariable could not be migrated, since it uses a non-declarative shadow on non-declarative shadow as a source.
1 parent bb72d66 commit b327e87

File tree

13 files changed

+95
-185
lines changed

13 files changed

+95
-185
lines changed

java/food-packaging/src/main/java/org/acme/foodpackaging/domain/Job.java

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
import ai.timefold.solver.core.api.domain.variable.NextElementShadowVariable;
99
import ai.timefold.solver.core.api.domain.variable.PreviousElementShadowVariable;
1010
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
11+
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowSources;
12+
1113
import com.fasterxml.jackson.annotation.JsonIgnore;
1214

1315
import java.time.Duration;
16+
import java.time.LocalDate;
1417
import java.time.LocalDateTime;
1518

1619
@PlanningEntity
@@ -34,6 +37,9 @@ public class Job {
3437

3538
@InverseRelationShadowVariable(sourceVariableName = "jobs")
3639
private Line line;
40+
41+
// TODO: Replace me when declarative shadow variables support
42+
// non-declarative shadow on non-declarative shadow sources
3743
@ShadowVariable(
3844
variableListenerClass = LineOperatorUpdatingVariableListener.class,
3945
sourceEntityClass = Line.class,
@@ -52,11 +58,11 @@ public class Job {
5258
/**
5359
* Start is after cleanup.
5460
*/
55-
@CascadingUpdateShadowVariable(targetMethodName = "updateStartCleaningDateTime")
61+
@ShadowVariable(supplierName = "startCleaningDateTimeSupplier")
5662
private LocalDateTime startCleaningDateTime;
57-
@CascadingUpdateShadowVariable(targetMethodName = "updateStartCleaningDateTime")
63+
@ShadowVariable(supplierName = "startProductionDateTimeSupplier")
5864
private LocalDateTime startProductionDateTime;
59-
@CascadingUpdateShadowVariable(targetMethodName = "updateStartCleaningDateTime")
65+
@ShadowVariable(supplierName = "endDateTimeSupplier")
6066
private LocalDateTime endDateTime;
6167

6268
// No-arg constructor required for Timefold
@@ -190,28 +196,34 @@ public void setEndDateTime(LocalDateTime endDateTime) {
190196
// ************************************************************************
191197

192198
@SuppressWarnings("unused")
193-
private void updateStartCleaningDateTime() {
194-
if (getLine() == null) {
195-
if (getStartCleaningDateTime() != null) {
196-
setStartCleaningDateTime(null);
197-
setStartProductionDateTime(null);
198-
setEndDateTime(null);
199-
}
200-
return;
199+
@ShadowSources({"line", "previousJob.endDateTime"})
200+
private LocalDateTime startCleaningDateTimeSupplier() {
201+
if (line == null) {
202+
return null;
201203
}
202-
Job previous = getPreviousJob();
203-
LocalDateTime startCleaning;
204-
LocalDateTime startProduction;
205-
if (previous == null) {
206-
startCleaning = line.getStartDateTime();
207-
startProduction = line.getStartDateTime();
204+
if (previousJob == null) {
205+
return line.getStartDateTime();
208206
} else {
209-
startCleaning = previous.getEndDateTime();
210-
startProduction = startCleaning == null ? null : startCleaning.plus(getProduct().getCleanupDuration(previous.getProduct()));
207+
return previousJob.getEndDateTime();
208+
}
209+
}
210+
211+
@SuppressWarnings("unused")
212+
@ShadowSources({"line", "startCleaningDateTime"})
213+
private LocalDateTime startProductionDateTimeSupplier() {
214+
if (line == null) {
215+
return null;
211216
}
212-
setStartCleaningDateTime(startCleaning);
213-
setStartProductionDateTime(startProduction);
214-
var endTime = startProduction == null ? null : startProduction.plus(getDuration());
215-
setEndDateTime(endTime);
217+
if (previousJob == null) {
218+
return line.getStartDateTime();
219+
} else {
220+
return startCleaningDateTime == null ? null : startCleaningDateTime.plus(getProduct().getCleanupDuration(previousJob.getProduct()));
221+
}
222+
}
223+
224+
@SuppressWarnings("unused")
225+
@ShadowSources({"startProductionDateTime"})
226+
private LocalDateTime endDateTimeSupplier() {
227+
return startProductionDateTime == null ? null : startProductionDateTime.plus(getDuration());
216228
}
217229
}

java/food-packaging/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h".
66
quarkus.timefold.solver.termination.spent-limit=30s
7+
quarkus.timefold.solver.enabled-preview-features=DECLARATIVE_SHADOW_VARIABLES
78

89
# To change how many solvers to run in parallel
910
# timefold.solver-manager.parallel-solver-count=4

java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/domain/Job.java

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@
77
import ai.timefold.solver.core.api.domain.lookup.PlanningId;
88
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
99
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
10-
11-
import org.acme.maintenancescheduling.solver.EndDateUpdatingVariableListener;
10+
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowSources;
1211

1312
@PlanningEntity
1413
public class Job {
@@ -29,7 +28,7 @@ public class Job {
2928
// Follows the TimeGrain Design Pattern
3029
@PlanningVariable
3130
private LocalDate startDate; // Inclusive
32-
@ShadowVariable(variableListenerClass = EndDateUpdatingVariableListener.class, sourceVariableName = "startDate")
31+
@ShadowVariable(supplierName = "endDateSupplier")
3332
private LocalDate endDate; // Exclusive
3433

3534
public Job() {
@@ -56,7 +55,7 @@ public Job(String id, String name, int durationInDays, LocalDate minStartDate, L
5655
this.tags = tags;
5756
this.crew = crew;
5857
this.startDate = startDate;
59-
this.endDate = EndDateUpdatingVariableListener.calculateEndDate(startDate, durationInDays);
58+
this.endDate = calculateEndDate(startDate, durationInDays);
6059
}
6160

6261
@Override
@@ -119,4 +118,25 @@ public LocalDate getEndDate() {
119118
public void setEndDate(LocalDate endDate) {
120119
this.endDate = endDate;
121120
}
121+
122+
// ************************************************************************
123+
// Complex methods
124+
// ************************************************************************
125+
@SuppressWarnings("unused")
126+
@ShadowSources("startDate")
127+
public LocalDate endDateSupplier() {
128+
return calculateEndDate(startDate, durationInDays);
129+
}
130+
131+
public static LocalDate calculateEndDate(LocalDate startDate, int durationInDays) {
132+
if (startDate == null) {
133+
return null;
134+
} else {
135+
// Skip weekends. Does not work for holidays.
136+
// To skip holidays too, cache all working days in WorkCalendar.
137+
// Keep in sync with MaintenanceSchedule.createStartDateList().
138+
int weekendPadding = 2 * ((durationInDays + (startDate.getDayOfWeek().getValue() - 1)) / 5);
139+
return startDate.plusDays(durationInDays + weekendPadding);
140+
}
141+
}
122142
}

java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/rest/DemoDataGenerator.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.acme.maintenancescheduling.domain.Job;
1515
import org.acme.maintenancescheduling.domain.MaintenanceSchedule;
1616
import org.acme.maintenancescheduling.domain.WorkCalendar;
17-
import org.acme.maintenancescheduling.solver.EndDateUpdatingVariableListener;
1817

1918
@ApplicationScoped
2019
public class DemoDataGenerator {
@@ -63,9 +62,9 @@ public MaintenanceSchedule generateDemoData(DemoData demoData) {
6362
+ random.nextInt(workdayTotal - (durationInDays + 5));
6463
int minWorkdayOffset = random.nextInt(workdayTotal - minMaxBetweenWorkdays + 1);
6564
int minIdealEndBetweenWorkdays = minMaxBetweenWorkdays - 1 - random.nextInt(4);
66-
LocalDate minStartDate = EndDateUpdatingVariableListener.calculateEndDate(fromDate, minWorkdayOffset);
67-
LocalDate maxEndDate = EndDateUpdatingVariableListener.calculateEndDate(minStartDate, minMaxBetweenWorkdays);
68-
LocalDate idealEndDate = EndDateUpdatingVariableListener.calculateEndDate(minStartDate, minIdealEndBetweenWorkdays);
65+
LocalDate minStartDate = Job.calculateEndDate(fromDate, minWorkdayOffset);
66+
LocalDate maxEndDate = Job.calculateEndDate(minStartDate, minMaxBetweenWorkdays);
67+
LocalDate idealEndDate = Job.calculateEndDate(minStartDate, minIdealEndBetweenWorkdays);
6968
Set<String> tags = random.nextDouble() < 0.1 ? Set.of(jobArea, "Subway") : Set.of(jobArea);
7069
jobs.add(new Job(Integer.toString(i), jobArea + " " + jobTarget, durationInDays, minStartDate, maxEndDate, idealEndDate,
7170
tags));

java/maintenance-scheduling/src/main/java/org/acme/maintenancescheduling/solver/EndDateUpdatingVariableListener.java

Lines changed: 0 additions & 60 deletions
This file was deleted.

java/maintenance-scheduling/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h".
66
quarkus.timefold.solver.termination.spent-limit=30s
7+
quarkus.timefold.solver.enabled-preview-features=DECLARATIVE_SHADOW_VARIABLES
78

89
# To change how many solvers to run in parallel
910
# timefold.solver-manager.parallel-solver-count=4

java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/Allocation.java

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import ai.timefold.solver.core.api.domain.valuerange.ValueRangeProvider;
1414
import ai.timefold.solver.core.api.domain.variable.PlanningVariable;
1515
import ai.timefold.solver.core.api.domain.variable.ShadowVariable;
16+
import ai.timefold.solver.core.preview.api.domain.variable.declarative.ShadowSources;
1617

1718
import org.acme.projectjobschedule.domain.solver.DelayStrengthComparator;
18-
import org.acme.projectjobschedule.domain.solver.PredecessorsDoneDateUpdatingVariableListener;
1919

2020
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
2121
import com.fasterxml.jackson.annotation.JsonIdentityReference;
@@ -50,9 +50,7 @@ public class Allocation {
5050
private Integer delay; // In days
5151

5252
// Shadow variables
53-
@ShadowVariable(variableListenerClass = PredecessorsDoneDateUpdatingVariableListener.class,
54-
sourceVariableName = "executionMode")
55-
@ShadowVariable(variableListenerClass = PredecessorsDoneDateUpdatingVariableListener.class, sourceVariableName = "delay")
53+
@ShadowVariable(supplierName = "predecessorsDoneDateSupplier")
5654
private Integer predecessorsDoneDate;
5755

5856
// Filled from shadow variables
@@ -199,6 +197,17 @@ public void setPredecessorsDoneDate(Integer predecessorsDoneDate) {
199197
// ************************************************************************
200198
// Complex methods
201199
// ************************************************************************
200+
@SuppressWarnings("unused")
201+
@ShadowSources({"executionMode", "delay", "predecessorAllocations[].predecessorsDoneDate"})
202+
public Integer predecessorsDoneDateSupplier() {
203+
// For the source the doneDate must be 0.
204+
Integer doneDate = 0;
205+
for (Allocation predecessorAllocation : predecessorAllocations) {
206+
int endDate = predecessorAllocation.getEndDate();
207+
doneDate = Math.max(doneDate, endDate);
208+
}
209+
return doneDate;
210+
}
202211

203212
public void invalidateComputedVariables() {
204213
this.startDate = null;

java/project-job-scheduling/src/main/java/org/acme/projectjobschedule/domain/solver/PredecessorsDoneDateUpdatingVariableListener.java

Lines changed: 0 additions & 80 deletions
This file was deleted.

java/project-job-scheduling/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
# The solver runs for 30 seconds. To run for 5 minutes use "5m" and for 2 hours use "2h".
66
quarkus.timefold.solver.termination.spent-limit=30s
7+
quarkus.timefold.solver.enabled-preview-features=DECLARATIVE_SHADOW_VARIABLES
78

89
# To change how many solvers to run in parallel
910
# timefold.solver-manager.parallel-solver-count=4

0 commit comments

Comments
 (0)