TSK-1972: calculates working time in any resolution

Prior working time calculation only happened in a full day.
This commit is contained in:
Jochen Just 2022-11-16 13:18:42 +01:00 committed by Mustapha Zorgati
parent 4697fbe5be
commit 4a42a35a21
45 changed files with 1699 additions and 928 deletions

View File

@ -1,39 +1,33 @@
package pro.taskana.common.api;
import java.time.LocalTime;
import java.util.Objects;
public class LocalTimeInterval {
import pro.taskana.common.internal.Interval;
private LocalTime begin;
private LocalTime end;
/**
* LocalTimeInterval provides a closed interval using {@link LocalTime}.
*
* <p>That means both begin and end must not be <code>null</code>.
*
* <p>Note: this class has a natural ordering that is inconsistent with equals.
*/
public class LocalTimeInterval extends Interval<LocalTime>
implements Comparable<LocalTimeInterval> {
public LocalTimeInterval(LocalTime begin, LocalTime end) {
this.begin = begin;
this.end = end;
}
public boolean isValid() {
return begin != null && end != null;
}
public LocalTime getBegin() {
return begin;
}
public void setBegin(LocalTime begin) {
this.begin = begin;
}
public LocalTime getEnd() {
return end;
}
public void setEnd(LocalTime end) {
this.end = end;
super(Objects.requireNonNull(begin), Objects.requireNonNull(end));
}
/**
* Compares two LocalTimeInterval objects in regard to their {@link #getBegin() begin}.
*
* @param o the LocalTimeInterval to be compared.
* @return a negative value if <code>o</code> begins before <code>this</code>, 0 if both have the
* same begin and a positive value if <code>o</code> begins after <code>this</code>.
*/
@Override
public String toString() {
return "LocalTimeInterval [begin=" + begin + ", end=" + end + "]";
public int compareTo(LocalTimeInterval o) {
return begin.compareTo(o.getBegin());
}
}

View File

@ -1,153 +1,101 @@
package pro.taskana.common.api;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.temporal.ChronoUnit;
import java.util.EnumMap;
import java.util.Map;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
public class WorkingTimeCalculator {
@SuppressWarnings("unused")
public interface WorkingTimeCalculator {
public static final Map<DayOfWeek, LocalTimeInterval> WORKING_TIME;
/**
* Subtracts {@code workingTime} from {@code workStart}. Respects the configured working time
* schedule and Holidays.
*
* <p>The returned Instant denotes the first time in point the work time starts or in short it is
* inclusive.
*
* <p>E.g can be used for planned date calculation.
*
* @param workStart The Instant {@code workingTime} is subtracted from.
* @param workingTime The Duration to subtract from {@code workStart}. May have any resolution
* Duration supports, e.g. minutes or seconds.
* @return A new Instant which represents the subtraction of working time.
* @throws InvalidArgumentException If {@code workingTime} is negative.
*/
Instant subtractWorkingTime(Instant workStart, Duration workingTime)
throws InvalidArgumentException;
static {
WORKING_TIME = new EnumMap<>(DayOfWeek.class);
WORKING_TIME.put(
DayOfWeek.MONDAY, new LocalTimeInterval(LocalTime.of(6, 0), LocalTime.of(18, 0)));
WORKING_TIME.put(
DayOfWeek.TUESDAY, new LocalTimeInterval(LocalTime.of(6, 0), LocalTime.of(18, 0)));
WORKING_TIME.put(
DayOfWeek.WEDNESDAY, new LocalTimeInterval(LocalTime.of(6, 0), LocalTime.of(18, 0)));
WORKING_TIME.put(
DayOfWeek.THURSDAY, new LocalTimeInterval(LocalTime.of(6, 0), LocalTime.of(18, 0)));
WORKING_TIME.put(
DayOfWeek.FRIDAY, new LocalTimeInterval(LocalTime.of(6, 0), LocalTime.of(18, 0)));
WORKING_TIME.put(DayOfWeek.SATURDAY, null);
WORKING_TIME.put(DayOfWeek.SUNDAY, null);
}
/**
* Adds {@code workingTime} from {@code workStart}. Respects the configured working time schedule
* and Holidays.
*
* <p>The returned Instant denotes the first time in point the work time has ended or in short it
* is exclusive.
*
* <p>E.g can be used for due date calculation.
*
* @param workStart The Instant {@code workingTime} is added to.
* @param workingTime The Duration to add to {@code workStart}. May have any resolution Duration
* supports, e.g. minutes or seconds.
* @return A new Instant which represents the addition of working time.
* @throws InvalidArgumentException If {@code workingTime} is negative.
*/
Instant addWorkingTime(Instant workStart, Duration workingTime) throws InvalidArgumentException;
private final ZoneId zone;
private final WorkingDaysToDaysConverter converter;
/**
* Calculates the working time between {@code first} and {@code second} according to the
* configured working time schedule. The returned Duration is precise to nanoseconds.
*
* <p>This method does not impose any ordering on {@code first} or {@code second}.
*
* @param first An Instant denoting the start or end of the considered time frame.
* @param second An Instant denoting the start or end of the considered time frame.
* @return The Duration representing the working time between {@code first} and {@code to }.
* @throws InvalidArgumentException If either {@code first} or {@code second} is {@code null}.
*/
Duration workingTimeBetween(Instant first, Instant second) throws InvalidArgumentException;
public WorkingTimeCalculator(WorkingDaysToDaysConverter converter) {
this.converter = converter;
zone = ZoneId.of("UTC");
}
public Duration workingTimeBetweenTwoTimestamps(Instant from, Instant to)
/**
* Decides whether there is any working time between {@code first} and {@code second}.
*
* @see #workingTimeBetween(Instant, Instant)
*/
@SuppressWarnings("checkstyle:JavadocMethod")
default boolean isWorkingTimeBetween(Instant first, Instant second)
throws InvalidArgumentException {
checkValidInput(from, to);
Instant currentTime = from;
LocalDate currentDate = LocalDateTime.ofInstant(from, zone).toLocalDate();
LocalDate untilDate = LocalDateTime.ofInstant(to, zone).toLocalDate();
DayOfWeek weekDay = currentDate.getDayOfWeek();
if (currentDate.isEqual(untilDate)) {
return calculateDurationWithinOneDay(from, to, weekDay, currentDate);
}
Duration duration = Duration.ZERO;
duration = duration.plus(calculateDurationOfStartDay(currentTime, weekDay, currentDate));
currentTime = currentTime.plus(1, ChronoUnit.DAYS);
currentDate = currentDate.plusDays(1);
weekDay = weekDay.plus(1);
while (!currentDate.isEqual(untilDate)) {
duration = duration.plus(calculateDurationOfOneWorkDay(weekDay, currentDate));
weekDay = weekDay.plus(1);
currentDate = currentDate.plusDays(1);
currentTime = currentTime.plus(1, ChronoUnit.DAYS);
}
return duration.plus(calculateDurationOnEndDay(to, weekDay, currentDate));
return !Duration.ZERO.equals(workingTimeBetween(first, second));
}
private Duration calculateDurationWithinOneDay(
Instant from, Instant to, DayOfWeek weekday, LocalDate currentDate) {
LocalTimeInterval workHours = WORKING_TIME.get(weekday);
if (WORKING_TIME.get(weekday) != null && !converter.isHoliday(currentDate)) {
LocalTime start = workHours.getBegin();
LocalTime end = workHours.getEnd();
LocalTime fromTime = from.atZone(zone).toLocalTime();
LocalTime toTime = to.atZone(zone).toLocalTime();
/**
* Decides whether {@code instant} is a working day.
*
* @param instant The Instant to check. May not be {@code null}.
* @return {@code true} if {@code instant} is a working day. {@code false} otherwise.
*/
boolean isWorkingDay(Instant instant);
if (!fromTime.isBefore(start) && toTime.isBefore(end)) {
return Duration.between(from, to);
} else if (fromTime.isBefore(start)) {
if (toTime.isAfter(end)) {
return addWorkingHoursOfOneDay(weekday);
} else if (!toTime.isBefore(start)) {
return Duration.between(start, toTime);
}
} else if (fromTime.isBefore(end)) {
return Duration.between(fromTime, end);
}
}
return Duration.ZERO;
}
/**
* Decides whether {@code instant} is a weekend day.
*
* @param instant The Instant to check. May not be {@code null}.
* @return {@code true} if {@code instant} is a weekend day. {@code false} otherwise.
*/
boolean isWeekend(Instant instant);
private Duration calculateDurationOfOneWorkDay(DayOfWeek weekday, LocalDate date) {
if (WORKING_TIME.get(weekday) != null && !converter.isHoliday(date)) {
return addWorkingHoursOfOneDay(weekday);
}
return Duration.ZERO;
}
/**
* Decides whether { @code instant} is a holiday.
*
* @param instant The Instant to check. May not be {@code null}.
* @return {@code true} if {@code instant} is a holiday. {@code false} otherwise.
*/
boolean isHoliday(Instant instant);
private Duration calculateDurationOfStartDay(
Instant startDay, DayOfWeek weekday, LocalDate date) {
LocalTimeInterval workHours = WORKING_TIME.get(weekday);
if (WORKING_TIME.get(weekday) != null && !converter.isHoliday(date)) {
LocalTime fromTime = startDay.atZone(zone).toLocalTime();
LocalTime end = workHours.getEnd();
if (fromTime.isBefore(workHours.getBegin())) {
return addWorkingHoursOfOneDay(weekday);
} else if (fromTime.isBefore(end)) {
return Duration.between(fromTime, end);
}
}
return Duration.ZERO;
}
private Duration calculateDurationOnEndDay(Instant endDate, DayOfWeek weekday, LocalDate date) {
LocalTimeInterval workHours = WORKING_TIME.get(weekday);
if (WORKING_TIME.get(weekday) != null && !converter.isHoliday(date)) {
LocalTime start = workHours.getBegin();
LocalTime toTime = endDate.atZone(zone).toLocalTime();
if (toTime.isAfter(workHours.getEnd())) {
return addWorkingHoursOfOneDay(weekday);
} else if (!toTime.isBefore(start)) {
return Duration.between(start, toTime);
}
}
return Duration.ZERO;
}
private void checkValidInput(Instant from, Instant to) throws InvalidArgumentException {
if (from == null || to == null || from.compareTo(to) > 0) {
throw new InvalidArgumentException("Instants are invalid.");
}
for (LocalTimeInterval interval : WORKING_TIME.values()) {
if (interval != null && !interval.isValid()) {
throw new InvalidArgumentException(
"The work period doesn't have two LocalTimes for start and end.");
}
}
}
private Duration addWorkingHoursOfOneDay(DayOfWeek weekday) {
LocalTimeInterval workHours = WORKING_TIME.get(weekday);
if (workHours.isValid()) {
return Duration.between(workHours.getBegin(), workHours.getEnd());
} else {
return Duration.ZERO;
}
}
/**
* Decides whether {@code instant} is a holiday in Germany.
*
* @param instant The Instant to check. May not be {@code null}.
* @return {@code true} if {@code instant} is a holiday in Germany. {@code false} otherwise.
*/
boolean isGermanHoliday(Instant instant);
}

View File

@ -11,9 +11,9 @@ import java.util.Objects;
*/
public class Interval<T extends Comparable<? super T>> {
private final T begin;
protected final T begin;
private final T end;
protected final T end;
public Interval(T begin, T end) {
this.begin = begin;

View File

@ -4,8 +4,10 @@ import static java.util.function.Predicate.not;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
@ -23,6 +25,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pro.taskana.common.api.CustomHoliday;
import pro.taskana.common.api.LocalTimeInterval;
import pro.taskana.common.api.TaskanaRole;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.api.exceptions.WrongCustomHolidayFormatException;
@ -47,6 +50,7 @@ public class TaskanaConfigurationInitializer {
PROPERTY_INITIALIZER_BY_CLASS.put(Duration.class, new DurationPropertyParser());
PROPERTY_INITIALIZER_BY_CLASS.put(Instant.class, new InstantPropertyParser());
PROPERTY_INITIALIZER_BY_CLASS.put(List.class, new ListPropertyParser());
PROPERTY_INITIALIZER_BY_CLASS.put(Map.class, new MapPropertyParser());
}
private TaskanaConfigurationInitializer() {
@ -161,6 +165,109 @@ public class TaskanaConfigurationInitializer {
TaskanaProperty taskanaProperty);
}
static class MapPropertyParser implements PropertyParser<Map<?, ?>> {
@Override
public Optional<Map<?, ?>> initialize(
Map<String, String> properties,
String separator,
Field field,
TaskanaProperty taskanaProperty) {
if (!Map.class.isAssignableFrom(field.getType())) {
throw new SystemException(
String.format(
"Cannot initialize field '%s' because field type '%s' is not a Map",
field, field.getType()));
}
ParameterizedType genericType = (ParameterizedType) field.getGenericType();
Type[] actualTypeArguments = genericType.getActualTypeArguments();
Class<?> keyClass = (Class<?>) actualTypeArguments[0];
Type valueClass = actualTypeArguments[1];
// Parses property files into a Map using the following layout: <Property>.<Key> = <value>
String propertyKey = taskanaProperty.value();
Map<?, ?> mapFromProperties =
properties.keySet().stream()
.filter(it -> it.startsWith(propertyKey))
.map(
it -> {
// Keys of the map entry is everything after the propertyKey + "."
String keyAsString = it.substring(propertyKey.length() + 1);
Object key = getStringAsObject(keyAsString, keyClass);
// Value of the map entry is the value from the property
String propertyValue = properties.get(it);
Object value = getStringAsObject(propertyValue, separator, valueClass);
return Pair.of(key, value);
})
.collect(Collectors.toMap(Pair::getLeft, Pair::getRight));
if (mapFromProperties.isEmpty()) {
return Optional.empty();
} else {
return Optional.of(mapFromProperties);
}
}
private Object getStringAsObject(String string, String separator, Type type) {
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type rawType = parameterizedType.getRawType();
if (rawType.equals(Set.class)) {
return getStringAsSet(
string, separator, (Class<?>) parameterizedType.getActualTypeArguments()[0]);
}
} else if (type instanceof Class) {
return getStringAsObject(string, (Class<?>) type);
}
throw new SystemException(
String.format(
"Cannot parse property value '%s': It is not convertible to '%s'",
string, type.getTypeName()));
}
private Object getStringAsObject(String string, Class<?> targetClass) {
if (targetClass.isEnum()) {
Map<String, ?> enumConstantsByLowerCasedName =
Arrays.stream(targetClass.getEnumConstants())
.collect(Collectors.toMap(e -> e.toString().toLowerCase(), Function.identity()));
Object o = enumConstantsByLowerCasedName.get(string.toLowerCase());
if (o == null) {
throw new SystemException(
String.format(
"Invalid property value '%s': Valid values are '%s' or '%s",
string,
enumConstantsByLowerCasedName.keySet(),
Arrays.toString(targetClass.getEnumConstants())));
}
return o;
} else if (targetClass.equals(LocalTimeInterval.class)) {
List<String> startAndEnd = splitStringAndTrimElements(string, "-");
if (startAndEnd.size() != 2) {
throw new SystemException("Cannot convert " + string + " to " + LocalTimeInterval.class);
}
LocalTime start = LocalTime.parse(startAndEnd.get(0));
LocalTime end = LocalTime.parse(startAndEnd.get(1));
if (end.equals(LocalTime.MIN)) {
end = LocalTime.MAX;
}
return new LocalTimeInterval(start, end);
} else {
throw new SystemException(
String.format(
"Cannot parse property value '%s': It is not convertible to '%s'",
string, targetClass.getName()));
}
}
private Set<?> getStringAsSet(String string, String separator, Class<?> elementClass) {
return splitStringAndTrimElements(string, separator).stream()
.map(it -> getStringAsObject(it, elementClass))
.collect(Collectors.toSet());
}
}
static class ListPropertyParser implements PropertyParser<List<?>> {
@Override
public Optional<List<?>> initialize(

View File

@ -1,13 +1,8 @@
package pro.taskana.common.api;
package pro.taskana.common.internal.workingtime;
import static java.time.temporal.ChronoUnit.DAYS;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
@ -16,15 +11,16 @@ import java.util.Set;
import java.util.stream.LongStream;
import java.util.stream.LongStream.Builder;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.api.CustomHoliday;
/**
* The WorkingDaysToDaysConverter provides a method to convert an age in working days into an age in
* days.
*/
public class WorkingDaysToDaysConverter {
public class HolidaySchedule {
// offset in days from easter sunday
private static final long OFFSET_EASTER_SUNDAY = 0;
private static final long OFFSET_GOOD_FRIDAY = -2; // Good Friday
private static final long OFFSET_EASTER_MONDAY = 1; // Easter Monday
private static final long OFFSET_ASCENSION_DAY = 39; // Ascension Day
@ -45,7 +41,7 @@ public class WorkingDaysToDaysConverter {
private final Set<CustomHoliday> customHolidays;
private final EasterCalculator easterCalculator;
public WorkingDaysToDaysConverter(boolean germanHolidaysEnabled, boolean corpusChristiEnabled) {
public HolidaySchedule(boolean germanHolidaysEnabled, boolean corpusChristiEnabled) {
this(germanHolidaysEnabled, corpusChristiEnabled, Collections.emptySet());
}
@ -57,7 +53,7 @@ public class WorkingDaysToDaysConverter {
* germanHolidaysEnabled and thus only validated if German holidays are enabled.
* @param customHolidays additional custom holidays
*/
public WorkingDaysToDaysConverter(
public HolidaySchedule(
boolean germanHolidaysEnabled,
boolean corpusChristiEnabled,
Collection<CustomHoliday> customHolidays) {
@ -67,33 +63,6 @@ public class WorkingDaysToDaysConverter {
easterCalculator = new EasterCalculator();
}
public Instant addWorkingDaysToInstant(Instant instant, Duration workingDays) {
long days = convertWorkingDaysToDays(instant, workingDays.toDays(), ZeroDirection.ADD_DAYS);
return instant.plus(Duration.ofDays(days));
}
public Instant subtractWorkingDaysFromInstant(Instant instant, Duration workingDays) {
long days = convertWorkingDaysToDays(instant, -workingDays.toDays(), ZeroDirection.SUB_DAYS);
return instant.plus(Duration.ofDays(days));
}
// counts working days between two dates, exclusive for both margins.
public boolean hasWorkingDaysInBetween(Instant left, Instant right) {
long days = Duration.between(left, right).abs().toDays();
Instant firstInstant = left.isBefore(right) ? left : right;
return LongStream.range(1, days).anyMatch(day -> isWorkingDay(firstInstant.plus(day, DAYS)));
}
public boolean isWorkingDay(Instant referenceDate) {
LocalDate dateToCheck = LocalDateTime.ofInstant(referenceDate, ZoneOffset.UTC).toLocalDate();
return !isWeekend(dateToCheck) && !isHoliday(dateToCheck);
}
public boolean isWeekend(LocalDate dateToCheck) {
return dateToCheck.getDayOfWeek().equals(DayOfWeek.SATURDAY)
|| dateToCheck.getDayOfWeek().equals(DayOfWeek.SUNDAY);
}
public boolean isHoliday(LocalDate date) {
if (germanHolidaysEnabled && isGermanHoliday(date)) {
return true;
@ -113,6 +82,7 @@ public class WorkingDaysToDaysConverter {
Builder builder =
LongStream.builder()
.add(OFFSET_EASTER_SUNDAY)
.add(OFFSET_GOOD_FRIDAY)
.add(OFFSET_EASTER_MONDAY)
.add(OFFSET_ASCENSION_DAY)
@ -125,29 +95,6 @@ public class WorkingDaysToDaysConverter {
return builder.build().anyMatch(c -> c == diffFromEasterSunday);
}
private long convertWorkingDaysToDays(
final Instant startTime, long numberOfDays, ZeroDirection zeroDirection) {
if (startTime == null) {
throw new SystemException(
"Internal Error: convertWorkingDaysToDays was called with a null startTime");
}
int direction = calculateDirection(numberOfDays, zeroDirection);
long limit = Math.abs(numberOfDays);
return LongStream.iterate(0, i -> i + direction)
.filter(day -> isWorkingDay(startTime.plus(day, DAYS)))
.skip(limit)
.findFirst()
.orElse(0);
}
private int calculateDirection(long numberOfDays, ZeroDirection zeroDirection) {
if (numberOfDays == 0) {
return zeroDirection.getDirection();
} else {
return numberOfDays >= 0 ? 1 : -1;
}
}
@Override
public String toString() {
return "WorkingDaysToDaysConverter [germanHolidaysEnabled="
@ -161,22 +108,8 @@ public class WorkingDaysToDaysConverter {
+ "]";
}
private enum ZeroDirection {
SUB_DAYS(-1),
ADD_DAYS(1);
private final int direction;
ZeroDirection(int direction) {
this.direction = direction;
}
public int getDirection() {
return direction;
}
}
static class EasterCalculator {
LocalDate cachedEasterDay;
/**

View File

@ -0,0 +1,272 @@
package pro.taskana.common.internal.workingtime;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneOffset;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedSet;
import pro.taskana.common.api.LocalTimeInterval;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
static final ZoneOffset UTC = ZoneOffset.UTC;
private final HolidaySchedule holidaySchedule;
private final WorkingTimeSchedule workingTimeSchedule;
public WorkingTimeCalculatorImpl(
HolidaySchedule holidaySchedule, Map<DayOfWeek, Set<LocalTimeInterval>> workingTimeSchedule) {
this.holidaySchedule = holidaySchedule;
this.workingTimeSchedule = new WorkingTimeSchedule(workingTimeSchedule);
}
@Override
public Instant subtractWorkingTime(Instant workStart, Duration workingTime)
throws InvalidArgumentException {
validatePositiveDuration(workingTime);
WorkSlot workSlot = getWorkSlotOrPrevious(toLocalDateTime(workStart));
return workSlot.subtractWorkingTime(workStart, workingTime);
}
@Override
public Instant addWorkingTime(Instant workStart, Duration workingTime)
throws InvalidArgumentException {
validatePositiveDuration(workingTime);
WorkSlot bestMatchingWorkSlot = getWorkSlotOrNext(toLocalDateTime(workStart));
return bestMatchingWorkSlot.addWorkingTime(workStart, workingTime);
}
@Override
public Duration workingTimeBetween(Instant first, Instant second)
throws InvalidArgumentException {
validateNonNullInstants(first, second);
Instant from;
Instant to;
if (first.isAfter(second)) {
from = second;
to = first;
} else {
from = first;
to = second;
}
WorkSlot bestMatchingWorkSlot = getWorkSlotOrNext(toLocalDateTime(from));
Instant earliestWorkStart = max(from, bestMatchingWorkSlot.start);
Instant endOfWorkSlot = bestMatchingWorkSlot.end;
if (endOfWorkSlot.compareTo(to) >= 0) {
if (bestMatchingWorkSlot.start.compareTo(to) <= 0) {
// easy part. _from_ and _to_ are in the same work slot
return Duration.between(earliestWorkStart, to);
} else {
// _from_ and _to_ are before the bestMatchingWorkSlot aka between two work slots. We simply
// drop it
return Duration.ZERO;
}
} else {
// Take the current duration and add the working time starting after this work slot.
return Duration.between(earliestWorkStart, endOfWorkSlot)
.plus(workingTimeBetween(endOfWorkSlot, to));
}
}
@Override
public boolean isWorkingDay(Instant instant) {
return workingTimeSchedule.isWorkingDay(toDayOfWeek(instant)) && !isHoliday(instant);
}
@Override
public boolean isWeekend(Instant instant) {
DayOfWeek dayOfWeek = toDayOfWeek(instant);
return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY;
}
@Override
public boolean isHoliday(Instant instant) {
return holidaySchedule.isHoliday(toLocalDate(instant));
}
@Override
public boolean isGermanHoliday(Instant instant) {
return holidaySchedule.isGermanHoliday(toLocalDate(instant));
}
private void validateNonNullInstants(Instant first, Instant second) {
if (first == null || second == null) {
throw new InvalidArgumentException("Neither first nor second may be null.");
}
}
private void validatePositiveDuration(Duration workingTime) {
if (workingTime.isNegative()) {
throw new InvalidArgumentException("Duration must be zero or positive.");
}
}
/**
* Returns the WorkSlot that matches best <code>currentDateTime</code>. If currentDateTime is
* within a WorkSlot that WorkSlot is returned, if currentDateTime is not within a WorkSlot the
* next WorkSlot is returned.
*
* @param currentDateTime The LocalDateTime we want the best matching WorkSlot for. May not be
* <code>null</code>.
* @return The WorkSlot that matches best <code>currentDateTime</code> if we want to add.
*/
private WorkSlot getWorkSlotOrNext(LocalDateTime currentDateTime) {
LocalDate currentDate = currentDateTime.toLocalDate();
// We do not work on Holidays
if (holidaySchedule.isHoliday(currentDate)) {
return getWorkSlotOrNext(getDayAfter(currentDateTime));
}
SortedSet<LocalTimeInterval> workSlotsOfWorkingDay =
workingTimeSchedule.workSlotsFor(currentDate.getDayOfWeek());
// We are looking for the first workingSlot whose end is after the current time
Optional<LocalTimeInterval> workSlotEndingAfterCurrentTime =
workSlotsOfWorkingDay.stream()
.filter(it -> it.getEnd().isAfter(currentDateTime.toLocalTime()))
.findFirst();
return workSlotEndingAfterCurrentTime
.map(it -> new WorkSlot(currentDate, it))
.orElseGet(
() ->
// we started after the last working slot on that day, the next start time is the
// first working slot of the next working day.
getWorkSlotOrNext(getDayAfter(currentDateTime)));
}
/**
* Returns the WorkSlot that matches best <code>currentDateTime</code>. If currentDateTime is
* within a WorkSlot that WorkSlot is returned, if currentDateTime is not within a WorkSlot the
* previous WorkSlot is returned.
*
* @param currentDateTime The LocalDateTime we want the best matching WorkSlot for. May not be
* <code>null</code>.
* @return The WorkSlot that matches best <code>currentDateTime</code> if we want to subtract.
*/
private WorkSlot getWorkSlotOrPrevious(LocalDateTime currentDateTime) {
LocalDate currentDate = currentDateTime.toLocalDate();
// We do not work on Holidays
if (holidaySchedule.isHoliday(currentDate)) {
return getWorkSlotOrPrevious(getDayBefore(currentDateTime));
}
SortedSet<LocalTimeInterval> workSlotsOfWorkingDay =
workingTimeSchedule.workSlotsForReversed(currentDate.getDayOfWeek());
// We are looking for the last workingSlot whose begin is before or equals the current time
Optional<LocalTimeInterval> workSlotStartingBeforeCurrentTime =
workSlotsOfWorkingDay.stream()
// we use beforeOrEquals because begin is inclusive
.filter(it -> isBeforeOrEquals(it.getBegin(), currentDateTime))
.findFirst();
return workSlotStartingBeforeCurrentTime
.map(it -> new WorkSlot(currentDate, it))
.orElseGet(
() ->
// we started before the first working slot on that day, the next start time is the
// last working slot of the previous working day.
getWorkSlotOrPrevious(getDayBefore(currentDateTime)));
}
private static boolean isBeforeOrEquals(LocalTime time, LocalDateTime currentDateTime) {
return !time.isAfter(currentDateTime.toLocalTime());
}
private LocalDateTime getDayAfter(LocalDateTime current) {
return LocalDateTime.of(current.toLocalDate().plusDays(1), LocalTime.MIN);
}
private LocalDateTime getDayBefore(LocalDateTime current) {
return LocalDateTime.of(current.toLocalDate().minusDays(1), LocalTime.MAX);
}
private LocalDateTime toLocalDateTime(Instant instant) {
return LocalDateTime.ofInstant(instant, UTC);
}
private LocalDate toLocalDate(Instant instant) {
return LocalDate.ofInstant(instant, UTC);
}
private DayOfWeek toDayOfWeek(Instant instant) {
return toLocalDate(instant).getDayOfWeek();
}
private static Instant max(Instant a, Instant b) {
if (a.isAfter(b)) {
return a;
} else {
return b;
}
}
private static Instant min(Instant a, Instant b) {
if (a.isBefore(b)) {
return a;
} else {
return b;
}
}
class WorkSlot {
private final Instant start;
private final Instant end;
public WorkSlot(LocalDate day, LocalTimeInterval interval) {
this.start = LocalDateTime.of(day, interval.getBegin()).toInstant(UTC);
if (interval.getEnd().equals(LocalTime.MAX)) {
this.end = day.plusDays(1).atStartOfDay().toInstant(UTC);
} else {
this.end = LocalDateTime.of(day, interval.getEnd()).toInstant(UTC);
}
}
public Instant addWorkingTime(Instant workStart, Duration workingTime) {
// _workStart_ might be outside the working hours. We need to adjust the start accordingly.
Instant earliestWorkStart = max(workStart, start);
Duration untilEndOfWorkSlot = Duration.between(earliestWorkStart, end);
if (workingTime.compareTo(untilEndOfWorkSlot) <= 0) {
// easy part. It is due within the same work slot
return earliestWorkStart.plus(workingTime);
} else {
// we subtract the duration of the currentWorkingSlot
Duration remainingWorkingTime = workingTime.minus(untilEndOfWorkSlot);
// We continue to calculate the dueDate by starting from an workStart outside the current
// working slot and the remainingWorkingTime
return next().addWorkingTime(end, remainingWorkingTime);
}
}
public Instant subtractWorkingTime(Instant workStart, Duration workingTime) {
// _workStart_ might be outside the working hours. We need to adjust the end accordingly.
Instant latestWorkEnd = min(workStart, end);
Duration untilStartOfWorkSlot = Duration.between(start, latestWorkEnd);
if (workingTime.compareTo(untilStartOfWorkSlot) <= 0) {
// easy part. It is due within the same work slot
return latestWorkEnd.minus(workingTime);
} else {
Duration remainingWorkingTime = workingTime.minus(untilStartOfWorkSlot);
return previous().subtractWorkingTime(start, remainingWorkingTime);
}
}
private WorkSlot previous() {
// We need to subtract a nanosecond because start is inclusive
return getWorkSlotOrPrevious(toLocalDateTime(start.minusNanos(1)));
}
private WorkSlot next() {
return getWorkSlotOrNext(toLocalDateTime(end));
}
}
}

View File

@ -0,0 +1,103 @@
package pro.taskana.common.internal.workingtime;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.function.Function;
import pro.taskana.common.api.LocalTimeInterval;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.internal.util.Pair;
class WorkingTimeSchedule {
// Holds WorkSlots as SortedSet of LocalTimeInterval sorted ascending and descending by DayOfWeek
private final Map<DayOfWeek, Pair<SortedSet<LocalTimeInterval>, SortedSet<LocalTimeInterval>>>
workingTimeByDayOfWeek = new EnumMap<>(DayOfWeek.class);
WorkingTimeSchedule(Map<DayOfWeek, Set<LocalTimeInterval>> workingTimeByDayOfWeek) {
if (workingTimeByDayOfWeek.isEmpty()) {
throw new InvalidArgumentException("At least one day of the week needs to have working time");
}
for (Entry<DayOfWeek, Set<LocalTimeInterval>> dayOfWeekSetEntry :
workingTimeByDayOfWeek.entrySet()) {
SortedSet<LocalTimeInterval> intervalsAscending =
sortedSetOf(dayOfWeekSetEntry.getValue(), null);
LocalTime previousEnd = null;
for (LocalTimeInterval current : intervalsAscending) {
if (previousEnd != null && current.getBegin().isBefore(previousEnd)) {
throw new IllegalArgumentException(
"Working time is overlapping for " + intervalsAscending);
}
previousEnd = current.getEnd();
}
SortedSet<LocalTimeInterval> intervalsDescending =
sortedSetOf(intervalsAscending, Comparator.reverseOrder());
this.workingTimeByDayOfWeek.put(
dayOfWeekSetEntry.getKey(), Pair.of(intervalsAscending, intervalsDescending));
}
}
/**
* Determines whether <code>dayOfWeek</code> is a working day.
*
* @param dayOfWeek The dayOfWeek. May not be <code>null</code>>.
* @return <code>true</code> if it is a working day, <code>false</code> otherwise.
*/
public boolean isWorkingDay(DayOfWeek dayOfWeek) {
return !workSlotsFor(dayOfWeek).isEmpty();
}
/**
* Returns all LocalTimeIntervals for <code>dayOfWeek</code> sorted ascending by their beginning.
*
* @param dayOfWeek The DayOfWeek to get LocalTimeIntervals for.
* @return All LocalTimeIntervals sorted ascending by their beginning. May be empty.
*/
public SortedSet<LocalTimeInterval> workSlotsFor(DayOfWeek dayOfWeek) {
return workSlotsForBySortOrder(dayOfWeek, Pair::getLeft);
}
/**
* Returns all LocalTimeIntervals for <code>dayOfWeek</code> sorted descending by their beginning.
*
* @param dayOfWeek The DayOfWeek to get LocalTimeIntervals for.
* @return All LocalTimeIntervals sorted descending by their beginning. May be empty.
*/
public SortedSet<LocalTimeInterval> workSlotsForReversed(DayOfWeek dayOfWeek) {
return workSlotsForBySortOrder(dayOfWeek, Pair::getRight);
}
private SortedSet<LocalTimeInterval> workSlotsForBySortOrder(
DayOfWeek dayOfWeek,
Function<
Pair<SortedSet<LocalTimeInterval>, SortedSet<LocalTimeInterval>>,
SortedSet<LocalTimeInterval>>
pairChooser) {
Pair<SortedSet<LocalTimeInterval>, SortedSet<LocalTimeInterval>> bothIntervalSets =
workingTimeByDayOfWeek.get(dayOfWeek);
if (bothIntervalSets == null) {
return Collections.emptySortedSet();
}
return pairChooser.apply(bothIntervalSets);
}
private SortedSet<LocalTimeInterval> sortedSetOf(
Set<LocalTimeInterval> original, Comparator<LocalTimeInterval> comparator) {
SortedSet<LocalTimeInterval> sorted = new TreeSet<>(comparator);
sorted.addAll(original);
return Collections.unmodifiableSortedSet(sorted);
}
}

View File

@ -0,0 +1,19 @@
package pro.taskana.common.api;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.Test;
public class LocalTimeIntervalTest {
@Test
void naturalOrderingIsDefinedByBegin() {
LocalTimeInterval ltiOne = new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX);
LocalTimeInterval ltiTwo =
new LocalTimeInterval(LocalTime.MIN.plus(1, ChronoUnit.MILLIS), LocalTime.MAX);
assertThat(ltiOne).isLessThan(ltiTwo);
}
}

View File

@ -1,214 +0,0 @@
package pro.taskana.common.api;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicContainer;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import pro.taskana.common.api.WorkingDaysToDaysConverter.EasterCalculator;
/** Test for the WorkingDaysToDaysConverter. */
class WorkingDaysToDaysConverterTest {
private final WorkingDaysToDaysConverter converter;
WorkingDaysToDaysConverterTest() {
CustomHoliday dayOfReformation = CustomHoliday.of(31, 10);
CustomHoliday allSaintsDays = CustomHoliday.of(1, 11);
converter =
new WorkingDaysToDaysConverter(true, false, List.of(dayOfReformation, allSaintsDays));
}
@TestFactory
Stream<DynamicTest> should_NotDetectCorpusChristiAsHoliday_When_CorpusChristiIsDisabled() {
DynamicTest year1980 =
DynamicTest.dynamicTest(
"year 1980",
() -> assertThat(converter.isGermanHoliday(LocalDate.parse("1980-06-05"))).isFalse());
DynamicTest year2020 =
DynamicTest.dynamicTest(
"year 2020",
() -> assertThat(converter.isGermanHoliday(LocalDate.parse("2020-06-11"))).isFalse());
return Stream.of(year1980, year2020);
}
@TestFactory
Stream<DynamicNode> should_DetectCorpusChristiAsHoliday_When_CorpusChristiIsEnabled() {
WorkingDaysToDaysConverter converter = new WorkingDaysToDaysConverter(true, true);
DynamicTest year1980 =
DynamicTest.dynamicTest(
"year 1980",
() -> assertThat(converter.isGermanHoliday(LocalDate.parse("1980-06-05"))).isTrue());
DynamicTest year2020 =
DynamicTest.dynamicTest(
"year 2020",
() -> assertThat(converter.isGermanHoliday(LocalDate.parse("2020-06-11"))).isTrue());
return Stream.of(year1980, year2020);
}
@TestFactory
Stream<DynamicNode> testHasWorkingInBetween() {
Instant thursday = Instant.parse("2020-04-30T07:12:00.000Z");
Instant friday = Instant.parse("2020-05-01T07:12:00.000Z"); // german holiday
Instant saturday = Instant.parse("2020-05-02T07:12:00.000Z");
Instant sunday = Instant.parse("2020-05-03T07:12:00.000Z");
Instant monday = Instant.parse("2020-05-04T07:12:00.000Z");
Instant tuesday = Instant.parse("2020-05-05T07:12:00.000Z");
DynamicContainer noWorkingDaysInBetween =
DynamicContainer.dynamicContainer(
"no working days in between",
Stream.of(
DynamicTest.dynamicTest(
"tuesday <-> tuesday",
() ->
assertThat(converter.hasWorkingDaysInBetween(tuesday, tuesday)).isFalse()),
DynamicTest.dynamicTest(
"thursday <-> saturday (friday is holiday)",
() ->
assertThat(converter.hasWorkingDaysInBetween(thursday, saturday))
.isFalse()),
DynamicTest.dynamicTest(
"friday <-> friday",
() -> assertThat(converter.hasWorkingDaysInBetween(friday, friday)).isFalse()),
DynamicTest.dynamicTest(
"friday <-> monday",
() -> assertThat(converter.hasWorkingDaysInBetween(friday, monday)).isFalse()),
DynamicTest.dynamicTest(
"saturday <-> monday",
() ->
assertThat(converter.hasWorkingDaysInBetween(saturday, monday)).isFalse()),
DynamicTest.dynamicTest(
"sunday <-> monday",
() -> assertThat(converter.hasWorkingDaysInBetween(sunday, monday)).isFalse()),
DynamicTest.dynamicTest(
"monday <-> monday",
() -> assertThat(converter.hasWorkingDaysInBetween(sunday, monday)).isFalse()),
DynamicTest.dynamicTest(
"monday <-> sunday",
() -> assertThat(converter.hasWorkingDaysInBetween(monday, sunday)).isFalse()),
DynamicTest.dynamicTest(
"monday <-> friday",
() ->
assertThat(converter.hasWorkingDaysInBetween(monday, friday)).isFalse())));
DynamicContainer hasWorkingDaysInBetween =
DynamicContainer.dynamicContainer(
"has working days in between",
Stream.of(
DynamicTest.dynamicTest(
"friday <-> tuesday",
() -> assertThat(converter.hasWorkingDaysInBetween(friday, tuesday)).isTrue()),
DynamicTest.dynamicTest(
"sunday <-> tuesday",
() ->
assertThat(converter.hasWorkingDaysInBetween(sunday, tuesday)).isTrue())));
return Stream.of(noWorkingDaysInBetween, hasWorkingDaysInBetween);
}
@Test
void testConvertWorkingDaysToDaysForTasks() {
Instant thursday0201 = Instant.parse("2018-02-01T07:00:00.000Z");
Instant days =
converter.subtractWorkingDaysFromInstant(
thursday0201, Duration.ofDays(7)); // = tuesday (sat + sun)
assertThat(days).isEqualTo(thursday0201.minus(9, ChronoUnit.DAYS));
days =
converter.subtractWorkingDaysFromInstant(
thursday0201, Duration.ofDays(6)); // = wednesday (sat + sun)
assertThat(days).isEqualTo(thursday0201.minus(8, ChronoUnit.DAYS));
days =
converter.subtractWorkingDaysFromInstant(
thursday0201, Duration.ofDays(5)); // = thursday (sat + sun)
assertThat(days).isEqualTo(thursday0201.minus(7, ChronoUnit.DAYS));
days = converter.subtractWorkingDaysFromInstant(thursday0201, Duration.ofDays(4)); // = friday
assertThat(days).isEqualTo(thursday0201.minus(6, ChronoUnit.DAYS));
days = converter.subtractWorkingDaysFromInstant(thursday0201, Duration.ofDays(3)); // monday
assertThat(days).isEqualTo(thursday0201.minus(3, ChronoUnit.DAYS));
days = converter.subtractWorkingDaysFromInstant(thursday0201, Duration.ofDays(2)); // tuesday
assertThat(days).isEqualTo(thursday0201.minus(2, ChronoUnit.DAYS));
days = converter.subtractWorkingDaysFromInstant(thursday0201, Duration.ofDays(1)); // wednesday
assertThat(days).isEqualTo(thursday0201.minus(1, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(0)); // = thursday
assertThat(days).isEqualTo(thursday0201.plus(0, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(1)); // fri
assertThat(days).isEqualTo(thursday0201.plus(1, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(2)); // mon
assertThat(days).isEqualTo(thursday0201.plus(4, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(3)); // tues
assertThat(days).isEqualTo(thursday0201.plus(5, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(4)); // we
assertThat(days).isEqualTo(thursday0201.plus(6, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(5)); // thurs
assertThat(days).isEqualTo(thursday0201.plus(7, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(6)); // fri
assertThat(days).isEqualTo(thursday0201.plus(8, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(7)); // mon
assertThat(days).isEqualTo(thursday0201.plus(11, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(8)); // tue
assertThat(days).isEqualTo(thursday0201.plus(12, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(9)); // we
assertThat(days).isEqualTo(thursday0201.plus(13, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(10)); // thu
assertThat(days).isEqualTo(thursday0201.plus(14, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(thursday0201, Duration.ofDays(11)); // fri
assertThat(days).isEqualTo(thursday0201.plus(15, ChronoUnit.DAYS));
}
@Test
void testConvertWorkingDaysToDaysForKarFreitag() {
Instant gruenDonnerstag2018 = Instant.parse("2018-03-29T01:00:00.000Z");
Instant days = converter.addWorkingDaysToInstant(gruenDonnerstag2018, Duration.ofDays(0));
assertThat(days).isEqualTo(gruenDonnerstag2018.plus(0, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(gruenDonnerstag2018, Duration.ofDays(1)); // Karfreitag
assertThat(days).isEqualTo(gruenDonnerstag2018.plus(5, ChronoUnit.DAYS)); // osterdienstag
days = converter.addWorkingDaysToInstant(gruenDonnerstag2018, Duration.ofDays(2)); // Karfreitag
assertThat(days).isEqualTo(gruenDonnerstag2018.plus(6, ChronoUnit.DAYS)); // ostermittwoch
}
@Test
void testConvertWorkingDaysToDaysForHolidays() {
Instant freitag0427 = Instant.parse("2018-04-27T19:00:00.000Z");
Instant days = converter.addWorkingDaysToInstant(freitag0427, Duration.ofDays(0));
assertThat(days).isEqualTo(freitag0427.plus(0, ChronoUnit.DAYS));
days = converter.addWorkingDaysToInstant(freitag0427, Duration.ofDays(1));
assertThat(days).isEqualTo(freitag0427.plus(3, ChronoUnit.DAYS)); // 30.4.
days = converter.addWorkingDaysToInstant(freitag0427, Duration.ofDays(2));
assertThat(days).isEqualTo(freitag0427.plus(5, ChronoUnit.DAYS)); // 2.5.
}
@Test
void testGetEasterSunday() {
EasterCalculator easterCalculator = new EasterCalculator();
assertThat(easterCalculator.getEasterSunday(2018)).isEqualTo(LocalDate.of(2018, 4, 1));
assertThat(easterCalculator.getEasterSunday(2019)).isEqualTo(LocalDate.of(2019, 4, 21));
assertThat(easterCalculator.getEasterSunday(2020)).isEqualTo(LocalDate.of(2020, 4, 12));
assertThat(easterCalculator.getEasterSunday(2021)).isEqualTo(LocalDate.of(2021, 4, 4));
assertThat(easterCalculator.getEasterSunday(2022)).isEqualTo(LocalDate.of(2022, 4, 17));
assertThat(easterCalculator.getEasterSunday(2023)).isEqualTo(LocalDate.of(2023, 4, 9));
assertThat(easterCalculator.getEasterSunday(2024)).isEqualTo(LocalDate.of(2024, 3, 31));
assertThat(easterCalculator.getEasterSunday(2025)).isEqualTo(LocalDate.of(2025, 4, 20));
assertThat(easterCalculator.getEasterSunday(2026)).isEqualTo(LocalDate.of(2026, 4, 5));
assertThat(easterCalculator.getEasterSunday(2027)).isEqualTo(LocalDate.of(2027, 3, 28));
assertThat(easterCalculator.getEasterSunday(2028)).isEqualTo(LocalDate.of(2028, 4, 16));
assertThat(easterCalculator.getEasterSunday(2029)).isEqualTo(LocalDate.of(2029, 4, 1));
assertThat(easterCalculator.getEasterSunday(2030)).isEqualTo(LocalDate.of(2030, 4, 21));
assertThat(easterCalculator.getEasterSunday(2031)).isEqualTo(LocalDate.of(2031, 4, 13));
assertThat(easterCalculator.getEasterSunday(2032)).isEqualTo(LocalDate.of(2032, 3, 28));
assertThat(easterCalculator.getEasterSunday(2033)).isEqualTo(LocalDate.of(2033, 4, 17));
assertThat(easterCalculator.getEasterSunday(2034)).isEqualTo(LocalDate.of(2034, 4, 9));
assertThat(easterCalculator.getEasterSunday(2035)).isEqualTo(LocalDate.of(2035, 3, 25));
assertThat(easterCalculator.getEasterSunday(2040)).isEqualTo(LocalDate.of(2040, 4, 1));
assertThat(easterCalculator.getEasterSunday(2050)).isEqualTo(LocalDate.of(2050, 4, 10));
assertThat(easterCalculator.getEasterSunday(2100)).isEqualTo(LocalDate.of(2100, 3, 28));
}
}

View File

@ -1,160 +0,0 @@
package pro.taskana.common.api;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.time.Duration;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import org.junit.jupiter.api.function.ThrowingConsumer;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.internal.util.Quadruple;
class WorkingTimeCalculatorTest {
private final WorkingTimeCalculator calculator;
WorkingTimeCalculatorTest() {
WorkingDaysToDaysConverter converter = new WorkingDaysToDaysConverter(true, false);
calculator = new WorkingTimeCalculator(converter);
}
@Test
void should_throwInvalidArgumentException_When_FromTimeIsAfterUntilTime() {
Instant from = Instant.parse("2021-09-30T12:00:00.000Z");
Instant to = Instant.parse("2021-09-30T09:00:00.000Z");
assertThatThrownBy(() -> calculator.workingTimeBetweenTwoTimestamps(from, to))
.isInstanceOf(InvalidArgumentException.class)
.hasMessage("Instants are invalid.");
}
@Test
void should_throwInvalidArgumentException_When_FromIsNull() {
Instant to = Instant.parse("2021-09-30T09:00:00.000Z");
assertThatThrownBy(() -> calculator.workingTimeBetweenTwoTimestamps(null, to))
.isInstanceOf(InvalidArgumentException.class)
.hasMessage("Instants are invalid.");
}
@Test
void should_ReturnMultipleWorkingTimes_When_CalculatorUsedMultipleTimes() throws Exception {
Instant from1 = Instant.parse("2021-09-30T10:02:00.000Z");
Instant to1 = Instant.parse("2021-09-30T10:38:00.000Z");
Duration duration1 = calculator.workingTimeBetweenTwoTimestamps(from1, to1);
assertThat(duration1).isEqualTo(Duration.of(36, ChronoUnit.MINUTES));
Instant from2 = Instant.parse("2021-09-27T10:00:00.000Z");
Instant to2 = Instant.parse("2021-10-02T15:00:00.000Z");
Duration duration2 = calculator.workingTimeBetweenTwoTimestamps(from2, to2);
assertThat(duration2).isEqualTo(Duration.of(56, ChronoUnit.HOURS));
}
@TestFactory
Stream<DynamicTest> should_ReturnWorkingTime() {
List<Quadruple<String, Instant, Instant, Duration>> valuesForTests =
List.of(
// Test instants that are within same day
Quadruple.of(
"Delta in hours",
Instant.parse("2021-09-30T09:00:00.000Z"),
Instant.parse("2021-09-30T14:00:00.000Z"),
Duration.of(5, ChronoUnit.HOURS)),
Quadruple.of(
"Delta in minutes",
Instant.parse("2021-09-30T09:02:00.000Z"),
Instant.parse("2021-09-30T09:38:00.000Z"),
Duration.of(36, ChronoUnit.MINUTES)),
Quadruple.of(
"Delta in seconds",
Instant.parse("2021-09-30T15:00:00.000Z"),
Instant.parse("2021-09-30T15:00:01.000Z"),
Duration.of(1, ChronoUnit.SECONDS)),
Quadruple.of(
"Delta in milliseconds",
Instant.parse("2021-09-30T15:00:00.000Z"),
Instant.parse("2021-09-30T15:00:00.111Z"),
Duration.of(111, ChronoUnit.MILLIS)),
Quadruple.of(
"Delta in all time units",
Instant.parse("2021-09-30T15:00:00.000Z"),
Instant.parse("2021-09-30T16:01:01.001Z"),
Duration.of(1, ChronoUnit.HOURS).plusMinutes(1).plusSeconds(1).plusMillis(1)),
Quadruple.of(
"Start time before working hours",
Instant.parse("2021-09-30T05:00:00.000Z"),
Instant.parse("2021-09-30T07:00:00.000Z"),
Duration.of(1, ChronoUnit.HOURS)),
Quadruple.of(
"End time after working hours",
Instant.parse("2021-09-30T17:00:00.000Z"),
Instant.parse("2021-09-30T19:00:00.000Z"),
Duration.of(1, ChronoUnit.HOURS)),
Quadruple.of(
"On holiday",
Instant.parse("2021-01-01T11:00:00.000Z"),
Instant.parse("2021-01-01T14:00:00.000Z"),
Duration.ZERO),
Quadruple.of(
"Start and end after hours",
Instant.parse("2021-09-30T19:00:00.000Z"),
Instant.parse("2021-09-30T20:00:00.000Z"),
Duration.ZERO),
// Test instants that are over two days
Quadruple.of(
"Two days, start before working hours",
Instant.parse("2021-09-30T05:00:00.000Z"),
Instant.parse("2021-10-01T10:00:00.000Z"),
Duration.of(12 + 4, ChronoUnit.HOURS)),
Quadruple.of(
"Two days, start after working hours",
Instant.parse("2021-09-30T19:00:00.000Z"),
Instant.parse("2021-10-01T10:00:00.000Z"),
Duration.of(4, ChronoUnit.HOURS)),
Quadruple.of(
"Two days, end before working hours",
Instant.parse("2021-09-30T17:00:00.000Z"),
Instant.parse("2021-10-01T05:00:00.000Z"),
Duration.of(1, ChronoUnit.HOURS)),
Quadruple.of(
"Two days, end after working hours",
Instant.parse("2021-09-30T17:00:00.000Z"),
Instant.parse("2021-10-01T19:00:00.000Z"),
Duration.of(1 + 12, ChronoUnit.HOURS)),
// Test instants that are over multiple days
Quadruple.of(
"Separated by weekend",
Instant.parse("2021-09-24T15:00:00.000Z"),
Instant.parse("2021-09-27T10:00:00.000Z"),
Duration.of(3 + 4, ChronoUnit.HOURS)),
Quadruple.of(
"Separated by holiday",
Instant.parse("2021-05-12T17:00:00.000Z"),
Instant.parse("2021-05-14T07:00:00.000Z"),
Duration.of(1 + 1, ChronoUnit.HOURS)),
Quadruple.of(
"From Monday to Saturday",
Instant.parse("2021-09-27T09:00:00.000Z"),
Instant.parse("2021-10-02T14:00:00.000Z"),
Duration.of(9 + 12 + 12 + 12 + 12, ChronoUnit.HOURS)));
ThrowingConsumer<Quadruple<String, Instant, Instant, Duration>> test =
q -> {
Duration duration =
calculator.workingTimeBetweenTwoTimestamps(q.getSecond(), q.getThird());
assertThat(duration).isEqualTo(q.getFourth());
};
return DynamicTest.stream(valuesForTests.iterator(), Quadruple::getFirst, test);
}
}

View File

@ -0,0 +1,75 @@
package pro.taskana.common.internal.workingtime;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalDate;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestFactory;
import pro.taskana.common.api.CustomHoliday;
import pro.taskana.common.internal.workingtime.HolidaySchedule.EasterCalculator;
/** Test for the HolidaySchedule. */
class HolidayScheduleTest {
@TestFactory
Stream<DynamicTest> should_NotDetectCorpusChristiAsHoliday_When_CorpusChristiIsDisabled() {
CustomHoliday dayOfReformation = CustomHoliday.of(31, 10);
CustomHoliday allSaintsDays = CustomHoliday.of(1, 11);
HolidaySchedule schedule =
new HolidaySchedule(true, false, List.of(dayOfReformation, allSaintsDays));
DynamicTest year1980 =
DynamicTest.dynamicTest(
"year 1980",
() -> assertThat(schedule.isGermanHoliday(LocalDate.parse("1980-06-05"))).isFalse());
DynamicTest year2020 =
DynamicTest.dynamicTest(
"year 2020",
() -> assertThat(schedule.isGermanHoliday(LocalDate.parse("2020-06-11"))).isFalse());
return Stream.of(year1980, year2020);
}
@TestFactory
Stream<DynamicNode> should_DetectCorpusChristiAsHoliday_When_CorpusChristiIsEnabled() {
HolidaySchedule schedule = new HolidaySchedule(true, true);
DynamicTest year1980 =
DynamicTest.dynamicTest(
"year 1980",
() -> assertThat(schedule.isGermanHoliday(LocalDate.parse("1980-06-05"))).isTrue());
DynamicTest year2020 =
DynamicTest.dynamicTest(
"year 2020",
() -> assertThat(schedule.isGermanHoliday(LocalDate.parse("2020-06-11"))).isTrue());
return Stream.of(year1980, year2020);
}
@Test
void testGetEasterSunday() {
EasterCalculator easterCalculator = new EasterCalculator();
assertThat(easterCalculator.getEasterSunday(2018)).isEqualTo(LocalDate.of(2018, 4, 1));
assertThat(easterCalculator.getEasterSunday(2019)).isEqualTo(LocalDate.of(2019, 4, 21));
assertThat(easterCalculator.getEasterSunday(2020)).isEqualTo(LocalDate.of(2020, 4, 12));
assertThat(easterCalculator.getEasterSunday(2021)).isEqualTo(LocalDate.of(2021, 4, 4));
assertThat(easterCalculator.getEasterSunday(2022)).isEqualTo(LocalDate.of(2022, 4, 17));
assertThat(easterCalculator.getEasterSunday(2023)).isEqualTo(LocalDate.of(2023, 4, 9));
assertThat(easterCalculator.getEasterSunday(2024)).isEqualTo(LocalDate.of(2024, 3, 31));
assertThat(easterCalculator.getEasterSunday(2025)).isEqualTo(LocalDate.of(2025, 4, 20));
assertThat(easterCalculator.getEasterSunday(2026)).isEqualTo(LocalDate.of(2026, 4, 5));
assertThat(easterCalculator.getEasterSunday(2027)).isEqualTo(LocalDate.of(2027, 3, 28));
assertThat(easterCalculator.getEasterSunday(2028)).isEqualTo(LocalDate.of(2028, 4, 16));
assertThat(easterCalculator.getEasterSunday(2029)).isEqualTo(LocalDate.of(2029, 4, 1));
assertThat(easterCalculator.getEasterSunday(2030)).isEqualTo(LocalDate.of(2030, 4, 21));
assertThat(easterCalculator.getEasterSunday(2031)).isEqualTo(LocalDate.of(2031, 4, 13));
assertThat(easterCalculator.getEasterSunday(2032)).isEqualTo(LocalDate.of(2032, 3, 28));
assertThat(easterCalculator.getEasterSunday(2033)).isEqualTo(LocalDate.of(2033, 4, 17));
assertThat(easterCalculator.getEasterSunday(2034)).isEqualTo(LocalDate.of(2034, 4, 9));
assertThat(easterCalculator.getEasterSunday(2035)).isEqualTo(LocalDate.of(2035, 3, 25));
assertThat(easterCalculator.getEasterSunday(2040)).isEqualTo(LocalDate.of(2040, 4, 1));
assertThat(easterCalculator.getEasterSunday(2050)).isEqualTo(LocalDate.of(2050, 4, 10));
assertThat(easterCalculator.getEasterSunday(2100)).isEqualTo(LocalDate.of(2100, 3, 28));
}
}

View File

@ -0,0 +1,501 @@
package pro.taskana.common.internal.workingtime;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import pro.taskana.common.api.LocalTimeInterval;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
class WorkingTimeCalculatorImplTest {
@Nested
class UsualWorkingSlotsWithSingleBreak {
private final Set<LocalTimeInterval> standardWorkingSlots =
Set.of(
new LocalTimeInterval(LocalTime.of(6, 0), LocalTime.of(12, 0)),
new LocalTimeInterval(LocalTime.of(13, 0), LocalTime.of(18, 0)));
private final WorkingTimeCalculator cut =
new WorkingTimeCalculatorImpl(
new HolidaySchedule(true, false),
Map.of(
DayOfWeek.MONDAY, standardWorkingSlots,
DayOfWeek.TUESDAY, standardWorkingSlots,
DayOfWeek.WEDNESDAY, standardWorkingSlots,
DayOfWeek.THURSDAY, standardWorkingSlots,
DayOfWeek.FRIDAY, standardWorkingSlots));
@Nested
class WorkingTimeAddition {
/*
* Examples (assuming a working day is from 06:00 - 12:00 and 13:00 - 18:00):
* Start | duration in minutes | Due Date
* Tue, 09:00 | 42 | Tue, 09:42
* Wed, 09:00 | 3*60 | Wed, 12:00
* Thu, 09:00 | 3*60 + 1 | Thu, 13:01
* Fri, 09:00 | 11*60 | Mon, 09:00
* Holy Thursday, 09:00 | 8*60 + 1 | Tue, 08:01
*/
@Test
void withinWorkSlot() {
Instant tuesday9oClock = Instant.parse("2022-11-15T09:00:00.000Z");
Instant dueDate = cut.addWorkingTime(tuesday9oClock, Duration.ofMinutes(42));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-15T09:42:00.000Z"));
}
@Test
void untilEndOfWorkSlot() {
Instant wednesday9oClock = Instant.parse("2022-11-16T09:00:00.000Z");
Instant dueDate = cut.addWorkingTime(wednesday9oClock, Duration.ofHours(3));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-16T12:00:00.000Z"));
}
@Test
void spanningToNextWorkSlot() {
Instant thursday9oClock = Instant.parse("2022-11-17T09:00:00.000Z");
Instant dueDate = cut.addWorkingTime(thursday9oClock, Duration.ofMinutes(3 * 60 + 1));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-17T13:01:00.000Z"));
}
@Test
void spanningOverWeekend() {
Instant friday9oClock = Instant.parse("2022-11-18T09:00:00.000Z");
Instant dueDate = cut.addWorkingTime(friday9oClock, Duration.ofHours(11));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-21T09:00:00.000Z"));
}
@Test
void spanningOverHoliday() {
Instant holyThursday = Instant.parse("2022-04-14T09:00:00.000Z");
Instant dueDate = cut.addWorkingTime(holyThursday, Duration.ofHours(11));
Instant tuesDayAfterEaster = Instant.parse("2022-04-19T09:00:00.000Z");
assertThat(dueDate).isEqualTo(tuesDayAfterEaster);
}
@Test
void startOnHoliday() {
Instant holyFriday = Instant.parse("2022-04-15T09:00:00.000Z");
Instant dueDate = cut.addWorkingTime(holyFriday, Duration.ofHours(2));
Instant tuesDayAfterEaster = Instant.parse("2022-04-19T08:00:00.000Z");
assertThat(dueDate).isEqualTo(tuesDayAfterEaster);
}
@Test
void withDurationOfZero() {
Instant mondayHalfPastNine = Instant.parse("2022-11-14T09:30:00.000Z");
Instant dueDate = cut.addWorkingTime(mondayHalfPastNine, Duration.ZERO);
assertThat(dueDate).isEqualTo(mondayHalfPastNine);
}
@Test
void withDurationOfZeroOnHolySaturday() {
Instant holySaturdayHalfPastNine = Instant.parse("2022-04-16T09:30:00.000Z");
Instant dueDate = cut.addWorkingTime(holySaturdayHalfPastNine, Duration.ZERO);
assertThat(dueDate).isEqualTo(Instant.parse("2022-04-19T06:00:00.000Z"));
}
@Test
void currentTimeIsBeforeWorkingHours() {
Instant wednesday5oClock = Instant.parse("2022-11-16T05:00:00.000Z");
Instant dueDate = cut.addWorkingTime(wednesday5oClock, Duration.ofHours(1));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-16T07:00:00.000Z"));
}
@Test
void currentTimeIsAfterWorkingHours() {
Instant wednesday19oClock = Instant.parse("2022-11-16T19:00:00.000Z");
Instant dueDate = cut.addWorkingTime(wednesday19oClock, Duration.ofHours(1));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-17T07:00:00.000Z"));
}
@Test
void withNegativeDuration() {
assertThatExceptionOfType(InvalidArgumentException.class)
.isThrownBy(() -> cut.addWorkingTime(Instant.now(), Duration.ofMillis(-1)));
}
@Test
void withNullInstant() {
assertThatExceptionOfType(NullPointerException.class)
.isThrownBy(() -> cut.addWorkingTime(null, Duration.ofMillis(1)));
}
}
@Nested
class WorkingTimeSubtraction {
/*
* Examples (assuming a working day is from 06:00 - 12:00 and 13:00 - 18:00):
* Start | duration in minutes | Due Date
* Tue, 09:00 | 42 | Tue, 08:18
* Wed, 09:00 | 3*60 | Wed, 06:00
* Thu, 16:00 | 3*60 + 1 | Thu, 11:59
* Mon, 17:00 | 11*60 | Fri, 17:00
* Tuesday after Easter, 09:00 | 3*60 + 1 | Thu, 17:59
*/
@Test
void withinWorkSlot() {
Instant tuesday9oClock = Instant.parse("2022-11-15T09:00:00.000Z");
Instant dueDate = cut.subtractWorkingTime(tuesday9oClock, Duration.ofMinutes(42));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-15T08:18:00.000Z"));
}
@Test
void untilEndOfWorkSlot() {
Instant wednesday9oClock = Instant.parse("2022-11-16T09:00:00.000Z");
Instant dueDate = cut.subtractWorkingTime(wednesday9oClock, Duration.ofHours(3));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-16T06:00:00.000Z"));
}
@Test
void spanningToPreviousWorkSlot() {
Instant thursday16oClock = Instant.parse("2022-11-17T16:00:00.000Z");
Instant dueDate = cut.subtractWorkingTime(thursday16oClock, Duration.ofMinutes(3 * 60 + 1));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-17T11:59:00.000Z"));
}
@Test
void spanningOverWeekend() {
Instant monday17oClock = Instant.parse("2022-11-14T17:00:00.000Z");
Instant dueDate = cut.subtractWorkingTime(monday17oClock, Duration.ofHours(11));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-11T17:00:00.000Z"));
}
@Test
void spanningOverHoliday() {
Instant tuesdayAfterEaster = Instant.parse("2022-04-19T09:00:00.000Z");
Instant dueDate =
cut.subtractWorkingTime(tuesdayAfterEaster, Duration.ofMinutes(3 * 60 + 1));
Instant holyThursday = Instant.parse("2022-04-14T17:59:00.000Z");
assertThat(dueDate).isEqualTo(holyThursday);
}
@Test
void startOnHoliday() {
Instant holyFriday = Instant.parse("2022-04-15T09:00:00.000Z");
Instant dueDate = cut.subtractWorkingTime(holyFriday, Duration.ofHours(2));
Instant tuesDayAfterEaster = Instant.parse("2022-04-14T16:00:00.000Z");
assertThat(dueDate).isEqualTo(tuesDayAfterEaster);
}
@Test
void withDurationOfZero() {
Instant mondayHalfPastNine = Instant.parse("2022-11-14T09:30:00.000Z");
Instant dueDate = cut.subtractWorkingTime(mondayHalfPastNine, Duration.ZERO);
assertThat(dueDate).isEqualTo(mondayHalfPastNine);
}
@Test
void withDurationOfZeroOnHolySaturday() {
Instant holySaturdayHalfPastNine = Instant.parse("2022-04-16T09:30:00.000Z");
Instant dueDate = cut.subtractWorkingTime(holySaturdayHalfPastNine, Duration.ZERO);
assertThat(dueDate).isEqualTo(Instant.parse("2022-04-14T18:00:00.000Z"));
}
@Test
void currentTimeIsBeforeWorkingHours() {
Instant wednesday1230 = Instant.parse("2022-11-16T12:30:00.000Z");
Instant dueDate = cut.subtractWorkingTime(wednesday1230, Duration.ofHours(1));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-16T11:00:00.000Z"));
}
@Test
void currentTimeIsAfterWorkingHours() {
Instant wednesday5oClock = Instant.parse("2022-11-16T05:00:00.000Z");
Instant dueDate = cut.subtractWorkingTime(wednesday5oClock, Duration.ofHours(1));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-15T17:00:00.000Z"));
}
@Test
void withNegativeDuration() {
assertThatExceptionOfType(InvalidArgumentException.class)
.isThrownBy(() -> cut.subtractWorkingTime(Instant.now(), Duration.ofMillis(-1)));
}
@Test
void withNullInstant() {
assertThatExceptionOfType(NullPointerException.class)
.isThrownBy(() -> cut.subtractWorkingTime(null, Duration.ofMillis(1)));
}
}
@Nested
class WorkingTimeBetweenTwoInstants {
/*
* Examples (assuming a working day is from 06:00 - 12:00 and 13:00 - 18:00):
* Start | duration in minutes | Due Date
* Tue, 09:00 | 42 | Tue, 08:18
* Wed, 09:00 | 3*60 | Wed, 06:00
* Thu, 16:00 | 3*60 + 1 | Thu, 11:59
* Mon, 17:00 | 11*60 | Fri, 17:00
* Tuesday after Easter, 09:00 | 3*60 + 1 | Thu, 17:59
*/
@Test
void precisionIsLowerThanDays() {
Instant from = Instant.parse("2022-11-15T09:00:00.000Z");
Instant to = Instant.parse("2022-11-15T09:00:00.001Z");
Duration workingTime = cut.workingTimeBetween(from, to);
assertThat(workingTime).isEqualTo(Duration.ofMillis(1));
}
@Test
void timestampsInSameWorkSlot() {
Instant tuesday9oClock = Instant.parse("2022-11-15T09:00:00.000Z");
Instant tuesday10oClock = Instant.parse("2022-11-15T10:00:00.000Z");
Duration workingTime = cut.workingTimeBetween(tuesday9oClock, tuesday10oClock);
assertThat(workingTime).isEqualTo(Duration.ofHours(1));
}
@Test
void timestampsInDifferentWorkSlot() {
Instant tuesday9oClock = Instant.parse("2022-11-15T09:00:00.000Z");
Instant tuesday10oClock = Instant.parse("2022-11-15T14:00:00.000Z");
Duration workingTime = cut.workingTimeBetween(tuesday9oClock, tuesday10oClock);
assertThat(workingTime).isEqualTo(Duration.ofHours(4));
}
@Test
void timestampsSpanningHoliday() {
Instant holyThursday = Instant.parse("2022-04-14T17:00:00.000Z");
Instant tuesdayAfterEaster = Instant.parse("2022-04-19T09:00:00.000Z");
Duration workingTime = cut.workingTimeBetween(holyThursday, tuesdayAfterEaster);
assertThat(workingTime).isEqualTo(Duration.ofHours(4));
}
@Test
void dropTimeIfFromIsOutsideWorkSlot() {
Instant tuesday5oClock = Instant.parse("2022-11-15T05:00:00.000Z");
Instant tuesday7oClock = Instant.parse("2022-11-15T07:00:00.000Z");
Duration workingTime = cut.workingTimeBetween(tuesday5oClock, tuesday7oClock);
assertThat(workingTime).isEqualTo(Duration.ofHours(1));
}
@Test
void dropRemainingTimeIfToIsOutsideWorkSlot() {
Instant tuesday1100 = Instant.parse("2022-11-15T11:00:00.000Z");
Instant tuesday1230 = Instant.parse("2022-11-15T12:30:00.000Z");
Duration workingTime = cut.workingTimeBetween(tuesday1100, tuesday1230);
assertThat(workingTime).isEqualTo(Duration.ofHours(1));
}
@Test
void worksIfFromIsAfterTo() {
Instant from = Instant.parse("2023-01-09T10:11:00.000Z");
Instant to = Instant.parse("2023-01-09T10:10:00.000Z");
Duration workingTime = cut.workingTimeBetween(from, to);
assertThat(workingTime).isEqualTo(Duration.ofMinutes(1));
}
@Test
void failsIfFromIsNull() {
assertThatExceptionOfType(InvalidArgumentException.class)
.isThrownBy(() -> cut.workingTimeBetween(null, Instant.now()));
}
@Test
void failsIfToIsNull() {
assertThatExceptionOfType(InvalidArgumentException.class)
.isThrownBy(() -> cut.workingTimeBetween(Instant.now(), null));
}
}
}
@Nested
class WorkSlotWithTimeIntervalsWithoutBreak {
private final Set<LocalTimeInterval> standardWorkday =
Set.of(
new LocalTimeInterval(LocalTime.of(9, 0), LocalTime.of(12, 0)),
new LocalTimeInterval(LocalTime.of(12, 0), LocalTime.of(17, 0)));
private final WorkingTimeCalculator cut =
new WorkingTimeCalculatorImpl(
new HolidaySchedule(true, false),
Map.of(
DayOfWeek.MONDAY, standardWorkday,
DayOfWeek.TUESDAY, standardWorkday,
DayOfWeek.WEDNESDAY, standardWorkday,
DayOfWeek.THURSDAY, standardWorkday,
DayOfWeek.FRIDAY, standardWorkday));
@Test
void addTimeToMatchEndOfFirstAndStartOfSecondSlot() {
Instant wednesday9oClock = Instant.parse("2022-11-16T09:00:00.000Z");
Instant dueDate = cut.addWorkingTime(wednesday9oClock, Duration.ofHours(3));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-16T12:00:00.000Z"));
}
@Test
void subtractTimeToMatchEndOfFirstAndStartOfSecondSlot() {
Instant wednesday15oClock = Instant.parse("2022-11-16T15:00:00.000Z");
Instant dueDate = cut.subtractWorkingTime(wednesday15oClock, Duration.ofHours(3));
assertThat(dueDate).isEqualTo(Instant.parse("2022-11-16T12:00:00.000Z"));
}
}
@Nested
class WorkSlotSpansCompleteDay {
private final Set<LocalTimeInterval> completeWorkDay =
Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX));
private final WorkingTimeCalculator cut =
new WorkingTimeCalculatorImpl(
new HolidaySchedule(true, false),
Map.of(
DayOfWeek.MONDAY, completeWorkDay,
DayOfWeek.TUESDAY, completeWorkDay,
DayOfWeek.WEDNESDAY, completeWorkDay,
DayOfWeek.THURSDAY, completeWorkDay,
DayOfWeek.FRIDAY, completeWorkDay));
@Test
void withDurationOfZeroOnHolySaturday() {
Instant holySaturdayHalfPastNine = Instant.parse("2022-04-16T09:30:00.000Z");
Instant dueDate = cut.subtractWorkingTime(holySaturdayHalfPastNine, Duration.ZERO);
assertThat(dueDate).isEqualTo(Instant.parse("2022-04-15T00:00:00Z"));
}
@Test
void calculatesWorkingTimeBetweenCorrectlySpanningWorkDays() {
Instant start = Instant.parse("2023-01-09T23:00:00.000Z");
Instant end = Instant.parse("2023-01-10T01:00:00.000Z");
Duration duration = cut.workingTimeBetween(start, end);
assertThat(duration).isEqualTo(Duration.ofHours(2));
}
@Test
void addsWorkingTimeCorrectly() {
Instant start = Instant.parse("2023-01-09T23:00:00.000Z");
Instant end = cut.addWorkingTime(start, Duration.ofDays(1));
assertThat(end).isEqualTo(Instant.parse("2023-01-10T23:00:00.000Z"));
}
@Test
void subtractsWorkingTimeCorrectly() {
Instant end = Instant.parse("2023-01-10T23:00:00.000Z");
Instant start = cut.subtractWorkingTime(end, Duration.ofDays(1));
assertThat(start).isEqualTo(Instant.parse("2023-01-09T23:00:00.000Z"));
}
}
@Nested
class WorkingDayDetermination {
private final WorkingTimeCalculator cut =
new WorkingTimeCalculatorImpl(
new HolidaySchedule(true, false),
Map.of(DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX))));
@Test
void returnsTrueIfWorkingTimeScheduleIsDefinedForDayOfWeek() {
Instant sunday = Instant.parse("2023-02-26T12:30:00.000Z");
boolean isWorkingDay = cut.isWorkingDay(sunday);
assertThat(isWorkingDay).isTrue();
}
@Test
void returnsFalseIfNoWorkingTimeScheduleIsDefinedForDayOfWeek() {
Instant monday = Instant.parse("2023-02-27T12:30:00.000Z");
boolean isWorkingDay = cut.isWorkingDay(monday);
assertThat(isWorkingDay).isFalse();
}
@Test
void returnsFalseForHolidays() {
Instant easterSunday = Instant.parse("2023-04-09T12:30:00.000Z");
boolean isWorkingDay = cut.isWorkingDay(easterSunday);
assertThat(isWorkingDay).isFalse();
}
@Test
void failForNull() {
assertThatExceptionOfType(NullPointerException.class)
.isThrownBy(() -> cut.isWorkingDay(null));
}
}
}

View File

@ -0,0 +1,53 @@
package pro.taskana.common.internal.workingtime;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import java.time.DayOfWeek;
import java.time.LocalTime;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import pro.taskana.common.api.LocalTimeInterval;
public class WorkingTimeScheduleTest {
@Test
void creationFailsIfWorkingTimesOverlap() {
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(
() ->
new WorkingTimeSchedule(
Map.of(
DayOfWeek.MONDAY,
Set.of(
new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX),
new LocalTimeInterval(LocalTime.NOON, LocalTime.MAX)))));
}
@Test
void workSlotsForReturnsUnmodifiableSets() {
WorkingTimeSchedule cut =
new WorkingTimeSchedule(
Map.of(DayOfWeek.MONDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX))));
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(
() ->
cut.workSlotsFor(DayOfWeek.MONDAY)
.add(new LocalTimeInterval(LocalTime.NOON, LocalTime.MIDNIGHT)));
}
@Test
void workSlotsForReversedReturnsUnmodifiableSets() {
WorkingTimeSchedule cut =
new WorkingTimeSchedule(
Map.of(DayOfWeek.MONDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX))));
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(
() ->
cut.workSlotsForReversed(DayOfWeek.MONDAY)
.add(new LocalTimeInterval(LocalTime.NOON, LocalTime.MIDNIGHT)));
}
}

View File

@ -20,5 +20,9 @@ taskana.jobs.history.cleanup.minimumAge=P15D
taskana.german.holidays.enabled=true
taskana.german.holidays.corpus-christi.enabled=false
taskana.history.deletion.on.task.deletion.enabled=true
taskana.workingtime.schedule.MONDAY=00:00-00:00
taskana.workingtime.schedule.TUESDAY=00:00-00:00
taskana.workingtime.schedule.WEDNESDAY=00:00-00:00
taskana.workingtime.schedule.THURSDAY=00:00-00:00
taskana.workingtime.schedule.FRIDAY=00:00-00:00

View File

@ -67,8 +67,11 @@ import pro.taskana.common.api.exceptions.TaskanaException;
import pro.taskana.common.api.exceptions.TaskanaRuntimeException;
import pro.taskana.common.internal.InternalTaskanaEngine;
import pro.taskana.common.internal.Interval;
import pro.taskana.common.internal.TaskanaEngineImpl;
import pro.taskana.common.internal.logging.LoggingAspect;
import pro.taskana.common.internal.util.MapCreator;
import pro.taskana.common.internal.workingtime.HolidaySchedule;
import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl;
import pro.taskana.testapi.TaskanaIntegrationTest;
/**
@ -425,6 +428,26 @@ class ArchitectureTest {
classes().that().areAssignableTo(Throwable.class).should().bePublic().check(importedClasses);
}
@Test
void classesShouldNotUseWorkingDaysToDaysConverter() {
classes()
.that()
.areNotAssignableFrom(ArchitectureTest.class)
.and()
.areNotAssignableTo(WorkingTimeCalculatorImpl.class)
.and()
.areNotAssignableTo(TaskanaEngineImpl.class)
.and()
.haveSimpleNameNotEndingWith("Test")
.should()
.onlyDependOnClassesThat()
.areNotAssignableTo(HolidaySchedule.class)
.because(
"we want to enforce the usage of the WorkingTimeCalculator"
+ " instead of the WorkingDaysToDaysConverter")
.check(importedClasses);
}
// endregion
// region Helper Methods

View File

@ -34,7 +34,7 @@ import pro.taskana.classification.api.models.ClassificationSummary;
import pro.taskana.classification.internal.models.ClassificationImpl;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.TaskanaRole;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.ConcurrencyException;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.MismatchedRoleException;
@ -60,7 +60,7 @@ class UpdateClassificationAccTest {
@TaskanaInject TaskanaEngine taskanaEngine;
@TaskanaInject TaskService taskService;
@TaskanaInject WorkbasketService workbasketService;
@TaskanaInject WorkingDaysToDaysConverter converter;
@TaskanaInject WorkingTimeCalculator workingTimeCalculator;
@TaskanaInject CurrentUserContext currentUserContext;
@WithAccessId(user = "businessadmin")
@ -238,7 +238,7 @@ class UpdateClassificationAccTest {
classificationService.updateClassification(classification);
runAssociatedJobs();
validateTaskProperties(before, directLinkedTask, taskService, converter, 15, 1);
validateTaskProperties(before, directLinkedTask, taskService, workingTimeCalculator, 15, 1);
}
@WithAccessId(user = "businessadmin")
@ -257,7 +257,8 @@ class UpdateClassificationAccTest {
classificationService.updateClassification(classification);
runAssociatedJobs();
validateTaskProperties(before, directLinkedTask, taskService, converter, 13, 1000);
validateTaskProperties(
before, directLinkedTask, taskService, workingTimeCalculator, 13, 1000);
}
@WithAccessId(user = "businessadmin")
@ -278,7 +279,8 @@ class UpdateClassificationAccTest {
classificationService.updateClassification(classification);
runAssociatedJobs();
validateTaskProperties(before, directLinkedTask, taskService, converter, 15, 1000);
validateTaskProperties(
before, directLinkedTask, taskService, workingTimeCalculator, 15, 1000);
}
@WithAccessId(user = "businessadmin")
@ -318,7 +320,7 @@ class UpdateClassificationAccTest {
before,
indirectLinkedTasks,
taskService,
converter,
workingTimeCalculator,
input.getRight().getLeft(),
input.getRight().getRight());
};
@ -369,7 +371,7 @@ class UpdateClassificationAccTest {
before,
indirectLinkedTasks,
taskService,
converter,
workingTimeCalculator,
input.getRight().getLeft(),
input.getRight().getRight());
};
@ -422,7 +424,7 @@ class UpdateClassificationAccTest {
before,
indirectLinkedTasks,
taskService,
converter,
workingTimeCalculator,
input.getRight().getLeft(),
input.getRight().getRight());
};
@ -475,7 +477,7 @@ class UpdateClassificationAccTest {
before,
indirectLinkedTasks,
taskService,
converter,
workingTimeCalculator,
input.getRight().getLeft(),
input.getRight().getRight());
};
@ -527,7 +529,7 @@ class UpdateClassificationAccTest {
before,
indirectLinkedTasks,
taskService,
converter,
workingTimeCalculator,
input.getRight().getLeft(),
input.getRight().getRight());
};
@ -579,7 +581,7 @@ class UpdateClassificationAccTest {
before,
indirectLinkedTasks,
taskService,
converter,
workingTimeCalculator,
input.getRight().getLeft(),
input.getRight().getRight());
};
@ -609,7 +611,7 @@ class UpdateClassificationAccTest {
Instant before,
List<String> tasksUpdated,
TaskService taskService,
WorkingDaysToDaysConverter converter,
WorkingTimeCalculator workingTimeCalculator,
int serviceLevel,
int priority)
throws Exception {
@ -617,7 +619,7 @@ class UpdateClassificationAccTest {
Task task = taskService.getTask(taskId);
Instant expDue =
converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(serviceLevel));
workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(serviceLevel));
assertThat(task.getModified())
.describedAs("Task " + task.getId() + " has not been refreshed.")
.isAfter(before);

View File

@ -5,7 +5,6 @@ import static pro.taskana.testapi.DefaultTestEntities.defaultTestClassification;
import static pro.taskana.testapi.DefaultTestEntities.defaultTestObjectReference;
import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
import org.junit.jupiter.api.BeforeAll;
@ -14,8 +13,6 @@ import org.junit.jupiter.api.Test;
import pro.taskana.classification.api.ClassificationService;
import pro.taskana.classification.api.models.ClassificationSummary;
import pro.taskana.common.api.BulkOperationResults;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.exceptions.TaskanaException;
import pro.taskana.task.api.TaskService;
import pro.taskana.task.api.models.Attachment;
@ -41,7 +38,6 @@ class ServiceLevelOfAllTasksAccTest {
private static final String SMALL_CLASSIFICATION_SERVICE_LEVEL = "P2D";
private static final String GREAT_CLASSIFICATION_SERVICE_LEVEL = "P7D";
@TaskanaInject TaskanaEngine taskanaEngine;
@TaskanaInject TaskService taskService;
@TaskanaInject WorkbasketService workbasketService;
@TaskanaInject ClassificationService classificationService;
@ -52,7 +48,6 @@ class ServiceLevelOfAllTasksAccTest {
Attachment attachmentSummaryGreatServiceLevel;
WorkbasketSummary defaultWorkbasketSummary;
ObjectReference defaultObjectReference;
WorkingDaysToDaysConverter converter;
@WithAccessId(user = "businessadmin")
@BeforeAll
@ -87,13 +82,12 @@ class ServiceLevelOfAllTasksAccTest {
.permission(WorkbasketPermission.READ)
.permission(WorkbasketPermission.APPEND)
.buildAndStore(workbasketService);
converter = taskanaEngine.getWorkingDaysToDaysConverter();
}
@WithAccessId(user = "user-1-1")
@Test
void should_SetPlannedOnMultipleTasks() throws Exception {
Instant planned = Instant.parse("2020-05-03T07:00:00.000Z");
Instant planned = Instant.parse("2020-05-03T07:00:00.000Z"); // Sunday
TaskSummary task1 =
createDefaultTask()
.classificationSummary(classificationSummarySmallServiceLevel)
@ -123,7 +117,7 @@ class ServiceLevelOfAllTasksAccTest {
@Test
void should_ChangeDue_When_SettingPlannedAndClassificationHasSmallerServiceLevel()
throws Exception {
Instant planned = Instant.parse("2020-05-03T07:00:00.000Z");
Instant planned = Instant.parse("2020-05-03T07:00:00.000Z"); // Sunday
TaskSummary task1 =
createDefaultTask()
.classificationSummary(classificationSummarySmallServiceLevel)
@ -141,11 +135,8 @@ class ServiceLevelOfAllTasksAccTest {
assertThat(bulkLog.containsErrors()).isFalse();
List<TaskSummary> result =
taskService.createTaskQuery().idIn(task1.getId(), task2.getId()).list();
assertThat(result)
.extracting(TaskSummary::getDue)
.containsOnly(
converter.addWorkingDaysToInstant(
planned, Duration.parse(SMALL_CLASSIFICATION_SERVICE_LEVEL)));
Instant expectedDue = Instant.parse("2020-05-05T23:00:00.000Z");
assertThat(result).extracting(TaskSummary::getDue).containsOnly(expectedDue);
}
@WithAccessId(user = "user-1-1")
@ -171,11 +162,8 @@ class ServiceLevelOfAllTasksAccTest {
assertThat(bulkLog.containsErrors()).isFalse();
List<TaskSummary> result =
taskService.createTaskQuery().idIn(task1.getId(), task2.getId()).list();
assertThat(result)
.extracting(TaskSummary::getDue)
.containsOnly(
converter.addWorkingDaysToInstant(
planned, Duration.parse(SMALL_CLASSIFICATION_SERVICE_LEVEL)));
Instant expectedDue = Instant.parse("2020-05-05T23:00:00.000Z");
assertThat(result).extracting(TaskSummary::getDue).containsOnly(expectedDue);
}
@WithAccessId(user = "user-1-1")
@ -205,13 +193,11 @@ class ServiceLevelOfAllTasksAccTest {
assertThat(bulkLog.containsErrors()).isFalse();
List<TaskSummary> result =
taskService.createTaskQuery().idIn(task1.getId(), task2.getId(), task3.getId()).list();
Instant expectedDueSmallServiceLevel = Instant.parse("2020-05-05T23:00:00.000Z");
Instant expectedDueGreatServiceLevel = Instant.parse("2020-05-12T23:00:00.000Z");
assertThat(result)
.extracting(TaskSummary::getDue)
.containsOnly(
converter.addWorkingDaysToInstant(
planned, Duration.parse(SMALL_CLASSIFICATION_SERVICE_LEVEL)),
converter.addWorkingDaysToInstant(
planned, Duration.parse(GREAT_CLASSIFICATION_SERVICE_LEVEL)));
.containsOnly(expectedDueSmallServiceLevel, expectedDueGreatServiceLevel);
}
private TaskBuilder createDefaultTask() {

View File

@ -9,8 +9,10 @@ import java.io.InputStream;
import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.DayOfWeek;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
@ -27,6 +29,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pro.taskana.common.api.CustomHoliday;
import pro.taskana.common.api.LocalTimeInterval;
import pro.taskana.common.api.TaskanaRole;
import pro.taskana.common.api.exceptions.SystemException;
import pro.taskana.common.internal.configuration.DB;
@ -61,6 +64,7 @@ public class TaskanaConfiguration {
private final boolean deleteHistoryOnTaskDeletionEnabled;
// region custom configuration
private final Map<String, String> properties;
private final Map<DayOfWeek, Set<LocalTimeInterval>> workingTimeSchedule;
@TaskanaProperty("taskana.domains")
private List<String> domains = new ArrayList<>();
@ -147,6 +151,11 @@ public class TaskanaConfiguration {
this.deleteHistoryOnTaskDeletionEnabled = builder.deleteHistoryOnTaskDeletionEnabled;
this.germanPublicHolidaysEnabled = builder.germanPublicHolidaysEnabled;
this.corpusChristiEnabled = builder.corpusChristiEnabled;
this.workingTimeSchedule =
builder.workingTimeSchedule.entrySet().stream()
.collect(
Collectors.toUnmodifiableMap(
Entry::getKey, e -> Collections.unmodifiableSet(e.getValue())));
this.jobBatchSize = builder.jobBatchSize;
this.maxNumberOfJobRetries = builder.maxNumberOfJobRetries;
this.cleanupJobFirstRun = builder.cleanupJobFirstRun;
@ -238,6 +247,10 @@ public class TaskanaConfiguration {
return customHolidays;
}
public Map<DayOfWeek, Set<LocalTimeInterval>> getWorkingTimeSchedule() {
return workingTimeSchedule;
}
public Map<TaskanaRole, Set<String>> getRoleMap() {
return roleMap;
}
@ -369,6 +382,10 @@ public class TaskanaConfiguration {
@TaskanaProperty("taskana.german.holidays.corpus-christi.enabled")
private boolean corpusChristiEnabled;
@TaskanaProperty("taskana.workingtime.schedule")
private Map<DayOfWeek, Set<LocalTimeInterval>> workingTimeSchedule =
initDefaultWorkingTimeSchedule();
// endregion
// region history configuration
@ -446,6 +463,7 @@ public class TaskanaConfiguration {
this.deleteHistoryOnTaskDeletionEnabled = tec.isDeleteHistoryOnTaskDeletionEnabled();
this.germanPublicHolidaysEnabled = tec.isGermanPublicHolidaysEnabled();
this.corpusChristiEnabled = tec.isCorpusChristiEnabled();
this.workingTimeSchedule = tec.getWorkingTimeSchedule();
this.jobBatchSize = tec.getJobBatchSize();
this.maxNumberOfJobRetries = tec.getMaxNumberOfJobRetries();
this.cleanupJobFirstRun = tec.getCleanupJobFirstRun();
@ -726,5 +744,22 @@ public class TaskanaConfiguration {
Collectors.toUnmodifiableMap(
e -> e.getKey().toString(), e -> e.getValue().toString()));
}
private static Map<DayOfWeek, Set<LocalTimeInterval>> initDefaultWorkingTimeSchedule() {
Map<DayOfWeek, Set<LocalTimeInterval>> workingTime = new EnumMap<>(DayOfWeek.class);
// Default working time schedule is from Monday 00:00 - Friday 24:00, but CET (hence -1 hour)
Set<LocalTimeInterval> standardWorkingSlots =
Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX));
workingTime.put(
DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.of(23, 0), LocalTime.MAX)));
workingTime.put(DayOfWeek.MONDAY, standardWorkingSlots);
workingTime.put(DayOfWeek.TUESDAY, standardWorkingSlots);
workingTime.put(DayOfWeek.WEDNESDAY, standardWorkingSlots);
workingTime.put(DayOfWeek.THURSDAY, standardWorkingSlots);
workingTime.put(
DayOfWeek.FRIDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.of(23, 0))));
return workingTime;
}
}
}

View File

@ -8,6 +8,7 @@ import pro.taskana.classification.api.ClassificationService;
import pro.taskana.common.api.exceptions.MismatchedRoleException;
import pro.taskana.common.api.security.CurrentUserContext;
import pro.taskana.common.internal.TaskanaEngineImpl;
import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl;
import pro.taskana.monitor.api.MonitorService;
import pro.taskana.task.api.TaskService;
import pro.taskana.task.api.models.Task;
@ -111,12 +112,13 @@ public interface TaskanaEngine {
}
/**
* Returns the {@linkplain WorkingDaysToDaysConverter} of the TaskanaEngine. The {@linkplain
* WorkingDaysToDaysConverter} is used to compute holidays.
* Returns the {@linkplain WorkingTimeCalculator} of the TaskanaEngine. The {@linkplain
* WorkingTimeCalculator} is used to add or subtract working time from Instants according to a
* working time schedule or to calculate the working time between Instants.
*
* @return {@linkplain WorkingDaysToDaysConverter}
* @return {@linkplain WorkingTimeCalculatorImpl}
*/
WorkingDaysToDaysConverter getWorkingDaysToDaysConverter();
WorkingTimeCalculator getWorkingTimeCalculator();
/**
* Checks if the {@linkplain pro.taskana.spi.history.api.TaskanaHistory TaskanaHistory} plugin is

View File

@ -34,7 +34,7 @@ import pro.taskana.common.api.ConfigurationService;
import pro.taskana.common.api.JobService;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.TaskanaRole;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.AutocommitFailedException;
import pro.taskana.common.api.exceptions.ConnectionNotSetException;
import pro.taskana.common.api.exceptions.MismatchedRoleException;
@ -47,6 +47,8 @@ import pro.taskana.common.internal.persistence.InstantTypeHandler;
import pro.taskana.common.internal.persistence.MapTypeHandler;
import pro.taskana.common.internal.persistence.StringTypeHandler;
import pro.taskana.common.internal.security.CurrentUserContextImpl;
import pro.taskana.common.internal.workingtime.HolidaySchedule;
import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl;
import pro.taskana.monitor.api.MonitorService;
import pro.taskana.monitor.internal.MonitorMapper;
import pro.taskana.monitor.internal.MonitorServiceImpl;
@ -94,7 +96,9 @@ public class TaskanaEngineImpl implements TaskanaEngine {
private final BeforeRequestChangesManager beforeRequestChangesManager;
private final AfterRequestChangesManager afterRequestChangesManager;
private final InternalTaskanaEngineImpl internalTaskanaEngineImpl;
private final WorkingDaysToDaysConverter workingDaysToDaysConverter;
private final WorkingTimeCalculator workingTimeCalculator;
private final HistoryEventManager historyEventManager;
private final CurrentUserContext currentUserContext;
protected ConnectionManagementMode mode;
@ -113,11 +117,14 @@ public class TaskanaEngineImpl implements TaskanaEngine {
this.taskanaEngineConfiguration = taskanaEngineConfiguration;
this.mode = connectionManagementMode;
internalTaskanaEngineImpl = new InternalTaskanaEngineImpl();
workingDaysToDaysConverter =
new WorkingDaysToDaysConverter(
HolidaySchedule holidaySchedule =
new HolidaySchedule(
taskanaEngineConfiguration.isGermanPublicHolidaysEnabled(),
taskanaEngineConfiguration.isCorpusChristiEnabled(),
taskanaEngineConfiguration.getCustomHolidays());
workingTimeCalculator =
new WorkingTimeCalculatorImpl(
holidaySchedule, taskanaEngineConfiguration.getWorkingTimeSchedule());
currentUserContext =
new CurrentUserContextImpl(TaskanaConfiguration.shouldUseLowerCaseForAccessIds());
createTransactionFactory(taskanaEngineConfiguration.isUseManagedTransactions());
@ -211,8 +218,8 @@ public class TaskanaEngineImpl implements TaskanaEngine {
}
@Override
public WorkingDaysToDaysConverter getWorkingDaysToDaysConverter() {
return workingDaysToDaysConverter;
public WorkingTimeCalculator getWorkingTimeCalculator() {
return workingTimeCalculator;
}
@Override

View File

@ -2,14 +2,14 @@ package pro.taskana.monitor.internal.preprocessor;
import java.util.List;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.monitor.api.reports.header.TimeIntervalColumnHeader;
import pro.taskana.monitor.api.reports.item.AgeQueryItem;
import pro.taskana.monitor.api.reports.item.QueryItemPreprocessor;
/**
* Uses {@linkplain WorkingDaysToDaysConverter} to convert an &lt;I&gt;s age to working days.
* Uses {@linkplain WorkingDaysToDaysReportConverter} to convert an &lt;I&gt;s age to working days.
*
* @param <I> QueryItem which is being processed
*/
@ -20,11 +20,11 @@ public class DaysToWorkingDaysReportPreProcessor<I extends AgeQueryItem>
public DaysToWorkingDaysReportPreProcessor(
List<? extends TimeIntervalColumnHeader> columnHeaders,
WorkingDaysToDaysConverter converter,
WorkingTimeCalculator workingTimeCalculator,
boolean activate)
throws InvalidArgumentException {
if (activate) {
instance = WorkingDaysToDaysReportConverter.initialize(columnHeaders, converter);
instance = WorkingDaysToDaysReportConverter.initialize(columnHeaders, workingTimeCalculator);
}
}

View File

@ -11,14 +11,14 @@ import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.monitor.api.reports.header.TimeIntervalColumnHeader;
/**
* The DaysToWorkingDaysReportConverter provides a method to convert an age in days into an age in
* working days. Before the method convertDaysToWorkingDays() can be used, the
* WorkingDaysToDaysConverter has to be initialized. For a list of {@linkplain
* WorkingDaysToDaysReportConverter has to be initialized. For a list of {@linkplain
* TimeIntervalColumnHeader}s the converter creates a "table" with integer that represents the age
* in days from the largest lower limit until the smallest upper limit of the
* timeIntervalColumnHeaders. This table is valid for a whole day until the converter is initialized
@ -29,21 +29,22 @@ public class WorkingDaysToDaysReportConverter {
private static final Logger LOGGER =
LoggerFactory.getLogger(WorkingDaysToDaysReportConverter.class);
private final WorkingDaysToDaysConverter daysToWorkingDaysConverter;
private final WorkingTimeCalculator workingTimeCalculator;
private final Map<Integer, Integer> cacheDaysToWorkingDays;
WorkingDaysToDaysReportConverter(
List<? extends TimeIntervalColumnHeader> columnHeaders,
WorkingDaysToDaysConverter daysToWorkingDaysConverter,
WorkingTimeCalculator workingTimeCalculator,
Instant referenceDate) {
this.daysToWorkingDaysConverter = daysToWorkingDaysConverter;
this.workingTimeCalculator = workingTimeCalculator;
cacheDaysToWorkingDays = generateDaysToWorkingDays(columnHeaders, referenceDate);
}
public static WorkingDaysToDaysReportConverter initialize(
List<? extends TimeIntervalColumnHeader> columnHeaders, WorkingDaysToDaysConverter converter)
List<? extends TimeIntervalColumnHeader> columnHeaders,
WorkingTimeCalculator workingTimeCalculator)
throws InvalidArgumentException {
return initialize(columnHeaders, converter, Instant.now());
return initialize(columnHeaders, workingTimeCalculator, Instant.now());
}
/**
@ -53,21 +54,22 @@ public class WorkingDaysToDaysReportConverter {
*
* @param columnHeaders a list of {@linkplain TimeIntervalColumnHeader}s that determines the size
* of the table
* @param converter the converter used by taskana to determine if a specific day is a working day.
* @param workingTimeCalculator the workingTimeCalculator used by taskana to determine if a
* specific day is a working day.
* @param referenceDate a {@linkplain Instant} that represents the current day of the table
* @return an instance of the WorkingDaysToDaysConverter
* @throws InvalidArgumentException thrown if columnHeaders or referenceDate is null
*/
public static WorkingDaysToDaysReportConverter initialize(
List<? extends TimeIntervalColumnHeader> columnHeaders,
WorkingDaysToDaysConverter converter,
WorkingTimeCalculator workingTimeCalculator,
Instant referenceDate)
throws InvalidArgumentException {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Initialize WorkingDaysToDaysConverter with columnHeaders: {}", columnHeaders);
}
if (converter == null) {
if (workingTimeCalculator == null) {
throw new InvalidArgumentException("WorkingDaysToDaysConverter can't be null");
}
if (columnHeaders == null) {
@ -77,7 +79,8 @@ public class WorkingDaysToDaysReportConverter {
throw new InvalidArgumentException("ReferenceDate can't be null");
}
return new WorkingDaysToDaysReportConverter(columnHeaders, converter, referenceDate);
return new WorkingDaysToDaysReportConverter(
columnHeaders, workingTimeCalculator, referenceDate);
}
public int convertDaysToWorkingDays(int amountOfDays) {
@ -129,8 +132,7 @@ public class WorkingDaysToDaysReportConverter {
int amountOfWorkdays = 0;
while (Math.abs(amountOfWorkdays) < Math.abs(workdayLimit)) {
amountOfDays += direction;
if (daysToWorkingDaysConverter.isWorkingDay(
referenceDate.plus(amountOfDays, ChronoUnit.DAYS))) {
if (workingTimeCalculator.isWorkingDay(referenceDate.plus(amountOfDays, ChronoUnit.DAYS))) {
amountOfWorkdays += direction;
}
daysToWorkingDaysMap.put(amountOfDays, amountOfWorkdays);
@ -142,7 +144,7 @@ public class WorkingDaysToDaysReportConverter {
return "DaysToWorkingDaysReportConverter [cacheDaysToWorkingDays="
+ cacheDaysToWorkingDays
+ ", daysToWorkingDaysConverter="
+ daysToWorkingDaysConverter
+ workingTimeCalculator
+ "]";
}
}

View File

@ -43,7 +43,7 @@ public class ClassificationCategoryReportBuilderImpl
report.addItems(
monitorQueryItems,
new DaysToWorkingDaysReportPreProcessor<>(
this.columnHeaders, converter, this.inWorkingDays));
this.columnHeaders, workingTimeCalculator, this.inWorkingDays));
return report;
} finally {
this.taskanaEngine.returnConnection();

View File

@ -54,7 +54,7 @@ public class ClassificationReportBuilderImpl
report.addItems(
monitorQueryItems,
new DaysToWorkingDaysReportPreProcessor<>(
this.columnHeaders, converter, this.inWorkingDays));
this.columnHeaders, workingTimeCalculator, this.inWorkingDays));
Map<String, String> displayMap =
classificationService
.createClassificationQuery()
@ -94,7 +94,7 @@ public class ClassificationReportBuilderImpl
report.addItems(
detailedMonitorQueryItems,
new DaysToWorkingDaysReportPreProcessor<>(
this.columnHeaders, converter, this.inWorkingDays));
this.columnHeaders, workingTimeCalculator, this.inWorkingDays));
Stream<String> attachmentKeys =
report.getRows().keySet().stream()
.map(report::getRow)

View File

@ -50,7 +50,7 @@ public class TaskCustomFieldValueReportBuilderImpl
report.addItems(
monitorQueryItems,
new DaysToWorkingDaysReportPreProcessor<>(
this.columnHeaders, converter, this.inWorkingDays));
this.columnHeaders, workingTimeCalculator, this.inWorkingDays));
return report;
} finally {
this.taskanaEngine.returnConnection();

View File

@ -7,7 +7,7 @@ import java.util.stream.Collectors;
import pro.taskana.common.api.IntInterval;
import pro.taskana.common.api.TaskanaRole;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.MismatchedRoleException;
import pro.taskana.common.api.exceptions.SystemException;
@ -48,7 +48,7 @@ abstract class TimeIntervalReportBuilderImpl<
protected String[] domains;
protected String[] classificationIds;
protected String[] excludedClassificationIds;
protected WorkingDaysToDaysConverter converter;
protected WorkingTimeCalculator workingTimeCalculator;
private String[] custom1In;
private String[] custom1NotIn;
private String[] custom1Like;
@ -135,7 +135,7 @@ abstract class TimeIntervalReportBuilderImpl<
this.taskanaEngine = taskanaEngine;
this.monitorMapper = monitorMapper;
this.columnHeaders = Collections.emptyList();
converter = taskanaEngine.getEngine().getWorkingDaysToDaysConverter();
workingTimeCalculator = taskanaEngine.getEngine().getWorkingTimeCalculator();
}
@Override
@ -609,7 +609,7 @@ abstract class TimeIntervalReportBuilderImpl<
private List<SelectedItem> convertWorkingDaysToDays(
List<SelectedItem> selectedItems, List<H> columnHeaders) throws InvalidArgumentException {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(columnHeaders, converter);
WorkingDaysToDaysReportConverter.initialize(columnHeaders, workingTimeCalculator);
return selectedItems.stream()
.map(
s ->

View File

@ -76,7 +76,8 @@ public class TimestampReportBuilderImpl
report.addItems(
items,
new DaysToWorkingDaysReportPreProcessor<>(columnHeaders, converter, inWorkingDays));
new DaysToWorkingDaysReportPreProcessor<>(
columnHeaders, workingTimeCalculator, inWorkingDays));
return report;
} finally {
this.taskanaEngine.returnConnection();

View File

@ -5,7 +5,6 @@ import java.util.List;
import pro.taskana.common.api.IntInterval;
import pro.taskana.common.api.TaskanaRole;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.MismatchedRoleException;
import pro.taskana.common.api.exceptions.SystemException;
@ -32,7 +31,6 @@ public class WorkbasketPriorityReportBuilderImpl implements WorkbasketPriorityRe
protected String[] domains;
protected String[] classificationIds;
protected String[] excludedClassificationIds;
protected WorkingDaysToDaysConverter converter;
private WorkbasketType[] workbasketTypes;
private String[] custom1In;
private String[] custom1NotIn;

View File

@ -50,7 +50,7 @@ public class WorkbasketReportBuilderImpl
report.addItems(
monitorQueryItems,
new DaysToWorkingDaysReportPreProcessor<>(
this.columnHeaders, converter, this.inWorkingDays));
this.columnHeaders, workingTimeCalculator, this.inWorkingDays));
Map<String, String> displayMap =
taskanaEngine

View File

@ -2,6 +2,7 @@ package pro.taskana.spi.priority.api;
import java.util.OptionalInt;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.task.api.models.Task;
import pro.taskana.task.api.models.TaskSummary;
@ -11,6 +12,18 @@ import pro.taskana.task.api.models.TaskSummary;
*/
public interface PriorityServiceProvider {
/**
* Provide the active {@linkplain TaskanaEngine} which is initialized for this TASKANA
* installation.
*
* <p>This method is called during TASKANA startup and allows the service provider to store the
* active {@linkplain TaskanaEngine} for later usage.
*
* @param taskanaEngine the active {@linkplain TaskanaEngine} which is initialized for this
* installation
*/
default void initialize(TaskanaEngine taskanaEngine) {}
/**
* Determine the {@linkplain Task#getPriority() priority} of a certain {@linkplain Task} during
* execution of {@linkplain pro.taskana.task.api.TaskService#createTask(Task)} and {@linkplain

View File

@ -18,7 +18,7 @@ import org.slf4j.LoggerFactory;
import pro.taskana.classification.api.models.ClassificationSummary;
import pro.taskana.common.api.BulkOperationResults;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.TaskanaException;
import pro.taskana.common.internal.InternalTaskanaEngine;
@ -38,7 +38,7 @@ class ServiceLevelHandler {
private final InternalTaskanaEngine taskanaEngine;
private final TaskMapper taskMapper;
private final AttachmentMapper attachmentMapper;
private final WorkingDaysToDaysConverter converter;
private final WorkingTimeCalculator workingTimeCalculator;
private final TaskServiceImpl taskServiceImpl;
ServiceLevelHandler(
@ -49,7 +49,7 @@ class ServiceLevelHandler {
this.taskanaEngine = taskanaEngine;
this.taskMapper = taskMapper;
this.attachmentMapper = attachmentMapper;
converter = taskanaEngine.getEngine().getWorkingDaysToDaysConverter();
workingTimeCalculator = taskanaEngine.getEngine().getWorkingTimeCalculator();
this.taskServiceImpl = taskServiceImpl;
}
@ -108,12 +108,14 @@ class ServiceLevelHandler {
return bulkLog;
}
TaskImpl updatePrioPlannedDueOfTask(
TaskImpl newTaskImpl, TaskImpl oldTaskImpl, boolean forRefreshOnClassificationUpdate)
// TODO: Is it worth splitting the logic of this method into two separate methods one for
// creating new task the other for updating a task.
TaskImpl updatePrioPlannedDueOfTask(TaskImpl newTaskImpl, TaskImpl oldTaskImpl)
throws InvalidArgumentException {
boolean onlyPriority = false;
if (newTaskImpl.getClassificationSummary() == null
|| newTaskImpl.getClassificationSummary().getServiceLevel() == null) {
// TODO this should never be the case
setPlannedDueOnMissingServiceLevel(newTaskImpl);
onlyPriority = true;
}
@ -123,6 +125,7 @@ class ServiceLevelHandler {
}
if (newTaskImpl.getPlanned() == null && newTaskImpl.getDue() == null) {
// TODO bitte oldTaskImpl berücksichtigen
newTaskImpl.setPlanned(Instant.now());
}
@ -135,21 +138,17 @@ class ServiceLevelHandler {
if (onlyPriority) {
return newTaskImpl;
}
// classification update
if (forRefreshOnClassificationUpdate) {
newTaskImpl.setDue(
getFollowingWorkingDays(newTaskImpl.getPlanned(), durationPrioHolder.getDuration()));
return newTaskImpl;
}
// creation of new task
if (oldTaskImpl == null) {
return updatePlannedDueOnCreationOfNewTask(newTaskImpl, durationPrioHolder);
return updatePlannedDueOnCreationOfNewTask(newTaskImpl, durationPrioHolder.getDuration());
} else {
return updatePlannedDueOnTaskUpdate(newTaskImpl, oldTaskImpl, durationPrioHolder);
return updatePlannedDueOnTaskUpdate(
newTaskImpl, oldTaskImpl, durationPrioHolder.getDuration());
}
}
DurationPrioHolder determineTaskPrioDuration(TaskImpl newTaskImpl, boolean onlyPriority) {
private DurationPrioHolder determineTaskPrioDuration(TaskImpl newTaskImpl, boolean onlyPriority) {
Set<ClassificationSummary> classificationsInvolved =
getClassificationsReferencedByATask(newTaskImpl);
@ -228,6 +227,7 @@ class ServiceLevelHandler {
MinimalTaskSummary minimalTaskSummary,
Map<String, Integer> classificationIdToPriorityMap,
Map<String, Set<String>> taskIdToClassificationIdsMap) {
// TODO this should allow negative Priorities just like #getFinalPrioDurationOfTask
int actualPriority = 0;
for (String classificationId :
taskIdToClassificationIdsMap.get(minimalTaskSummary.getTaskId())) {
@ -250,56 +250,71 @@ class ServiceLevelHandler {
}
private TaskImpl updatePlannedDueOnTaskUpdate(
TaskImpl newTaskImpl, TaskImpl oldTaskImpl, DurationPrioHolder durationPrioHolder)
TaskImpl newTaskImpl, TaskImpl oldTaskImpl, Duration duration)
throws InvalidArgumentException {
// TODO pull this one out and in updatePlannedDueOnCreationOfNewTask, too.
if (taskanaEngine.getEngine().getConfiguration().isAllowTimestampServiceLevelMismatch()
&& newTaskImpl.getDue() != null
&& newTaskImpl.getPlanned() != null) {
return newTaskImpl;
}
if (newTaskImpl.getPlanned() == null && newTaskImpl.getDue() == null) {
newTaskImpl.setPlanned(oldTaskImpl.getPlanned());
}
if (oldTaskImpl.getDue().equals(newTaskImpl.getDue())
&& oldTaskImpl.getPlanned().equals(newTaskImpl.getPlanned())) {
// case 1: no change of planned/due, but potentially change of an attachment or classification
// -> recalculate due
newTaskImpl.setDue(
getFollowingWorkingDays(newTaskImpl.getPlanned(), durationPrioHolder.getDuration()));
} else if (dueIsUnchangedOrNull(newTaskImpl, oldTaskImpl) && newTaskImpl.getPlanned() != null) {
// case 2: due is null or only planned was changed -> normalize planned & recalculate due
newTaskImpl.setPlanned(getFollowingWorkingDays(newTaskImpl.getPlanned(), Duration.ofDays(0)));
newTaskImpl.setDue(
getFollowingWorkingDays(newTaskImpl.getPlanned(), durationPrioHolder.getDuration()));
boolean forcedDueRecalculation = newTaskImpl.getDue() == null;
boolean forcedPlannedRecalculation = newTaskImpl.getPlanned() == null;
if (forcedDueRecalculation) {
recalcDueBasedPlanned(newTaskImpl, duration);
} else if (forcedPlannedRecalculation) {
recalcPlannedBasedOnDue(newTaskImpl, oldTaskImpl, duration);
} else if (oldTaskImpl.getDue().equals(newTaskImpl.getDue())) {
// We know due has not changed, but the following two options may happen
// * no change of planned, but potentially change of an attachment or classification
// * planned has changed
// -> normalize planned and recalculate due
recalcDueBasedPlanned(newTaskImpl, duration);
} else {
// case 3: due and (maybe) planned were changed -> validate SLA if planned changed
Instant calcDue = getPrecedingWorkingDays(newTaskImpl.getDue(), Duration.ofDays(0));
Instant calcPlanned = getPrecedingWorkingDays(calcDue, durationPrioHolder.getDuration());
if (plannedHasChanged(newTaskImpl, oldTaskImpl)) {
ensureServiceLevelIsNotViolated(newTaskImpl, durationPrioHolder.getDuration(), calcPlanned);
}
newTaskImpl.setPlanned(calcPlanned);
newTaskImpl.setDue(calcDue);
// Due has changed and (maybe) planned has changed
// -> normalize due and recalculate planned
recalcPlannedBasedOnDue(newTaskImpl, oldTaskImpl, duration);
}
return newTaskImpl;
}
private boolean dueIsUnchangedOrNull(Task newTask, Task oldTask) {
return newTask.getDue() == null || oldTask.getDue().equals(newTask.getDue());
private void recalcPlannedBasedOnDue(
TaskImpl newTaskImpl, TaskImpl oldTaskImpl, Duration duration)
throws InvalidArgumentException {
Instant calcDue = instantOrEndOfPreviousWorkSlot(newTaskImpl.getDue());
Instant calcPlanned = subtractWorkingTime(calcDue, duration);
if (plannedHasChanged(newTaskImpl, oldTaskImpl)) {
ensureServiceLevelIsNotViolated(newTaskImpl, duration, calcPlanned);
}
newTaskImpl.setPlanned(calcPlanned);
newTaskImpl.setDue(calcDue);
}
private void recalcDueBasedPlanned(TaskImpl newTaskImpl, Duration duration) {
newTaskImpl.setPlanned(instantOrStartOfNextWorkSlot(newTaskImpl.getPlanned()));
newTaskImpl.setDue(addWorkingTime(newTaskImpl.getPlanned(), duration));
}
private boolean plannedHasChanged(Task newTask, Task oldTask) {
return newTask.getPlanned() != null && !oldTask.getPlanned().equals(newTask.getPlanned());
}
private Instant getPrecedingWorkingDays(Instant instant, Duration days) {
return converter.subtractWorkingDaysFromInstant(instant, days);
private Instant instantOrEndOfPreviousWorkSlot(Instant instant) {
return subtractWorkingTime(instant, Duration.ZERO);
}
private Instant getFollowingWorkingDays(Instant instant, Duration days) {
return converter.addWorkingDaysToInstant(instant, days);
private Instant subtractWorkingTime(Instant instant, Duration workingTime) {
return workingTimeCalculator.subtractWorkingTime(instant, workingTime);
}
private Instant instantOrStartOfNextWorkSlot(Instant instant) {
return addWorkingTime(instant, Duration.ZERO);
}
private Instant addWorkingTime(Instant instant, Duration workingTime) {
return workingTimeCalculator.addWorkingTime(instant, workingTime);
}
/**
@ -325,11 +340,12 @@ class ServiceLevelHandler {
*/
private void ensureServiceLevelIsNotViolated(
TaskImpl task, Duration duration, Instant calcPlanned) throws InvalidArgumentException {
// TODO tests mit coverage. falls die Exception nie auftritt weg mit der Methode
if (task.getPlanned() != null
&& !task.getPlanned().equals(calcPlanned)
// manual entered planned date is a different working day than computed value
&& (converter.isWorkingDay(task.getPlanned())
|| converter.hasWorkingDaysInBetween(task.getPlanned(), calcPlanned))) {
&& (workingTimeCalculator.isWorkingDay(task.getPlanned())
|| workingTimeCalculator.isWorkingTimeBetween(task.getPlanned(), calcPlanned))) {
throw new InvalidArgumentException(
String.format(
"Cannot update a task with given planned %s "
@ -338,8 +354,8 @@ class ServiceLevelHandler {
}
}
private TaskImpl updatePlannedDueOnCreationOfNewTask(
TaskImpl newTask, DurationPrioHolder durationPrioHolder) throws InvalidArgumentException {
private TaskImpl updatePlannedDueOnCreationOfNewTask(TaskImpl newTask, Duration duration)
throws InvalidArgumentException {
if (taskanaEngine.getEngine().getConfiguration().isAllowTimestampServiceLevelMismatch()
&& newTask.getDue() != null
&& newTask.getPlanned() != null) {
@ -347,16 +363,14 @@ class ServiceLevelHandler {
}
if (newTask.getDue() != null) {
// due is specified: calculate back and check correctness
Instant calcDue = getPrecedingWorkingDays(newTask.getDue(), Duration.ofDays(0));
Instant calcPlanned = getPrecedingWorkingDays(calcDue, durationPrioHolder.getDuration());
ensureServiceLevelIsNotViolated(newTask, durationPrioHolder.getDuration(), calcPlanned);
Instant calcDue = instantOrEndOfPreviousWorkSlot(newTask.getDue());
Instant calcPlanned = subtractWorkingTime(calcDue, duration);
ensureServiceLevelIsNotViolated(newTask, duration, calcPlanned);
newTask.setDue(calcDue);
newTask.setPlanned(calcPlanned);
} else {
// task.due is null: calculate forward from planned
newTask.setPlanned(getFollowingWorkingDays(newTask.getPlanned(), Duration.ofDays(0)));
newTask.setDue(
getFollowingWorkingDays(newTask.getPlanned(), durationPrioHolder.getDuration()));
recalcDueBasedPlanned(newTask, duration);
}
return newTask;
}
@ -378,8 +392,7 @@ class ServiceLevelHandler {
TaskImpl referenceTask = new TaskImpl();
referenceTask.setPlanned(durationHolder.getPlanned());
referenceTask.setModified(Instant.now());
referenceTask.setDue(
getFollowingWorkingDays(referenceTask.getPlanned(), durationHolder.getDuration()));
referenceTask.setDue(addWorkingTime(referenceTask.getPlanned(), durationHolder.getDuration()));
List<String> taskIdsToUpdate =
taskDurationList.stream().map(TaskDuration::getTaskId).collect(Collectors.toList());
Pair<List<MinimalTaskSummary>, BulkLog> existingAndAuthorizedTasks =
@ -399,7 +412,7 @@ class ServiceLevelHandler {
taskIdsByDueDuration.forEach(
(duration, taskIds) -> {
referenceTask.setDue(getFollowingWorkingDays(planned, duration));
referenceTask.setDue(addWorkingTime(planned, duration));
Pair<List<MinimalTaskSummary>, BulkLog> existingAndAuthorizedTasks =
taskServiceImpl.getMinimalTaskSummaries(taskIds);
bulkLog.addAllErrors(existingAndAuthorizedTasks.getRight());
@ -584,40 +597,35 @@ class ServiceLevelHandler {
}
private boolean isPriorityAndDurationAlreadyCorrect(TaskImpl newTaskImpl, TaskImpl oldTaskImpl) {
if (oldTaskImpl != null) {
final boolean isClassificationKeyChanged =
newTaskImpl.getClassificationKey() != null
&& (oldTaskImpl.getClassificationKey() == null
|| !newTaskImpl
.getClassificationKey()
.equals(oldTaskImpl.getClassificationKey()));
final boolean isManualPriorityChanged =
newTaskImpl.getManualPriority() != oldTaskImpl.getManualPriority();
final boolean isClassificationIdChanged =
newTaskImpl.getClassificationId() != null
&& (oldTaskImpl.getClassificationId() == null
|| !newTaskImpl.getClassificationId().equals(oldTaskImpl.getClassificationId()));
return oldTaskImpl.getPlanned().equals(newTaskImpl.getPlanned())
&& oldTaskImpl.getDue().equals(newTaskImpl.getDue())
&& !isClassificationKeyChanged
&& !isClassificationIdChanged
&& !isManualPriorityChanged
&& areAttachmentsUnchanged(newTaskImpl, oldTaskImpl);
} else {
if (oldTaskImpl == null) {
return false;
}
// TODO Do we need to compare Key and Id or could we simply compare ClassificationSummary only?
final boolean isClassificationKeyChanged =
Objects.equals(newTaskImpl.getClassificationKey(), oldTaskImpl.getClassificationKey());
final boolean isClassificationIdChanged =
Objects.equals(newTaskImpl.getClassificationId(), oldTaskImpl.getClassificationId());
final boolean isManualPriorityChanged =
newTaskImpl.getManualPriority() != oldTaskImpl.getManualPriority();
return oldTaskImpl.getPlanned().equals(newTaskImpl.getPlanned())
&& oldTaskImpl.getDue().equals(newTaskImpl.getDue())
&& !isClassificationKeyChanged
&& !isClassificationIdChanged
&& !isManualPriorityChanged
&& areAttachmentsUnchanged(newTaskImpl, oldTaskImpl);
}
private boolean areAttachmentsUnchanged(TaskImpl newTaskImpl, TaskImpl oldTaskImpl) {
List<String> oldAttachmentIds =
Set<String> oldAttachmentIds =
oldTaskImpl.getAttachments().stream()
.map(AttachmentSummary::getId)
.collect(Collectors.toList());
List<String> newAttachmentIds =
.collect(Collectors.toSet());
Set<String> newAttachmentIds =
newTaskImpl.getAttachments().stream()
.map(AttachmentSummary::getId)
.collect(Collectors.toList());
.collect(Collectors.toSet());
Set<String> oldClassificationIds =
oldTaskImpl.getAttachments().stream()
.map(Attachment::getClassificationSummary)

View File

@ -967,11 +967,9 @@ public class TaskServiceImpl implements TaskService {
taskanaEngine
.getEngine()
.runAsAdmin(
() -> {
serviceLevelHandler.refreshPriorityAndDueDatesOfTasks(
tasks, serviceLevelChanged, priorityChanged);
return null;
});
() ->
serviceLevelHandler.refreshPriorityAndDueDatesOfTasks(
tasks, serviceLevelChanged, priorityChanged));
}
} finally {
taskanaEngine.returnConnection();
@ -1738,7 +1736,7 @@ public class TaskServiceImpl implements TaskService {
// This has to be called after the AttachmentHandler because the AttachmentHandler fetches
// the Classifications of the Attachments.
// This is necessary to guarantee that the following calculation is correct.
serviceLevelHandler.updatePrioPlannedDueOfTask(task, null, false);
serviceLevelHandler.updatePrioPlannedDueOfTask(task, null);
}
private void setDefaultTaskReceivedDateFromAttachments(TaskImpl task) {
@ -2048,7 +2046,7 @@ public class TaskServiceImpl implements TaskService {
updateClassificationSummary(newTaskImpl, oldTaskImpl);
TaskImpl newTaskImpl1 =
serviceLevelHandler.updatePrioPlannedDueOfTask(newTaskImpl, oldTaskImpl, false);
serviceLevelHandler.updatePrioPlannedDueOfTask(newTaskImpl, oldTaskImpl);
// if no business process id is provided, use the id of the old task.
if (newTaskImpl1.getBusinessProcessId() == null) {

View File

@ -19,7 +19,7 @@ import pro.taskana.TaskanaConfiguration;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.TaskanaEngine.ConnectionManagementMode;
import pro.taskana.common.api.TimeInterval;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.internal.JobMapper;
import pro.taskana.common.internal.TaskanaEngineImpl;
import pro.taskana.common.test.config.DataSourceGenerator;
@ -41,7 +41,7 @@ public abstract class AbstractAccTest {
protected static TaskanaEngine taskanaEngine;
protected static TaskServiceImpl taskService;
protected static WorkingDaysToDaysConverter converter;
protected static WorkingTimeCalculator workingTimeCalculator;
@BeforeAll
protected static void setupTest() throws Exception {
@ -66,7 +66,7 @@ public abstract class AbstractAccTest {
taskanaEngine =
TaskanaEngine.buildTaskanaEngine(
taskanaEngineConfiguration, ConnectionManagementMode.AUTOCOMMIT);
converter = taskanaEngine.getWorkingDaysToDaysConverter();
workingTimeCalculator = taskanaEngine.getWorkingTimeCalculator();
taskService = (TaskServiceImpl) taskanaEngine.getTaskService();
sampleDataGenerator.clearDb();
@ -140,10 +140,10 @@ public abstract class AbstractAccTest {
}
protected Instant moveForwardToWorkingDay(Instant date) {
return converter.addWorkingDaysToInstant(date, Duration.ZERO);
return workingTimeCalculator.addWorkingTime(date, Duration.ZERO);
}
protected Instant moveBackToWorkingDay(Instant date) {
return converter.subtractWorkingDaysFromInstant(date, Duration.ZERO);
return workingTimeCalculator.subtractWorkingTime(date, Duration.ZERO);
}
}

View File

@ -10,7 +10,6 @@ import pro.taskana.common.api.CustomHoliday;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.test.config.DataSourceGenerator;
/** Test of configuration. */
class TaskanaEngineConfigTest {
@Test
@ -27,7 +26,7 @@ class TaskanaEngineConfigTest {
}
@Test
void should_SetCorpusChristiEnabled_When_PropertyIsSet() throws Exception {
void should_SetCorpusChristiEnabled_When_PropertyIsSet() {
DataSource ds = DataSourceGenerator.getDataSource();
TaskanaConfiguration taskEngineConfiguration =
new TaskanaConfiguration.Builder(ds, false, DataSourceGenerator.getSchemaName(), true)
@ -38,20 +37,18 @@ class TaskanaEngineConfigTest {
}
@Test
void should_ReturnTheTwoCustomHolidays_When_TwoCustomHolidaysAreConfiguredInThePropertiesFile()
throws Exception {
void should_ReturnTheTwoCustomHolidays_When_TwoCustomHolidaysAreConfiguredInThePropertiesFile() {
DataSource ds = DataSourceGenerator.getDataSource();
TaskanaConfiguration taskEngineConfiguration =
new TaskanaConfiguration.Builder(ds, false, DataSourceGenerator.getSchemaName(), true)
.initTaskanaProperties("/custom_holiday_taskana.properties", "|")
.build();
assertThat(taskEngineConfiguration.getCustomHolidays()).contains(CustomHoliday.of(31, 7));
assertThat(taskEngineConfiguration.getCustomHolidays()).contains(CustomHoliday.of(16, 12));
assertThat(taskEngineConfiguration.getCustomHolidays())
.contains(CustomHoliday.of(31, 7), CustomHoliday.of(16, 12));
}
@Test
void should_ReturnEmptyCustomHolidaysList_When_AllCustomHolidaysAreInWrongFormatInPropertiesFile()
throws Exception {
void should_ReturnEmptyList_When_AllCustomHolidaysAreInWrongFormatInPropertiesFile() {
DataSource ds = DataSourceGenerator.getDataSource();
TaskanaConfiguration taskEngineConfiguration =
new TaskanaConfiguration.Builder(ds, false, DataSourceGenerator.getSchemaName(), true)

View File

@ -7,7 +7,6 @@ import acceptance.AbstractAccTest;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.concurrent.atomic.AtomicReference;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ -21,23 +20,22 @@ import pro.taskana.task.internal.jobs.helper.SqlConnectionRunner;
class SqlConnectionRunnerAccTest extends AbstractAccTest {
@Test
void should_executeSimpleQuery() throws Exception {
void should_executeSimpleQuery() {
// given
SqlConnectionRunner runner = new SqlConnectionRunner(taskanaEngine);
String taskId = "TKI:000000000000000000000000000000000050";
// when
AtomicReference<ResultSet> resultSet = new AtomicReference<>();
runner.runWithConnection(
connection -> {
ResultSet resultSet;
PreparedStatement preparedStatement =
connection.prepareStatement("select * from TASK where ID = ?");
preparedStatement.setString(1, taskId);
resultSet.set(preparedStatement.executeQuery());
resultSet = preparedStatement.executeQuery();
// then
assertThat(resultSet.next()).isTrue();
});
// then
assertThat(resultSet.get().next()).isTrue();
}
@Test

View File

@ -45,7 +45,7 @@ class UpdateObjectsUseUtcTimeStampsAccTest extends AbstractAccTest {
task.setPlanned(now.plus(Duration.ofHours(17)));
// associated Classification has ServiceLevel 'P1D'
task.setDue(converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1)));
task.setDue(workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)));
TaskImpl ti = (TaskImpl) task;
ti.setCompleted(now.plus(Duration.ofHours(27)));

View File

@ -4,17 +4,22 @@ import java.time.Duration;
import java.time.Instant;
import java.util.OptionalInt;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.spi.priority.api.PriorityServiceProvider;
import pro.taskana.task.api.TaskCustomField;
import pro.taskana.task.api.models.TaskSummary;
public class TestPriorityServiceProvider implements PriorityServiceProvider {
private static final int MULTIPLIER = 10;
private final WorkingDaysToDaysConverter converter = new WorkingDaysToDaysConverter(true, true);
private final WorkingTimeCalculator calculator = new WorkingTimeCalculator(converter);
private WorkingTimeCalculator calculator;
@Override
public void initialize(TaskanaEngine taskanaEngine) {
calculator = taskanaEngine.getWorkingTimeCalculator();
}
@Override
public OptionalInt calculatePriority(TaskSummary taskSummary) {
@ -22,10 +27,7 @@ public class TestPriorityServiceProvider implements PriorityServiceProvider {
long priority;
try {
priority =
calculator
.workingTimeBetweenTwoTimestamps(taskSummary.getCreated(), Instant.now())
.toMinutes()
+ 1;
calculator.workingTimeBetween(taskSummary.getCreated(), Instant.now()).toMinutes() + 1;
} catch (Exception e) {
priority = Duration.between(taskSummary.getCreated(), Instant.now()).toMinutes();
}

View File

@ -2,45 +2,70 @@ package acceptance.report;
import static org.assertj.core.api.Assertions.assertThat;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.EnumMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.Test;
import pro.taskana.common.api.CustomHoliday;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.LocalTimeInterval;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.internal.workingtime.HolidaySchedule;
import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl;
import pro.taskana.monitor.api.reports.header.TimeIntervalColumnHeader;
import pro.taskana.monitor.internal.preprocessor.WorkingDaysToDaysReportConverter;
/** Test for the DaysToWorkingDaysReportConverter. */
class WorkingDaysToDaysReportConverterTest {
private final WorkingDaysToDaysConverter converter;
private final WorkingTimeCalculator workingTimeCalculator;
public WorkingDaysToDaysReportConverterTest() {
Map<DayOfWeek, Set<LocalTimeInterval>> workingTime = new EnumMap<>(DayOfWeek.class);
Set<LocalTimeInterval> standardWorkingSlots =
Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX));
workingTime.put(DayOfWeek.MONDAY, standardWorkingSlots);
workingTime.put(DayOfWeek.TUESDAY, standardWorkingSlots);
workingTime.put(DayOfWeek.WEDNESDAY, standardWorkingSlots);
workingTime.put(DayOfWeek.THURSDAY, standardWorkingSlots);
workingTime.put(DayOfWeek.FRIDAY, standardWorkingSlots);
CustomHoliday dayOfReformation = CustomHoliday.of(31, 10);
CustomHoliday allSaintsDays = CustomHoliday.of(1, 11);
converter =
new WorkingDaysToDaysConverter(true, false, List.of(dayOfReformation, allSaintsDays));
workingTimeCalculator =
new WorkingTimeCalculatorImpl(
new HolidaySchedule(true, false, List.of(dayOfReformation, allSaintsDays)),
workingTime);
}
@Test
void should_AssertNotEqual_When_InitializingDifferentDates() throws Exception {
void should_AssertNotEqual_When_InitializingDifferentDates() {
WorkingDaysToDaysReportConverter instance1 =
WorkingDaysToDaysReportConverter.initialize(
getShortListOfColumnHeaders(), converter, Instant.parse("2018-02-04T00:00:00.000Z"));
getShortListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-02-04T00:00:00.000Z"));
WorkingDaysToDaysReportConverter instance2 =
WorkingDaysToDaysReportConverter.initialize(
getShortListOfColumnHeaders(), converter, Instant.parse("2018-02-05T00:00:00.000Z"));
getShortListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-02-05T00:00:00.000Z"));
assertThat(instance1).isNotEqualTo(instance2);
}
@Test
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDays() throws Exception {
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDays() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-02-06T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-02-06T00:00:00.000Z"));
int oneBelowLimit = -16;
int oneAboveLimit = 16;
@ -53,7 +78,7 @@ class WorkingDaysToDaysReportConverterTest {
assertThat(instance.convertDaysToWorkingDays(-2)).isEqualTo(-1);
assertThat(instance.convertDaysToWorkingDays(-1)).isEqualTo(-1);
assertThat(instance.convertDaysToWorkingDays(0)).isZero();
assertThat(instance.convertDaysToWorkingDays(1)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(1)).isOne();
assertThat(instance.convertDaysToWorkingDays(2)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(3)).isEqualTo(3);
assertThat(instance.convertDaysToWorkingDays(4)).isEqualTo(3);
@ -65,30 +90,34 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnWorkingDaysUnchanged_When_ConvertingWorkingDaysOutOfNegativeLimit()
throws Exception {
void should_ReturnWorkingDaysUnchanged_When_ConvertingWorkingDaysOutOfNegativeLimit() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-02-06T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-02-06T00:00:00.000Z"));
assertThat(instance.convertWorkingDaysToDays(-999)).containsExactlyInAnyOrder(-999);
}
@Test
void should_ReturnWorkingDaysUnchanged_When_ConvertingWorkingDaysOutOfPositiveLimit()
throws Exception {
void should_ReturnWorkingDaysUnchanged_When_ConvertingWorkingDaysOutOfPositiveLimit() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-02-06T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-02-06T00:00:00.000Z"));
assertThat(instance.convertWorkingDaysToDays(999)).containsExactlyInAnyOrder(999);
}
@Test
void should_ReturnAllMatchingDays_When_ConvertingWorkingDaysToDays() throws Exception {
void should_ReturnAllMatchingDays_When_ConvertingWorkingDaysToDays() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-02-27T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-02-27T00:00:00.000Z"));
assertThat(instance.convertWorkingDaysToDays(-13)).containsExactlyInAnyOrder(-13);
assertThat(instance.convertWorkingDaysToDays(-12)).containsExactlyInAnyOrder(-12);
@ -119,10 +148,12 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnAllMatchingDays_When_ConvertingWorkingDaysToDaysAtWeekend() throws Exception {
void should_ReturnAllMatchingDays_When_ConvertingWorkingDaysToDaysAtWeekend() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-03-10T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-03-10T00:00:00.000Z"));
assertThat(instance.convertWorkingDaysToDays(-13)).containsExactlyInAnyOrder(-13);
assertThat(instance.convertWorkingDaysToDays(-12)).containsExactlyInAnyOrder(-12);
@ -153,11 +184,12 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnAllMatchingDays_When_ConvertingWorkingDaysToDaysOnEasterSunday()
throws Exception {
void should_ReturnAllMatchingDays_When_ConvertingWorkingDaysToDaysOnEasterSunday() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-04-01T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-04-01T00:00:00.000Z"));
assertThat(instance.convertWorkingDaysToDays(-13)).containsExactlyInAnyOrder(-13);
assertThat(instance.convertWorkingDaysToDays(-12)).containsExactlyInAnyOrder(-12);
@ -188,30 +220,32 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnEasterHolidays()
throws Exception {
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnEasterHolidays() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-03-28T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-03-28T00:00:00.000Z"));
assertThat(instance.convertDaysToWorkingDays(0)).isZero();
assertThat(instance.convertDaysToWorkingDays(1)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(2)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(3)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(4)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(5)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(1)).isOne();
assertThat(instance.convertDaysToWorkingDays(2)).isOne();
assertThat(instance.convertDaysToWorkingDays(3)).isOne();
assertThat(instance.convertDaysToWorkingDays(4)).isOne();
assertThat(instance.convertDaysToWorkingDays(5)).isOne();
assertThat(instance.convertDaysToWorkingDays(6)).isEqualTo(2);
}
@Test
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnWhitsunHolidays()
throws Exception {
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnWhitsunHolidays() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-05-16T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-05-16T00:00:00.000Z"));
assertThat(instance.convertDaysToWorkingDays(0)).isZero();
assertThat(instance.convertDaysToWorkingDays(1)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(1)).isOne();
assertThat(instance.convertDaysToWorkingDays(2)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(3)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(4)).isEqualTo(2);
@ -220,15 +254,17 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnLabourDay() throws Exception {
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnLabourDay() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-04-26T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-04-26T00:00:00.000Z"));
assertThat(instance.convertDaysToWorkingDays(0)).isZero();
assertThat(instance.convertDaysToWorkingDays(1)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(2)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(3)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(1)).isOne();
assertThat(instance.convertDaysToWorkingDays(2)).isOne();
assertThat(instance.convertDaysToWorkingDays(3)).isOne();
assertThat(instance.convertDaysToWorkingDays(4)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(5)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(6)).isEqualTo(3);
@ -236,13 +272,15 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnAscensionDay() throws Exception {
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnAscensionDay() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-05-07T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-05-07T00:00:00.000Z"));
assertThat(instance.convertDaysToWorkingDays(0)).isZero();
assertThat(instance.convertDaysToWorkingDays(1)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(1)).isOne();
assertThat(instance.convertDaysToWorkingDays(2)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(3)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(4)).isEqualTo(3);
@ -252,15 +290,16 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnDayOfGermanUnity()
throws Exception {
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnDayOfGermanUnity() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-10-01T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-10-01T00:00:00.000Z"));
assertThat(instance.convertDaysToWorkingDays(0)).isZero();
assertThat(instance.convertDaysToWorkingDays(1)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(2)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(1)).isOne();
assertThat(instance.convertDaysToWorkingDays(2)).isOne();
assertThat(instance.convertDaysToWorkingDays(3)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(4)).isEqualTo(3);
assertThat(instance.convertDaysToWorkingDays(5)).isEqualTo(3);
@ -269,16 +308,17 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnChristmasAndNewYearHolidays()
throws Exception {
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnChristmasAndNewYearHolidays() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-12-20T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-12-20T00:00:00.000Z"));
assertThat(instance.convertDaysToWorkingDays(0)).isZero();
assertThat(instance.convertDaysToWorkingDays(1)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(2)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(3)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(1)).isOne();
assertThat(instance.convertDaysToWorkingDays(2)).isOne();
assertThat(instance.convertDaysToWorkingDays(3)).isOne();
assertThat(instance.convertDaysToWorkingDays(4)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(5)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(6)).isEqualTo(2);
@ -293,15 +333,17 @@ class WorkingDaysToDaysReportConverterTest {
}
@Test
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnCustomHoliday() throws Exception {
void should_ReturnWorkingDays_When_ConvertingDaysToWorkingDaysOnCustomHoliday() {
WorkingDaysToDaysReportConverter instance =
WorkingDaysToDaysReportConverter.initialize(
getLargeListOfColumnHeaders(), converter, Instant.parse("2018-10-26T00:00:00.000Z"));
getLargeListOfColumnHeaders(),
workingTimeCalculator,
Instant.parse("2018-10-26T00:00:00.000Z"));
assertThat(instance.convertDaysToWorkingDays(0)).isZero();
assertThat(instance.convertDaysToWorkingDays(1)).isZero();
assertThat(instance.convertDaysToWorkingDays(2)).isZero();
assertThat(instance.convertDaysToWorkingDays(3)).isEqualTo(1);
assertThat(instance.convertDaysToWorkingDays(3)).isOne();
assertThat(instance.convertDaysToWorkingDays(4)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(5)).isEqualTo(2);
assertThat(instance.convertDaysToWorkingDays(6)).isEqualTo(2);

View File

@ -20,7 +20,6 @@ import org.junit.jupiter.api.extension.ExtendWith;
import pro.taskana.classification.api.ClassificationService;
import pro.taskana.classification.api.models.Classification;
import pro.taskana.common.api.BulkOperationResults;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.common.api.exceptions.TaskanaException;
import pro.taskana.common.test.security.JaasExtension;
@ -35,11 +34,9 @@ import pro.taskana.workbasket.api.exceptions.MismatchedWorkbasketPermissionExcep
class ServiceLevelPriorityAccTest extends AbstractAccTest {
private final ClassificationService classificationService;
private final WorkingDaysToDaysConverter converter;
ServiceLevelPriorityAccTest() {
classificationService = taskanaEngine.getClassificationService();
converter = taskanaEngine.getWorkingDaysToDaysConverter();
}
/* CREATE TASK */
@ -70,7 +67,7 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
assertThat(readTask.getDue()).isEqualTo(due);
Instant expectedPlanned =
converter.subtractWorkingDaysFromInstant(due, Duration.ofDays(serviceLevelDays));
workingTimeCalculator.subtractWorkingTime(due, Duration.ofDays(serviceLevelDays));
assertThat(readTask.getPlanned()).isEqualTo(expectedPlanned);
}
@ -98,7 +95,8 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
assertThat(readTask.getPlanned()).isEqualTo(planned);
Instant expectedDue =
converter.addWorkingDaysToInstant(readTask.getPlanned(), Duration.ofDays(serviceLevelDays));
workingTimeCalculator.addWorkingTime(
readTask.getPlanned(), Duration.ofDays(serviceLevelDays));
assertThat(readTask.getDue()).isEqualTo(expectedDue);
}
@ -119,7 +117,7 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
// due date according to service level
Instant expectedDue =
converter.addWorkingDaysToInstant(newTask.getPlanned(), Duration.ofDays(duration));
workingTimeCalculator.addWorkingTime(newTask.getPlanned(), Duration.ofDays(duration));
newTask.setDue(expectedDue);
ThrowingCallable call = () -> taskService.createTask(newTask);
@ -365,10 +363,10 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
Instant dueBulk2 = taskService.getTask(tkId2).getDue();
Instant dueBulk3 = taskService.getTask(tkId3).getDue();
Instant dueBulk4 = taskService.getTask(tkId4).getDue();
assertThat(dueBulk1).isEqualTo(getInstant("2020-05-14T07:00:00"));
assertThat(dueBulk2).isEqualTo(getInstant("2020-05-22T07:00:00"));
assertThat(dueBulk3).isEqualTo(getInstant("2020-05-14T07:00:00"));
assertThat(dueBulk4).isEqualTo(getInstant("2020-05-22T07:00:00"));
assertThat(dueBulk1).isEqualTo(getInstant("2020-05-14T00:00:00"));
assertThat(dueBulk2).isEqualTo(getInstant("2020-05-21T00:00:00"));
assertThat(dueBulk3).isEqualTo(getInstant("2020-05-14T00:00:00"));
assertThat(dueBulk4).isEqualTo(getInstant("2020-05-21T00:00:00"));
}
@WithAccessId(user = "admin")
@ -399,7 +397,8 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
taskService.setPlannedPropertyOfTasks(planned, List.of(taskId));
Task task = taskService.getTask(taskId);
assertThat(results.containsErrors()).isFalse();
Instant expectedDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1));
Instant expectedDue =
workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1));
assertThat(task.getDue()).isEqualTo(expectedDue);
}
@ -411,7 +410,8 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
// test update of planned date via updateTask()
task.setPlanned(task.getPlanned().plus(Duration.ofDays(3)));
task = taskService.updateTask(task);
Instant expectedDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1));
Instant expectedDue =
workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1));
assertThat(task.getDue()).isEqualTo(expectedDue);
}
@ -432,14 +432,15 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
@Test
void should_SetDue_When_OnlyPlannedWasChanged() throws Exception {
String taskId = "TKI:000000000000000000000000000000000002";
Instant planned = getInstant("2020-05-03T07:00:00");
Instant planned = getInstant("2020-05-03T07:00:00"); // Sunday
Instant expectedPlanned = getInstant("2020-05-04T00:00:00");
Task task = taskService.getTask(taskId);
task.setPlanned(planned);
task = taskService.updateTask(task);
String serviceLevel = task.getClassificationSummary().getServiceLevel();
Instant expDue =
converter.addWorkingDaysToInstant(task.getPlanned(), Duration.parse(serviceLevel));
assertThat(task.getPlanned()).isEqualTo(planned);
workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.parse(serviceLevel));
assertThat(task.getPlanned()).isEqualTo(expectedPlanned);
assertThat(task.getDue()).isEqualTo(expDue);
}
@ -455,7 +456,7 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
String serviceLevel = task.getClassificationSummary().getServiceLevel();
Instant expPlanned =
converter.subtractWorkingDaysFromInstant(task.getDue(), Duration.parse(serviceLevel));
workingTimeCalculator.subtractWorkingTime(task.getDue(), Duration.parse(serviceLevel));
assertThat(task.getPlanned()).isEqualTo(expPlanned);
assertThat(task.getDue()).isEqualTo(due);
}
@ -469,53 +470,56 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
task.setPlanned(null);
task = taskService.updateTask(task);
Instant expectedDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1));
Instant expectedDue =
workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1));
assertThat(task.getDue()).isEqualTo(expectedDue);
task.setDue(null);
task = taskService.updateTask(task);
expectedDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1));
expectedDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1));
assertThat(task.getDue()).isEqualTo(expectedDue);
task.setPlanned(planned.plus(Duration.ofDays(13))); // Saturday
task.setDue(null);
task = taskService.updateTask(task);
expectedDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1));
expectedDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1));
assertThat(task.getDue()).isEqualTo(expectedDue);
task.setDue(planned.plus(Duration.ofDays(13))); // Saturday
task.setPlanned(null);
task = taskService.updateTask(task);
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-05-14T07:00:00"));
assertThat(task.getDue()).isEqualTo(getInstant("2020-05-15T07:00:00"));
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-05-15T00:00:00"));
assertThat(task.getDue()).isEqualTo(getInstant("2020-05-16T00:00:00"));
}
@WithAccessId(user = "user-1-2")
@Test
void should_UpdateTaskPlannedOrDue_When_PlannedOrDueAreWeekendDays() throws Exception {
Task task = taskService.getTask("TKI:000000000000000000000000000000000030"); // SL=P13D
task.setPlanned(getInstant("2020-03-21T07:00:00")); // planned = saturday
task.setPlanned(getInstant("2020-03-23T07:00:00")); // planned = saturday
task = taskService.updateTask(task);
assertThat(task.getDue()).isEqualTo(getInstant("2020-04-09T07:00:00"));
task.setDue(getInstant("2020-04-11T07:00:00")); // due = saturday
task.setPlanned(null);
task = taskService.updateTask(task);
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00"));
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-24T00:00:00"));
task.setDue(getInstant("2020-04-12T07:00:00")); // due = sunday
task = taskService.updateTask(task);
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00"));
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-24T00:00:00"));
task.setPlanned(getInstant("2020-03-21T07:00:00")); // planned = saturday
task.setDue(getInstant("2020-04-09T07:00:00")); // thursday
task.setDue(getInstant("2020-04-09T00:00:00")); // thursday
task = taskService.updateTask(task);
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00"));
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T00:00:00"));
task.setPlanned(getInstant("2020-03-03T07:00:00")); // planned on tuesday
task.setPlanned(getInstant("2020-03-04T00:00:00")); // planned on tuesday
task.setDue(getInstant("2020-03-22T07:00:00")); // due = sunday
task = taskService.updateTask(task);
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-20T07:00:00")); // friday
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-21T00:00:00")); // friday, EOB
}
@WithAccessId(user = "user-1-1")
@ -532,51 +536,50 @@ class ServiceLevelPriorityAccTest extends AbstractAccTest {
// planned changed, due did not change
task.setPlanned(getInstant("2020-03-21T07:00:00")); // Saturday
task = taskService.updateTask(task);
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T00:00:00")); // Monday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T00:00:00")); // Monday
// due changed, planned did not change
task.setDue(getInstant("2020-04-12T07:00:00")); // Sunday
task = taskService.updateTask(task);
assertThat(task.getPlanned())
.isEqualTo(getInstant("2020-04-09T07:00:00")); // Thursday (skip Good Friday)
assertThat(task.getDue()).isEqualTo(getInstant("2020-04-09T07:00:00"));
Instant endOfHolyThursday = getInstant("2020-04-10T00:00:00");
assertThat(task.getPlanned()).isEqualTo(endOfHolyThursday); // Thursday (skip Good Friday)
assertThat(task.getDue()).isEqualTo(endOfHolyThursday);
// due changed, planned is null
task.setDue(getInstant("2020-04-11T07:00:00")); // Saturday
task.setPlanned(null);
task = taskService.updateTask(task);
assertThat(task.getPlanned())
.isEqualTo(getInstant("2020-04-09T07:00:00")); // Thursday (skip Good Friday)
assertThat(task.getDue()).isEqualTo(getInstant("2020-04-09T07:00:00"));
assertThat(task.getPlanned()).isEqualTo(endOfHolyThursday); // Thursday (skip Good Friday)
assertThat(task.getDue()).isEqualTo(endOfHolyThursday);
// planned changed, due is null
task.setPlanned(getInstant("2020-03-22T07:00:00")); // Sunday
task.setDue(null);
task = taskService.updateTask(task);
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T00:00:00")); // Monday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T00:00:00")); // Monday
// both changed, not null (due at weekend)
task.setPlanned(getInstant("2020-03-20T07:00:00")); // Friday
task.setPlanned(getInstant("2020-03-21T00:00:00")); // Friday
task.setDue(getInstant("2020-03-22T07:00:00")); // Sunday
task = taskService.updateTask(task);
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-20T07:00:00")); // Friday
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-20T07:00:00")); // Friday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-21T00:00:00")); // Friday
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-21T00:00:00")); // Friday
// both changed, not null (planned at weekend)
task.setPlanned(getInstant("2020-03-22T07:00:00")); // Sunday
task.setDue(getInstant("2020-03-23T07:00:00")); // Monday
task.setDue(getInstant("2020-03-23T00:00:00")); // Monday
task = taskService.updateTask(task);
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T00:00:00")); // Monday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T00:00:00")); // Monday
// both changed, not null (both at weekend) within SLA
task.setPlanned(getInstant("2020-03-22T07:00:00")); // Sunday
task.setDue(getInstant("2020-03-22T07:00:00")); // Sunday
task = taskService.updateTask(task);
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-20T07:00:00")); // Friday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-20T07:00:00")); // Friday
assertThat(task.getDue()).isEqualTo(getInstant("2020-03-21T00:00:00")); // Friday
assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-21T00:00:00")); // Friday
// both changed, not null (planned > due)
task.setPlanned(getInstant("2020-03-24T07:00:00")); // Tuesday

View File

@ -556,7 +556,8 @@ class CreateTaskAccTest extends AbstractAccTest {
assertThat(readTask.getPriority()).isEqualTo(99);
Instant expDue = converter.addWorkingDaysToInstant(readTask.getPlanned(), Duration.ofDays(1));
Instant expDue =
workingTimeCalculator.addWorkingTime(readTask.getPlanned(), Duration.ofDays(1));
assertThat(readTask.getDue()).isEqualTo(expDue);
}

View File

@ -198,7 +198,7 @@ class UpdateTaskAttachmentsAccTest extends AbstractAccTest {
assertThat(task.getAttachments().get(0).getChannel()).isEqualTo(newChannel);
assertThat(task.getPriority()).isEqualTo(99);
Instant expDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1));
Instant expDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1));
assertThat(task.getDue()).isEqualTo(expDue);
}
@ -321,7 +321,7 @@ class UpdateTaskAttachmentsAccTest extends AbstractAccTest {
assertThat(task.getAttachments()).hasSize(attachmentCount);
assertThat(task.getAttachments().get(0).getChannel()).isEqualTo(newChannel);
assertThat(task.getPriority()).isEqualTo(99);
Instant expDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1));
Instant expDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1));
assertThat(task.getDue()).isEqualTo(expDue);
}
@ -350,7 +350,7 @@ class UpdateTaskAttachmentsAccTest extends AbstractAccTest {
task = taskService.getTask(task.getId());
assertThat(task.getPriority()).isEqualTo(101);
Instant expDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(1));
Instant expDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1));
assertThat(task.getDue()).isEqualTo(expDue);
assertThat(task.getAttachments())
.hasSize(2)
@ -383,7 +383,7 @@ class UpdateTaskAttachmentsAccTest extends AbstractAccTest {
task = taskService.getTask(task.getId());
assertThat(task.getPriority()).isEqualTo(99);
expDue = converter.addWorkingDaysToInstant(task.getPlanned(), Duration.ofDays(16));
expDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(16));
assertThat(task.getDue()).isEqualTo(expDue);
assertThat(task.getAttachments())
.hasSize(2)
@ -513,7 +513,8 @@ class UpdateTaskAttachmentsAccTest extends AbstractAccTest {
assertThat(readTask.getPriority()).isEqualTo(99);
Instant expDue = converter.addWorkingDaysToInstant(readTask.getPlanned(), Duration.ofDays(1));
Instant expDue =
workingTimeCalculator.addWorkingTime(readTask.getPlanned(), Duration.ofDays(1));
assertThat(readTask.getDue()).isEqualTo(expDue);
}

View File

@ -19,4 +19,8 @@ taskana.german.holidays.corpus-christi.enabled=false
taskana.history.deletion.on.task.deletion.enabled=true
taskana.validation.allowTimestampServiceLevelMismatch=false
taskana.query.includeLongName=false
taskana.workingtime.schedule.MONDAY=00:00-00:00
taskana.workingtime.schedule.TUESDAY=00:00-00:00
taskana.workingtime.schedule.WEDNESDAY=00:00-00:00
taskana.workingtime.schedule.THURSDAY=00:00-00:00
taskana.workingtime.schedule.FRIDAY=00:00-00:00

View File

@ -22,7 +22,7 @@ import pro.taskana.common.api.ConfigurationService;
import pro.taskana.common.api.JobService;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.TaskanaEngine.ConnectionManagementMode;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.security.CurrentUserContext;
import pro.taskana.common.internal.ConfigurationMapper;
import pro.taskana.common.internal.ConfigurationServiceImpl;
@ -32,6 +32,7 @@ import pro.taskana.common.internal.TaskanaEngineImpl;
import pro.taskana.common.internal.security.CurrentUserContextImpl;
import pro.taskana.common.internal.util.ReflectionUtil;
import pro.taskana.common.internal.util.SpiLoader;
import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl;
import pro.taskana.monitor.api.MonitorService;
import pro.taskana.monitor.internal.MonitorServiceImpl;
import pro.taskana.task.api.TaskService;
@ -106,8 +107,7 @@ public class TaskanaInitializationExtension implements TestInstancePostProcessor
throw new JUnitException("Expected dataSource to be defined in store, but it's not.");
}
return new TaskanaConfiguration.Builder(dataSource, false, schemaName)
.initTaskanaProperties();
return new TaskanaConfiguration.Builder(dataSource, false, schemaName).initTaskanaProperties();
}
private static Map<Class<?>, Object> generateTaskanaEntityMap(TaskanaEngine taskanaEngine)
@ -122,6 +122,7 @@ public class TaskanaInitializationExtension implements TestInstancePostProcessor
CurrentUserContext currentUserContext = taskanaEngine.getCurrentUserContext();
UserService userService = taskanaEngine.getUserService();
SqlSession sqlSession = taskanaEngineProxy.getSqlSession();
WorkingTimeCalculator workingTimeCalculator = taskanaEngine.getWorkingTimeCalculator();
return Map.ofEntries(
Map.entry(TaskanaConfiguration.class, taskanaEngine.getConfiguration()),
Map.entry(TaskanaEngineImpl.class, taskanaEngine),
@ -141,7 +142,8 @@ public class TaskanaInitializationExtension implements TestInstancePostProcessor
Map.entry(JobServiceImpl.class, jobService),
Map.entry(CurrentUserContext.class, currentUserContext),
Map.entry(CurrentUserContextImpl.class, currentUserContext),
Map.entry(WorkingDaysToDaysConverter.class, taskanaEngine.getWorkingDaysToDaysConverter()),
Map.entry(WorkingTimeCalculator.class, workingTimeCalculator),
Map.entry(WorkingTimeCalculatorImpl.class, workingTimeCalculator),
Map.entry(ConfigurationMapper.class, sqlSession.getMapper(ConfigurationMapper.class)),
Map.entry(UserService.class, userService),
Map.entry(UserServiceImpl.class, userService));

View File

@ -10,7 +10,7 @@ import pro.taskana.classification.internal.ClassificationServiceImpl;
import pro.taskana.common.api.ConfigurationService;
import pro.taskana.common.api.JobService;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.security.CurrentUserContext;
import pro.taskana.common.internal.ConfigurationMapper;
import pro.taskana.common.internal.ConfigurationServiceImpl;
@ -18,6 +18,7 @@ import pro.taskana.common.internal.InternalTaskanaEngine;
import pro.taskana.common.internal.JobServiceImpl;
import pro.taskana.common.internal.TaskanaEngineImpl;
import pro.taskana.common.internal.security.CurrentUserContextImpl;
import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl;
import pro.taskana.monitor.api.MonitorService;
import pro.taskana.monitor.internal.MonitorServiceImpl;
import pro.taskana.task.api.TaskService;
@ -48,7 +49,8 @@ class TaskanaDependencyInjectionExtensionTest {
@TaskanaInject JobServiceImpl jobServiceImpl;
@TaskanaInject ConfigurationService configurationService;
@TaskanaInject ConfigurationServiceImpl configurationServiceImpl;
@TaskanaInject WorkingDaysToDaysConverter workingDaysToDaysConverter;
@TaskanaInject WorkingTimeCalculator workingTimeCalculator;
@TaskanaInject WorkingTimeCalculatorImpl workingTimeCalculatorImpl;
@TaskanaInject CurrentUserContext currentUserContext;
@TaskanaInject CurrentUserContextImpl currentUserContextImpl;
@TaskanaInject ConfigurationMapper configurationMapper;
@ -194,9 +196,21 @@ class TaskanaDependencyInjectionExtensionTest {
}
@Test
void should_InjectWorkingDaysToDaysConverter_When_FieldIsAnnotatedOrDeclaredAsParameter(
WorkingDaysToDaysConverter workingDaysToDaysConverter) {
assertThat(workingDaysToDaysConverter).isSameAs(this.workingDaysToDaysConverter).isNotNull();
void should_InjectWorkingTimeCalculator_When_FieldIsAnnotatedOrDeclaredAsParameter(
WorkingTimeCalculator workingTimeCalculator) {
assertThat(workingTimeCalculator)
.isSameAs(this.workingTimeCalculator)
.isSameAs(this.workingTimeCalculatorImpl)
.isNotNull();
}
@Test
void should_InjectWorkingTimeCalculatorImpl_When_FieldIsAnnotatedOrDeclaredAsParameter(
WorkingTimeCalculatorImpl workingTimeCalculatorImpl) {
assertThat(workingTimeCalculatorImpl)
.isSameAs(this.workingTimeCalculatorImpl)
.isSameAs(this.workingTimeCalculator)
.isNotNull();
}
@Test

View File

@ -5,7 +5,6 @@ import org.junit.jupiter.api.BeforeAll;
import pro.taskana.common.api.TaskanaEngine;
import pro.taskana.common.api.TaskanaEngine.ConnectionManagementMode;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.internal.configuration.DbSchemaCreator;
import pro.taskana.common.test.config.DataSourceGenerator;
import pro.taskana.sampledata.SampleDataGenerator;
@ -16,7 +15,6 @@ public abstract class AbstractAccTest {
protected static TaskanaConfiguration taskanaEngineConfiguration;
protected static TaskanaEngine taskanaEngine;
protected static WorkingDaysToDaysConverter converter;
@BeforeAll
protected static void setupTest() throws Exception {
@ -45,7 +43,6 @@ public abstract class AbstractAccTest {
taskanaEngine =
TaskanaEngine.buildTaskanaEngine(
taskanaEngineConfiguration, ConnectionManagementMode.AUTOCOMMIT);
converter = taskanaEngine.getWorkingDaysToDaysConverter();
}
protected ObjectReference createObjectReference(