Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.adoc
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
= Changelog

== v2026.X.X

=== Dependency update

=== New features


=== Improvements


=== Bug fixes

- https://github.com/ObeoNetwork/pepper/issues/25[#25] Add holiday adjustment to prevent tasks from starting or ending on weekends.
Bug: Tasks with dependencies may incorrectly extend indefinitely. If a task is repeatedly moved backward by less than two days and this causes the end date of one of its dependencies to fall on a weekend, the dependent task may keep growing on each recalculation.

== v2026.6.1

=== Dependency update
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
******************************************************************************/
package pepper.starter.services.view;

import graphql.util.Pair;
import pepper.peppermm.AbstractTask;
import pepper.peppermm.DependencyLink;
import pepper.peppermm.DependencyRelatedObject;
Expand All @@ -25,11 +26,13 @@
import pepper.peppermm.TaskTag;
import pepper.peppermm.Workpackage;

import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoUnit;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
Expand Down Expand Up @@ -131,17 +134,16 @@ else if (zonedDateTimeEnd.getHour() <= 18) {
if (startTimeControlledByDependency && !endTimeControlledByDependency) {
this.setTaskDuration(task, newStartTime, newEndTime);
newEndTime = newEndTime.plus(differenceStart, ChronoUnit.SECONDS);
task.setEndTime(newEndTime);
adjustTaskTimesHoliday(task, task.getStartTime(), newEndTime, false);
} else if (!startTimeControlledByDependency && endTimeControlledByDependency) {
this.setTaskDuration(task, newStartTime, newEndTime);
newStartTime = newStartTime.plus(differenceEnd, ChronoUnit.SECONDS);
task.setStartTime(newStartTime);
adjustTaskTimesHoliday(task, newStartTime, task.getEndTime(), true);
} else if (!startTimeControlledByDependency && !endTimeControlledByDependency) {
if (!keepDuration) {
this.setTaskDuration(task, newStartTime, newEndTime);
}
task.setStartTime(newStartTime);
task.setEndTime(newEndTime);
adjustTaskTimesHoliday(task, newStartTime, newEndTime, false);
}
if (!startTimeControlledByDependency || !endTimeControlledByDependency) {
followMoveDependency(task);
Expand All @@ -165,13 +167,13 @@ public void createTask(EObject context) {
if (context instanceof AbstractTask abstractTask) {
// The new task follows the context task and has the same duration as the context task.
if (abstractTask.getEndTime() != null && abstractTask.getStartTime() != null) {
Instant start = abstractTask.getEndTime();
Instant end = Instant.ofEpochSecond(2 * abstractTask.getEndTime().getEpochSecond() - abstractTask.getStartTime().getEpochSecond());
if (abstractTask.getEndTime().equals(abstractTask.getStartTime())) {
// If the task is a Milestone
task.setStartTime(abstractTask.getEndTime());
task.setEndTime(Instant.ofEpochSecond(2 * abstractTask.getEndTime().getEpochSecond() - abstractTask.getStartTime().getEpochSecond()));
adjustTaskTimesHoliday(task, start, end, false);
} else {
task.setStartTime(abstractTask.getEndTime().plus(1, ChronoUnit.MINUTES));
task.setEndTime(Instant.ofEpochSecond(2 * abstractTask.getEndTime().getEpochSecond() - abstractTask.getStartTime().getEpochSecond()).plus(1, ChronoUnit.MINUTES));
adjustTaskTimesHoliday(task, start.plus(1, ChronoUnit.MINUTES), end.plus(1, ChronoUnit.MINUTES), false);
}
}

Expand All @@ -183,12 +185,70 @@ public void createTask(EObject context) {
int index = parentTask.getSubTasks().indexOf(context);
parentTask.getSubTasks().add(index + 1, task);
}
} else if (context instanceof Workpackage workpackage) {
long epochSecondStartTime = Instant.now().getEpochSecond();
task.setStartTime(Instant.ofEpochMilli(epochSecondStartTime));
task.setEndTime(Instant.ofEpochMilli(epochSecondStartTime + 3600 * 4));
}
}

workpackage.getOwnedTasks().add(task);
private void adjustTaskTimesHoliday(Task task, Instant start, Instant end, boolean isTaskEndPointed) {
Pair<Instant, Instant> startEnd = new Pair<>(start, end);
if (isInstantHoliday(start, true) || isInstantHoliday(end, false)) {
startEnd = adjustTaskHoliday(start, end, isTaskEndPointed);
}
setTaskDuration(task, startEnd.first, startEnd.second);
task.setStartTime(startEnd.first);
task.setEndTime(startEnd.second);
}

private Pair<Instant, Instant> adjustTaskHoliday(Instant start, Instant end, boolean isTaskEndPointed) {
Instant newStart = start;
Instant newEnd = end;

if (isTaskEndPointed) {
while (isInstantHoliday(newEnd, false)) {
newEnd = newEnd.minus(12, ChronoUnit.HOURS); //TEMPORAIRE
}
long durationChange = newEnd.getEpochSecond() - end.getEpochSecond();
newStart = newStart.plusSeconds(durationChange);
while (isInstantHoliday(newStart, true)) {
newStart = newStart.minus(12, ChronoUnit.HOURS); //TEMPORAIRE
}
boolean test = false;
} else {
while (isInstantHoliday(newStart, true)) {
newStart = newStart.plus(12, ChronoUnit.HOURS); //TEMPORAIRE
}
long durationChange = newStart.getEpochSecond() - start.getEpochSecond();
newEnd = newEnd.plusSeconds(durationChange);
while (isInstantHoliday(newEnd, false)) {
newEnd = newEnd.plus(12, ChronoUnit.HOURS); //TEMPORAIRE
}
}

return new Pair<>(newStart, newEnd);
}

private boolean isNonWorkingDay(Temporal temporal) {
LocalDate date = null;
if (temporal instanceof Instant instant) {
date = LocalDate.ofInstant(instant, ZoneId.systemDefault());
} else if (temporal instanceof LocalDate localDate) {
date = localDate;
}
return date != null && (date.getDayOfWeek().equals(DayOfWeek.SATURDAY) || date.getDayOfWeek().equals(DayOfWeek.SUNDAY));
}

private boolean isInstantHoliday(Instant instant, boolean isStartOfTask) {
boolean isNonWorkingDay = isNonWorkingDay(instant);
boolean isPreviousNonWorkingDay = isNonWorkingDay(instant.minus(1, ChronoUnit.DAYS));
if (isStartOfTask) {
return isNonWorkingDay;
} else {
int hours = instant.atZone(ZoneId.systemDefault()).getHour();
int minutes = instant.atZone(ZoneId.systemDefault()).getMinute();
boolean isDayStart = hours == 0 && minutes == 0;
boolean cond1 = isNonWorkingDay && !isPreviousNonWorkingDay && !isDayStart;
boolean cond2 = isNonWorkingDay && isPreviousNonWorkingDay;
boolean cond3 = !isNonWorkingDay && isPreviousNonWorkingDay && isDayStart;
return cond1 || cond2 || cond3;
}
}

Expand Down Expand Up @@ -587,27 +647,23 @@ private void setTaskNewDates(Task task, DependencyLink dep) {
Instant newTaskStart = sourceEnd.plus(delay, ChronoUnit.HOURS)
.plus(startAdjustmentMinutes(bestSourceTask), ChronoUnit.MINUTES);
Instant newTaskEnd = Instant.ofEpochSecond(newTaskStart.getEpochSecond() + oldTaskEnd.getEpochSecond() - oldTaskStart.getEpochSecond());
task.setEndTime(newTaskEnd);
task.setStartTime(newTaskStart);
adjustTaskTimesHoliday(task, newTaskStart, newTaskEnd, false);
} else if (sourceStartOrEnd == StartOrEnd.START && targetStartOrEnd == StartOrEnd.START) {
Instant newTaskStart = sourceStart.plus(delay, ChronoUnit.HOURS);
Instant newTaskEnd = Instant.ofEpochSecond(newTaskStart.getEpochSecond() + oldTaskEnd.getEpochSecond() - oldTaskStart.getEpochSecond());
task.setEndTime(newTaskEnd);
task.setStartTime(newTaskStart);
adjustTaskTimesHoliday(task, newTaskStart, newTaskEnd, false);
} else if (sourceStartOrEnd == StartOrEnd.END && targetStartOrEnd == StartOrEnd.END) {
Instant newTaskEnd = sourceEnd.plus(delay, ChronoUnit.HOURS)
.plus(endAdjustmentMinutes(bestSourceTask, task), ChronoUnit.MINUTES);
Instant newTaskStart = Instant.ofEpochSecond(newTaskEnd.getEpochSecond() - (oldTaskEnd.getEpochSecond() - oldTaskStart.getEpochSecond()));
task.setEndTime(newTaskEnd);
task.setStartTime(newTaskStart);
adjustTaskTimesHoliday(task, newTaskStart, newTaskEnd, true);
} else if (sourceStartOrEnd == StartOrEnd.START && targetStartOrEnd == StartOrEnd.END) {
Instant newTaskEnd = sourceStart.plus(delay, ChronoUnit.HOURS).minus(1, ChronoUnit.MINUTES);
if (isMilestone(task)) {
newTaskEnd = newTaskEnd.plus(1, ChronoUnit.MINUTES);
}
Instant newTaskStart = Instant.ofEpochSecond(newTaskEnd.getEpochSecond() - (oldTaskEnd.getEpochSecond() - oldTaskStart.getEpochSecond()));
task.setEndTime(newTaskEnd);
task.setStartTime(newTaskStart);
adjustTaskTimesHoliday(task, newTaskStart, newTaskEnd, true);
}
}

Expand All @@ -620,6 +676,7 @@ private void setTaskNewEndDate(Task task, DependencyLink dep) {
Instant sourceEnd = getTaskEndTime(bestSourceTask);
int delay = dep.getDuration();
StartOrEnd sourceStartOrEnd = dep.getSourceKind();
StartOrEnd targetStartOrEnd = dep.getTargetKind();
Instant newTaskEnd = task.getEndTime();
if (sourceStartOrEnd == StartOrEnd.END) {
newTaskEnd = sourceEnd.plus(delay, ChronoUnit.HOURS)
Expand All @@ -631,7 +688,7 @@ private void setTaskNewEndDate(Task task, DependencyLink dep) {
}
}
setTaskDuration(task, task.getStartTime(), newTaskEnd);
task.setEndTime(newTaskEnd);
adjustTaskTimesHoliday(task, task.getStartTime(), newTaskEnd, targetStartOrEnd.equals(StartOrEnd.END));
}

/**
Expand All @@ -643,6 +700,7 @@ private void setTaskNewStartDate(Task task, DependencyLink dep) {
Instant sourceEnd = getTaskEndTime(bestSourceTask);
int delay = dep.getDuration();
StartOrEnd sourceStartOrEnd = dep.getSourceKind();
StartOrEnd targetStartOrEnd = dep.getTargetKind();
Instant newTaskStart = task.getStartTime();
if (sourceStartOrEnd == StartOrEnd.END) {
newTaskStart = sourceEnd.plus(delay, ChronoUnit.HOURS)
Expand All @@ -651,7 +709,7 @@ private void setTaskNewStartDate(Task task, DependencyLink dep) {
newTaskStart = sourceStart.plus(delay, ChronoUnit.HOURS);
}
setTaskDuration(task, task.getStartTime(), newTaskStart);
task.setStartTime(newTaskStart);
adjustTaskTimesHoliday(task, newTaskStart, task.getEndTime(), targetStartOrEnd.equals(StartOrEnd.END));
}

/**
Expand Down Expand Up @@ -681,23 +739,19 @@ private void setWorkpackageNewDates(Workpackage workpackage, DependencyLink depe
if (sourceStartOrEnd == StartOrEnd.END && targetStartOrEnd == StartOrEnd.START) {
LocalDate newWorkpackageStart = sourceEnd.plusDays(delay);
LocalDate newWorkpackageEnd = newWorkpackageStart.plusDays(duration);
workpackage.setEndDate(newWorkpackageEnd);
workpackage.setStartDate(newWorkpackageStart);
adjustWorkpackageDatesHoliday(workpackage, newWorkpackageStart, newWorkpackageEnd);
} else if (sourceStartOrEnd == StartOrEnd.START && targetStartOrEnd == StartOrEnd.START) {
LocalDate newWorkpackageStart = sourceStart.plusDays(delay);
LocalDate newWorkpackageEnd = newWorkpackageStart.plusDays(duration);
workpackage.setEndDate(newWorkpackageEnd);
workpackage.setStartDate(newWorkpackageStart);
adjustWorkpackageDatesHoliday(workpackage, newWorkpackageStart, newWorkpackageEnd);
} else if (sourceStartOrEnd == StartOrEnd.END && targetStartOrEnd == StartOrEnd.END) {
LocalDate newWorkpackageEnd = sourceEnd.plusDays(delay);
LocalDate newWorkpackageStart = newWorkpackageEnd.minusDays(duration);
workpackage.setEndDate(newWorkpackageEnd);
workpackage.setStartDate(newWorkpackageStart);
adjustWorkpackageDatesHoliday(workpackage, newWorkpackageStart, newWorkpackageEnd);
} else if (sourceStartOrEnd == StartOrEnd.START && targetStartOrEnd == StartOrEnd.END) {
LocalDate newWorkpackageEnd = sourceStart.plusDays(delay);
LocalDate newWorkpackageStart = newWorkpackageEnd.minusDays(duration);
workpackage.setEndDate(newWorkpackageEnd);
workpackage.setStartDate(newWorkpackageStart);
adjustWorkpackageDatesHoliday(workpackage, newWorkpackageStart, newWorkpackageEnd);
}
}

Expand All @@ -721,7 +775,7 @@ private void setWorkpackageNewEndDate(Workpackage workpackage, DependencyLink de
newWorkpackageEnd = sourceStart.plusDays(delay);
}
workpackage.setDuration((int) ChronoUnit.DAYS.between(workpackage.getStartDate(), newWorkpackageEnd));
workpackage.setEndDate(newWorkpackageEnd);
adjustWorkpackageDatesHoliday(workpackage, workpackage.getStartDate(), newWorkpackageEnd);
}

/**
Expand All @@ -741,7 +795,7 @@ private void setWorkpackageNewStartDate(Workpackage workpackage, DependencyLink
newWorkpackageStart = sourceStart.plusDays(delay);
}
workpackage.setDuration((int) ChronoUnit.DAYS.between(newWorkpackageStart, workpackage.getEndDate()));
workpackage.setStartDate(newWorkpackageStart);
adjustWorkpackageDatesHoliday(workpackage, newWorkpackageStart, workpackage.getEndDate());
}

private static Instant getlaterInstant(DependencyLink dep) {
Expand Down Expand Up @@ -861,23 +915,18 @@ public void createWorkpackage(EObject context) {
Workpackage newWorkpackage = PepperFactory.eINSTANCE.createWorkpackage();
newWorkpackage.setName("New Workpackage");
if (context instanceof Workpackage workpackage) {
// The new task follows the context task and has the same duration than the context task.
// The new task follows the context task and has the same duration as the context task.
if (workpackage.getEndDate() != null && workpackage.getStartDate() != null) {
newWorkpackage.setStartDate(workpackage.getEndDate().plusDays(1));
newWorkpackage.setEndDate(workpackage.getEndDate().plusDays(workpackage.getEndDate().toEpochDay() - workpackage.getStartDate().toEpochDay() + 1));
LocalDate start = workpackage.getEndDate().plusDays(1);
LocalDate end = workpackage.getEndDate().plusDays(workpackage.getEndDate().toEpochDay() - workpackage.getStartDate().toEpochDay() + 1);
adjustWorkpackageDatesHoliday(newWorkpackage, start, end);
}

EObject parent = context.eContainer();
if (parent instanceof Project project) {
int index = project.getOwnedWorkpackages().indexOf(context);
project.getOwnedWorkpackages().add(index + 1, newWorkpackage);
}
} else if (context instanceof Project project) {
LocalDate now = LocalDate.now();
newWorkpackage.setStartDate(now);
newWorkpackage.setEndDate(now.plusDays(28));

project.getOwnedWorkpackages().add(newWorkpackage);
}
}

Expand Down Expand Up @@ -910,16 +959,15 @@ public void editWorkpackage(EObject eObject, String name, String description, Lo
if (dependencies.isEmpty() || differenceEnd != differenceStart) {
if (startDateControlledByDependency && !endDateControlledByDependency) {
this.workpackageSetDuration(workpackage, startDate, endDate);
workpackage.setEndDate(endDate.plusDays(differenceStart));
adjustWorkpackageDatesHoliday(workpackage, workpackage.getStartDate(), endDate.plusDays(differenceStart));
} else if (endDateControlledByDependency && !startDateControlledByDependency) {
this.workpackageSetDuration(workpackage, startDate, endDate);
workpackage.setStartDate(startDate.plusDays(differenceEnd));
adjustWorkpackageDatesHoliday(workpackage, startDate.plusDays(differenceEnd), workpackage.getEndDate());
} else if (!startDateControlledByDependency && !endDateControlledByDependency) {
if (!keepDuration) {
this.workpackageSetDuration(workpackage, startDate, endDate);
}
workpackage.setStartDate(startDate);
workpackage.setEndDate(endDate);
adjustWorkpackageDatesHoliday(workpackage, startDate, endDate);
}
if (!startDateControlledByDependency || !endDateControlledByDependency) {
followMoveDependency(workpackage);
Expand All @@ -932,6 +980,32 @@ public void editWorkpackage(EObject eObject, String name, String description, Lo
}
}

private void adjustWorkpackageDatesHoliday(Workpackage workpackage, LocalDate start, LocalDate end) {
Pair<LocalDate, LocalDate> startEnd = new Pair<>(start, end);
if (isNonWorkingDay(start) || isNonWorkingDay(end)) {
startEnd = adjustWorkpackageHoliday(start, end);
}
workpackage.setStartDate(startEnd.first);
workpackage.setEndDate(startEnd.second);
}

private Pair<LocalDate, LocalDate> adjustWorkpackageHoliday(LocalDate start, LocalDate end) {
LocalDate newStart = start;
LocalDate newEnd = end;

while (isNonWorkingDay(newStart)) {
newStart = newStart.plusDays(1); //TEMPORAIRE
}

long durationChange = newStart.toEpochDay() - start.toEpochDay();
newEnd = newEnd.plusDays(durationChange);
while (isNonWorkingDay(newEnd)) {
newEnd = newEnd.plusDays(1); //TEMPORAIRE
}

return new Pair<>(newStart, newEnd);
}

private void workpackageSetDuration(Workpackage workpackage, LocalDate start, LocalDate end) {
int duration = (int) ChronoUnit.DAYS.between(start, end) + 1; //+1 because between(00:00, 00:59) = 0. We want 1.
workpackage.setDuration(duration);
Expand Down
Loading
Loading