TSK-1972: Uses time zone for working time calculations
This commit is contained in:
parent
37280cc73b
commit
b916d577ca
|
@ -8,6 +8,7 @@ import java.lang.reflect.Type;
|
|||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
|
@ -52,6 +53,7 @@ public class TaskanaConfigurationInitializer {
|
|||
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());
|
||||
PROPERTY_INITIALIZER_BY_CLASS.put(ZoneId.class, new ZoneIdPropertyParser());
|
||||
PROPERTY_INITIALIZER_BY_CLASS.put(Enum.class, new EnumPropertyParser());
|
||||
}
|
||||
|
||||
|
@ -432,6 +434,18 @@ public class TaskanaConfigurationInitializer {
|
|||
}
|
||||
}
|
||||
|
||||
static class ZoneIdPropertyParser implements PropertyParser<ZoneId> {
|
||||
|
||||
@Override
|
||||
public Optional<ZoneId> initialize(
|
||||
Map<String, String> properties,
|
||||
String separator,
|
||||
Field field,
|
||||
TaskanaProperty taskanaProperty) {
|
||||
return parseProperty(properties, taskanaProperty.value(), ZoneId::of);
|
||||
}
|
||||
}
|
||||
|
||||
static class EnumPropertyParser implements PropertyParser<Enum<?>> {
|
||||
@Override
|
||||
public Optional<Enum<?>> initialize(
|
||||
|
|
|
@ -6,8 +6,10 @@ import java.time.Instant;
|
|||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.SortedSet;
|
||||
|
@ -18,31 +20,36 @@ import pro.taskana.common.api.exceptions.InvalidArgumentException;
|
|||
|
||||
public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
||||
|
||||
static final ZoneOffset UTC = ZoneOffset.UTC;
|
||||
private final ZoneId zoneId;
|
||||
|
||||
private final HolidaySchedule holidaySchedule;
|
||||
private final WorkingTimeSchedule workingTimeSchedule;
|
||||
|
||||
public WorkingTimeCalculatorImpl(
|
||||
HolidaySchedule holidaySchedule, Map<DayOfWeek, Set<LocalTimeInterval>> workingTimeSchedule) {
|
||||
HolidaySchedule holidaySchedule,
|
||||
Map<DayOfWeek, Set<LocalTimeInterval>> workingTimeSchedule,
|
||||
ZoneId zoneId) {
|
||||
this.holidaySchedule = holidaySchedule;
|
||||
this.workingTimeSchedule = new WorkingTimeSchedule(workingTimeSchedule);
|
||||
this.zoneId = Objects.requireNonNull(zoneId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant subtractWorkingTime(Instant workStart, Duration workingTime)
|
||||
throws InvalidArgumentException {
|
||||
validatePositiveDuration(workingTime);
|
||||
WorkSlot workSlot = getWorkSlotOrPrevious(toLocalDateTime(workStart));
|
||||
return workSlot.subtractWorkingTime(workStart, workingTime);
|
||||
ZonedDateTime workStartInTimeZone = toZonedDateTime(workStart);
|
||||
WorkSlot workSlot = getWorkSlotOrPrevious(workStartInTimeZone);
|
||||
return workSlot.subtractWorkingTime(workStartInTimeZone, workingTime).toInstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Instant addWorkingTime(Instant workStart, Duration workingTime)
|
||||
throws InvalidArgumentException {
|
||||
validatePositiveDuration(workingTime);
|
||||
WorkSlot bestMatchingWorkSlot = getWorkSlotOrNext(toLocalDateTime(workStart));
|
||||
return bestMatchingWorkSlot.addWorkingTime(workStart, workingTime);
|
||||
ZonedDateTime workStartInTimeZone = toZonedDateTime(workStart);
|
||||
WorkSlot workSlot = getWorkSlotOrNext(workStartInTimeZone);
|
||||
return workSlot.addWorkingTime(workStartInTimeZone, workingTime).toInstant();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -60,24 +67,7 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
|||
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));
|
||||
}
|
||||
return calculateWorkingTime(toZonedDateTime(from), toZonedDateTime(to));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -101,6 +91,29 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
|||
return holidaySchedule.isGermanHoliday(toLocalDate(instant));
|
||||
}
|
||||
|
||||
private Duration calculateWorkingTime(ZonedDateTime from, ZonedDateTime to)
|
||||
throws InvalidArgumentException {
|
||||
|
||||
WorkSlot bestMatchingWorkSlot = getWorkSlotOrNext(from);
|
||||
ZonedDateTime earliestWorkStart = max(from, bestMatchingWorkSlot.start);
|
||||
ZonedDateTime 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(calculateWorkingTime(endOfWorkSlot, to));
|
||||
}
|
||||
}
|
||||
|
||||
private void validateNonNullInstants(Instant first, Instant second) {
|
||||
if (first == null || second == null) {
|
||||
throw new InvalidArgumentException("Neither first nor second may be null.");
|
||||
|
@ -118,11 +131,11 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
|||
* 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
|
||||
* @param currentDateTime The ZonedDateTime 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) {
|
||||
private WorkSlot getWorkSlotOrNext(ZonedDateTime currentDateTime) {
|
||||
LocalDate currentDate = currentDateTime.toLocalDate();
|
||||
// We do not work on Holidays
|
||||
if (holidaySchedule.isHoliday(currentDate)) {
|
||||
|
@ -149,11 +162,11 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
|||
* 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
|
||||
* @param currentDateTime The ZonedDateTime 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) {
|
||||
private WorkSlot getWorkSlotOrPrevious(ZonedDateTime currentDateTime) {
|
||||
LocalDate currentDate = currentDateTime.toLocalDate();
|
||||
// We do not work on Holidays
|
||||
if (holidaySchedule.isHoliday(currentDate)) {
|
||||
|
@ -177,31 +190,41 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
|||
getWorkSlotOrPrevious(getDayBefore(currentDateTime)));
|
||||
}
|
||||
|
||||
private static boolean isBeforeOrEquals(LocalTime time, LocalDateTime currentDateTime) {
|
||||
private static boolean isBeforeOrEquals(LocalTime time, ZonedDateTime 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 ZonedDateTime getDayAfter(ZonedDateTime current) {
|
||||
return LocalDateTime.of(current.toLocalDate().plusDays(1), LocalTime.MIN)
|
||||
.atZone(current.getZone());
|
||||
}
|
||||
|
||||
private DayOfWeek toDayOfWeek(Instant instant) {
|
||||
return toLocalDate(instant).getDayOfWeek();
|
||||
}
|
||||
|
||||
private static Instant max(Instant a, Instant b) {
|
||||
private ZonedDateTime getDayBefore(ZonedDateTime current) {
|
||||
return LocalDateTime.of(current.toLocalDate().minusDays(1), LocalTime.MAX)
|
||||
.atZone(current.getZone());
|
||||
}
|
||||
|
||||
private ZonedDateTime toZonedDateTime(Instant instant) {
|
||||
return instant.atZone(zoneId);
|
||||
}
|
||||
|
||||
private ZonedDateTime toZonedDateTime(LocalDateTime localDateTime) {
|
||||
return localDateTime.atZone(zoneId);
|
||||
}
|
||||
|
||||
private ZonedDateTime toZonedDateTime(LocalDate day, LocalTime time) {
|
||||
return toZonedDateTime(LocalDateTime.of(day, time));
|
||||
}
|
||||
|
||||
private LocalDate toLocalDate(Instant instant) {
|
||||
return LocalDate.ofInstant(instant, zoneId);
|
||||
}
|
||||
|
||||
private static ZonedDateTime max(ZonedDateTime a, ZonedDateTime b) {
|
||||
if (a.isAfter(b)) {
|
||||
return a;
|
||||
} else {
|
||||
|
@ -209,7 +232,7 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
|||
}
|
||||
}
|
||||
|
||||
private static Instant min(Instant a, Instant b) {
|
||||
private static ZonedDateTime min(ZonedDateTime a, ZonedDateTime b) {
|
||||
if (a.isBefore(b)) {
|
||||
return a;
|
||||
} else {
|
||||
|
@ -219,21 +242,34 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
|||
|
||||
class WorkSlot {
|
||||
|
||||
private final Instant start;
|
||||
private final Instant end;
|
||||
private final ZonedDateTime start;
|
||||
private final ZonedDateTime end;
|
||||
|
||||
public WorkSlot(LocalDate day, LocalTimeInterval interval) {
|
||||
this.start = LocalDateTime.of(day, interval.getBegin()).toInstant(UTC);
|
||||
this.start = toZonedDateTime(day, interval.getBegin());
|
||||
if (interval.getEnd().equals(LocalTime.MAX)) {
|
||||
this.end = day.plusDays(1).atStartOfDay().toInstant(UTC);
|
||||
this.end = toZonedDateTime(day.plusDays(1).atStartOfDay());
|
||||
} else {
|
||||
this.end = LocalDateTime.of(day, interval.getEnd()).toInstant(UTC);
|
||||
this.end = toZonedDateTime(day, interval.getEnd());
|
||||
}
|
||||
}
|
||||
|
||||
public Instant addWorkingTime(Instant workStart, Duration workingTime) {
|
||||
private ZonedDateTime subtractWorkingTime(ZonedDateTime workStart, Duration workingTime) {
|
||||
// _workStart_ might be outside the working hours. We need to adjust the end accordingly.
|
||||
ZonedDateTime 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 ZonedDateTime addWorkingTime(ZonedDateTime workStart, Duration workingTime) {
|
||||
// _workStart_ might be outside the working hours. We need to adjust the start accordingly.
|
||||
Instant earliestWorkStart = max(workStart, start);
|
||||
ZonedDateTime 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
|
||||
|
@ -247,26 +283,13 @@ public class WorkingTimeCalculatorImpl implements WorkingTimeCalculator {
|
|||
}
|
||||
}
|
||||
|
||||
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)));
|
||||
return getWorkSlotOrPrevious(start.minusNanos(1));
|
||||
}
|
||||
|
||||
private WorkSlot next() {
|
||||
return getWorkSlotOrNext(toLocalDateTime(end));
|
||||
return getWorkSlotOrNext(end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,11 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
|||
import java.time.DayOfWeek;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
|
@ -33,7 +37,8 @@ class WorkingTimeCalculatorImplTest {
|
|||
DayOfWeek.TUESDAY, standardWorkingSlots,
|
||||
DayOfWeek.WEDNESDAY, standardWorkingSlots,
|
||||
DayOfWeek.THURSDAY, standardWorkingSlots,
|
||||
DayOfWeek.FRIDAY, standardWorkingSlots));
|
||||
DayOfWeek.FRIDAY, standardWorkingSlots),
|
||||
ZoneOffset.UTC);
|
||||
|
||||
@Nested
|
||||
class WorkingTimeAddition {
|
||||
|
@ -382,7 +387,8 @@ class WorkingTimeCalculatorImplTest {
|
|||
DayOfWeek.TUESDAY, standardWorkday,
|
||||
DayOfWeek.WEDNESDAY, standardWorkday,
|
||||
DayOfWeek.THURSDAY, standardWorkday,
|
||||
DayOfWeek.FRIDAY, standardWorkday));
|
||||
DayOfWeek.FRIDAY, standardWorkday),
|
||||
ZoneOffset.UTC);
|
||||
|
||||
@Test
|
||||
void addTimeToMatchEndOfFirstAndStartOfSecondSlot() {
|
||||
|
@ -417,7 +423,8 @@ class WorkingTimeCalculatorImplTest {
|
|||
DayOfWeek.TUESDAY, completeWorkDay,
|
||||
DayOfWeek.WEDNESDAY, completeWorkDay,
|
||||
DayOfWeek.THURSDAY, completeWorkDay,
|
||||
DayOfWeek.FRIDAY, completeWorkDay));
|
||||
DayOfWeek.FRIDAY, completeWorkDay),
|
||||
ZoneOffset.UTC);
|
||||
|
||||
@Test
|
||||
void withDurationOfZeroOnHolySaturday() {
|
||||
|
@ -463,7 +470,8 @@ class WorkingTimeCalculatorImplTest {
|
|||
private final WorkingTimeCalculator cut =
|
||||
new WorkingTimeCalculatorImpl(
|
||||
new HolidaySchedule(true, false),
|
||||
Map.of(DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX))));
|
||||
Map.of(DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX))),
|
||||
ZoneOffset.UTC);
|
||||
|
||||
@Test
|
||||
void returnsTrueIfWorkingTimeScheduleIsDefinedForDayOfWeek() {
|
||||
|
@ -498,4 +506,102 @@ class WorkingTimeCalculatorImplTest {
|
|||
.isThrownBy(() -> cut.isWorkingDay(null));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WorkingTimeWithNonUtcTimeZoneAcrossDaylightSavingTimeSwitch {
|
||||
|
||||
private final ZoneId cet = ZoneId.of("Europe/Berlin");
|
||||
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),
|
||||
cet);
|
||||
|
||||
@Test
|
||||
void addsWorkingTimeCorrectly() {
|
||||
Instant fridayBefore =
|
||||
ZonedDateTime.of(LocalDateTime.parse("2022-03-25T09:30:00"), cet).toInstant();
|
||||
|
||||
Instant dueDate = cut.addWorkingTime(fridayBefore, Duration.ofHours(17));
|
||||
|
||||
assertThat(dueDate)
|
||||
.isEqualTo(ZonedDateTime.of(LocalDateTime.parse("2022-03-28T02:30:00"), cet).toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void subtractsWorkingTimeCorrectly() {
|
||||
Instant mondayAfter =
|
||||
ZonedDateTime.of(LocalDateTime.parse("2022-10-31T08:54:00"), cet).toInstant();
|
||||
|
||||
Instant dueDate = cut.subtractWorkingTime(mondayAfter, Duration.ofHours(18));
|
||||
|
||||
assertThat(dueDate)
|
||||
.isEqualTo(ZonedDateTime.of(LocalDateTime.parse("2022-10-28T14:54:00"), cet).toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculatesWorkingTimeBetweenCorrectly() {
|
||||
Instant fridayBefore =
|
||||
ZonedDateTime.of(LocalDateTime.parse("2023-03-24T09:30:00"), cet).toInstant();
|
||||
Instant mondayAfter =
|
||||
ZonedDateTime.of(LocalDateTime.parse("2023-03-27T02:30:00"), cet).toInstant();
|
||||
|
||||
Duration duration = cut.workingTimeBetween(fridayBefore, mondayAfter);
|
||||
|
||||
assertThat(duration).isEqualTo(Duration.ofHours(17));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WorkingTimeWithWorkSlotsSpanningAcrossDaylightSavingTimeSwitch {
|
||||
|
||||
ZoneId cet = ZoneId.of("Europe/Berlin");
|
||||
WorkingTimeCalculator cut =
|
||||
new WorkingTimeCalculatorImpl(
|
||||
new HolidaySchedule(true, false),
|
||||
Map.of(DayOfWeek.SUNDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.MAX))),
|
||||
cet);
|
||||
|
||||
@Test
|
||||
void addsCorrectly() {
|
||||
Instant beforeSwitch =
|
||||
ZonedDateTime.of(LocalDateTime.parse("2023-03-26T00:00:00"), cet).toInstant();
|
||||
|
||||
Instant afterSwitch = cut.addWorkingTime(beforeSwitch, Duration.ofHours(3));
|
||||
|
||||
assertThat(afterSwitch)
|
||||
.isEqualTo(ZonedDateTime.of(LocalDateTime.parse("2023-03-26T04:00:00"), cet).toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void subtractsCorrectly() {
|
||||
Instant afterSwitch =
|
||||
ZonedDateTime.of(LocalDateTime.parse("2023-03-26T05:00:00"), cet).toInstant();
|
||||
|
||||
Instant beforeSwitch = cut.subtractWorkingTime(afterSwitch, Duration.ofHours(3));
|
||||
|
||||
assertThat(beforeSwitch)
|
||||
.isEqualTo(ZonedDateTime.of(LocalDateTime.parse("2023-03-26T01:00:00"), cet).toInstant());
|
||||
}
|
||||
|
||||
@Test
|
||||
void calculatesWorkingTimeBetweenCorrectly() {
|
||||
Instant beforeSwitch =
|
||||
ZonedDateTime.of(LocalDateTime.parse("2023-03-26T00:00:00"), cet).toInstant();
|
||||
Instant afterSwitch =
|
||||
ZonedDateTime.of(LocalDateTime.parse("2023-03-26T09:00:00"), cet).toInstant();
|
||||
|
||||
Duration duration = cut.workingTimeBetween(beforeSwitch, afterSwitch);
|
||||
|
||||
assertThat(duration).isEqualTo(Duration.ofHours(8));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ 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
|
||||
taskana.workingtime.timezone=UTC
|
||||
# enable or disable the jobscheduler at all
|
||||
# set it to false and no jobs are running
|
||||
taskana.jobscheduler.enabled=false
|
||||
|
@ -40,4 +41,3 @@ taskana.jobscheduler.enableTaskUpdatePriorityJob=true
|
|||
taskana.jobscheduler.enableWorkbasketCleanupJob=true
|
||||
taskana.jobscheduler.enableUserInfoRefreshJob=false
|
||||
taskana.jobscheduler.enableHistorieCleanupJob=true
|
||||
|
||||
|
|
|
@ -9,6 +9,8 @@ import java.time.DayOfWeek;
|
|||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZoneOffset;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
@ -114,6 +116,7 @@ class TaskanaConfigurationTest {
|
|||
long expectedJobSchedulerPeriod = 10;
|
||||
TimeUnit expectedJobSchedulerPeriodTimeUnit = TimeUnit.DAYS;
|
||||
List<String> expectedJobSchedulerCustomJobs = List.of("Job_A", "Job_B");
|
||||
ZoneId expectedWorkingTimeScheduleTimeZone = ZoneId.ofOffset("UTC", ZoneOffset.ofHours(4));
|
||||
|
||||
// when
|
||||
Map<DayOfWeek, Set<LocalTimeInterval>> expectedWorkingTimeSchedule =
|
||||
|
@ -153,6 +156,7 @@ class TaskanaConfigurationTest {
|
|||
.jobSchedulerEnableHistorieCleanupJob(false)
|
||||
.jobSchedulerCustomJobs(expectedJobSchedulerCustomJobs)
|
||||
.workingTimeSchedule(expectedWorkingTimeSchedule)
|
||||
.workingTimeScheduleTimeZone(expectedWorkingTimeScheduleTimeZone)
|
||||
.build();
|
||||
|
||||
// then
|
||||
|
@ -194,6 +198,8 @@ class TaskanaConfigurationTest {
|
|||
assertThat(configuration.isJobSchedulerEnableHistorieCleanupJob()).isFalse();
|
||||
assertThat(configuration.getJobSchedulerCustomJobs()).isEqualTo(expectedJobSchedulerCustomJobs);
|
||||
assertThat(configuration.getWorkingTimeSchedule()).isEqualTo(expectedWorkingTimeSchedule);
|
||||
assertThat(configuration.getWorkingTimeScheduleTimeZone())
|
||||
.isEqualTo(expectedWorkingTimeScheduleTimeZone);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -236,6 +242,7 @@ class TaskanaConfigurationTest {
|
|||
.workingTimeSchedule(
|
||||
Map.of(
|
||||
DayOfWeek.MONDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.NOON))))
|
||||
.workingTimeScheduleTimeZone(ZoneId.ofOffset("UTC", ZoneOffset.ofHours(4)))
|
||||
.build();
|
||||
|
||||
TaskanaConfiguration copyConfiguration = new Builder(configuration).build();
|
||||
|
|
|
@ -12,6 +12,7 @@ import java.time.DayOfWeek;
|
|||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumMap;
|
||||
|
@ -75,6 +76,8 @@ public class TaskanaConfiguration {
|
|||
private final boolean corpusChristiEnabled;
|
||||
|
||||
private final Map<DayOfWeek, Set<LocalTimeInterval>> workingTimeSchedule;
|
||||
|
||||
private final ZoneId workingTimeScheduleTimeZone;
|
||||
// endregion
|
||||
|
||||
// region history configuration
|
||||
|
@ -165,6 +168,7 @@ public class TaskanaConfiguration {
|
|||
.collect(
|
||||
Collectors.toUnmodifiableMap(
|
||||
Entry::getKey, e -> Collections.unmodifiableSet(e.getValue())));
|
||||
this.workingTimeScheduleTimeZone = builder.workingTimeScheduleTimeZone;
|
||||
this.jobBatchSize = builder.jobBatchSize;
|
||||
this.maxNumberOfJobRetries = builder.maxNumberOfJobRetries;
|
||||
this.cleanupJobFirstRun = builder.cleanupJobFirstRun;
|
||||
|
@ -249,6 +253,10 @@ public class TaskanaConfiguration {
|
|||
return workingTimeSchedule;
|
||||
}
|
||||
|
||||
public ZoneId getWorkingTimeScheduleTimeZone() {
|
||||
return workingTimeScheduleTimeZone;
|
||||
}
|
||||
|
||||
public Map<TaskanaRole, Set<String>> getRoleMap() {
|
||||
return roleMap;
|
||||
}
|
||||
|
@ -507,6 +515,9 @@ public class TaskanaConfiguration {
|
|||
@TaskanaProperty("taskana.workingtime.schedule")
|
||||
private Map<DayOfWeek, Set<LocalTimeInterval>> workingTimeSchedule =
|
||||
initDefaultWorkingTimeSchedule();
|
||||
|
||||
@TaskanaProperty("taskana.workingtime.timezone")
|
||||
private ZoneId workingTimeScheduleTimeZone = ZoneId.of("Europe/Berlin");
|
||||
// endregion
|
||||
|
||||
// region history configuration
|
||||
|
@ -610,6 +621,7 @@ public class TaskanaConfiguration {
|
|||
this.germanPublicHolidaysEnabled = conf.isGermanPublicHolidaysEnabled();
|
||||
this.corpusChristiEnabled = conf.isCorpusChristiEnabled();
|
||||
this.workingTimeSchedule = conf.getWorkingTimeSchedule();
|
||||
this.workingTimeScheduleTimeZone = conf.getWorkingTimeScheduleTimeZone();
|
||||
this.jobBatchSize = conf.getJobBatchSize();
|
||||
this.maxNumberOfJobRetries = conf.getMaxNumberOfJobRetries();
|
||||
this.cleanupJobFirstRun = conf.getCleanupJobFirstRun();
|
||||
|
@ -656,93 +668,78 @@ public class TaskanaConfiguration {
|
|||
|
||||
// region builder methods
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
// TODO: why do we need this method?
|
||||
public Builder schemaName(String schemaName) {
|
||||
this.schemaName = initSchemaName(schemaName);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder roleMap(Map<TaskanaRole, Set<String>> roleMap) {
|
||||
this.roleMap = roleMap;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder domains(List<String> domains) {
|
||||
this.domains = domains;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder classificationTypes(List<String> classificationTypes) {
|
||||
this.classificationTypes = classificationTypes;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder classificationCategoriesByTypeMap(
|
||||
Map<String, List<String>> classificationCategoriesByTypeMap) {
|
||||
this.classificationCategoriesByType = classificationCategoriesByTypeMap;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder customHolidays(List<CustomHoliday> customHolidays) {
|
||||
this.customHolidays = customHolidays;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder deleteHistoryOnTaskDeletionEnabled(boolean deleteHistoryOnTaskDeletionEnabled) {
|
||||
this.deleteHistoryOnTaskDeletionEnabled = deleteHistoryOnTaskDeletionEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder germanPublicHolidaysEnabled(boolean germanPublicHolidaysEnabled) {
|
||||
this.germanPublicHolidaysEnabled = germanPublicHolidaysEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder corpusChristiEnabled(boolean corpusChristiEnabled) {
|
||||
this.corpusChristiEnabled = corpusChristiEnabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder jobBatchSize(int jobBatchSize) {
|
||||
this.jobBatchSize = jobBatchSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder maxNumberOfJobRetries(int maxNumberOfJobRetries) {
|
||||
this.maxNumberOfJobRetries = maxNumberOfJobRetries;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder cleanupJobFirstRun(Instant cleanupJobFirstRun) {
|
||||
this.cleanupJobFirstRun = cleanupJobFirstRun;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder cleanupJobRunEvery(Duration cleanupJobRunEvery) {
|
||||
this.cleanupJobRunEvery = cleanupJobRunEvery;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder cleanupJobMinimumAge(Duration cleanupJobMinimumAge) {
|
||||
this.cleanupJobMinimumAge = cleanupJobMinimumAge;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder taskCleanupJobAllCompletedSameParentBusiness(
|
||||
boolean taskCleanupJobAllCompletedSameParentBusiness) {
|
||||
this.taskCleanupJobAllCompletedSameParentBusiness =
|
||||
|
@ -750,56 +747,47 @@ public class TaskanaConfiguration {
|
|||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder allowTimestampServiceLevelMismatch(
|
||||
boolean validationAllowTimestampServiceLevelMismatch) {
|
||||
this.allowTimestampServiceLevelMismatch = validationAllowTimestampServiceLevelMismatch;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder addAdditionalUserInfo(boolean addAdditionalUserInfo) {
|
||||
this.addAdditionalUserInfo = addAdditionalUserInfo;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder priorityJobBatchSize(int priorityJobBatchSize) {
|
||||
this.priorityJobBatchSize = priorityJobBatchSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder priorityJobFirstRun(Instant priorityJobFirstRun) {
|
||||
this.priorityJobFirstRun = priorityJobFirstRun;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder priorityJobRunEvery(Duration priorityJobRunEvery) {
|
||||
this.priorityJobRunEvery = priorityJobRunEvery;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder priorityJobActive(boolean priorityJobActive) {
|
||||
this.priorityJobActive = priorityJobActive;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder userRefreshJobRunEvery(Duration userRefreshJobRunEvery) {
|
||||
this.userRefreshJobRunEvery = userRefreshJobRunEvery;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder userRefreshJobFirstRun(Instant userRefreshJobFirstRun) {
|
||||
this.userRefreshJobFirstRun = userRefreshJobFirstRun;
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public Builder minimalPermissionsToAssignDomains(
|
||||
List<WorkbasketPermission> minimalPermissionsToAssignDomains) {
|
||||
this.minimalPermissionsToAssignDomains = minimalPermissionsToAssignDomains;
|
||||
|
@ -865,6 +853,11 @@ public class TaskanaConfiguration {
|
|||
return this;
|
||||
}
|
||||
|
||||
public Builder workingTimeScheduleTimeZone(ZoneId workingTimeScheduleTimeZone) {
|
||||
this.workingTimeScheduleTimeZone = workingTimeScheduleTimeZone;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TaskanaConfiguration build() {
|
||||
validateConfiguration();
|
||||
return new TaskanaConfiguration(this);
|
||||
|
@ -1018,17 +1011,13 @@ public class TaskanaConfiguration {
|
|||
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))));
|
||||
workingTime.put(DayOfWeek.FRIDAY, standardWorkingSlots);
|
||||
return workingTime;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -132,7 +132,9 @@ public class TaskanaEngineImpl implements TaskanaEngine {
|
|||
taskanaConfiguration.getCustomHolidays());
|
||||
workingTimeCalculator =
|
||||
new WorkingTimeCalculatorImpl(
|
||||
holidaySchedule, taskanaConfiguration.getWorkingTimeSchedule());
|
||||
holidaySchedule,
|
||||
taskanaConfiguration.getWorkingTimeSchedule(),
|
||||
taskanaConfiguration.getWorkingTimeScheduleTimeZone());
|
||||
currentUserContext =
|
||||
new CurrentUserContextImpl(TaskanaConfiguration.shouldUseLowerCaseForAccessIds());
|
||||
createTransactionFactory(taskanaConfiguration.isUseManagedTransactions());
|
||||
|
|
|
@ -5,6 +5,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||
import java.time.DayOfWeek;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalTime;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
|
@ -40,7 +41,8 @@ class WorkingDaysToDaysReportConverterTest {
|
|||
workingTimeCalculator =
|
||||
new WorkingTimeCalculatorImpl(
|
||||
new HolidaySchedule(true, false, List.of(dayOfReformation, allSaintsDays)),
|
||||
workingTime);
|
||||
workingTime,
|
||||
ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -24,6 +24,7 @@ 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
|
||||
taskana.workingtime.timezone=UTC
|
||||
# enable or disable the jobscheduler at all
|
||||
# set it to false and no jobs are running
|
||||
taskana.jobscheduler.enabled=false
|
||||
|
|
Loading…
Reference in New Issue