TSK-1744: Algorithm that calculates work hours between two instants

TSK-1744: Algorithm that calculates work hours between two instants

TSK-1744: Algorithm that calculates work hours between two instants
This commit is contained in:
Lia Lissmann 2021-10-08 16:00:10 +02:00 committed by Mustapha Zorgati
parent 48ed6da956
commit 2bf90fd0fe
4 changed files with 416 additions and 4 deletions

View File

@ -0,0 +1,34 @@
package pro.taskana.common.api;
import java.time.LocalTime;
public class LocalTimeInterval {
private LocalTime begin;
private LocalTime end;
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;
}
}

View File

@ -0,0 +1,147 @@
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.HashMap;
import java.util.Map;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
public class WorkingTimeCalculator {
private static final Map<DayOfWeek, LocalTimeInterval> WORKING_TIME =
new HashMap<DayOfWeek, LocalTimeInterval>() {
{
put(DayOfWeek.MONDAY, new LocalTimeInterval(LocalTime.of(9, 0), LocalTime.of(17, 0)));
put(DayOfWeek.TUESDAY, new LocalTimeInterval(LocalTime.of(9, 0), LocalTime.of(17, 0)));
put(DayOfWeek.WEDNESDAY, new LocalTimeInterval(LocalTime.of(9, 0), LocalTime.of(17, 0)));
put(DayOfWeek.THURSDAY, new LocalTimeInterval(LocalTime.of(9, 0), LocalTime.of(17, 0)));
put(DayOfWeek.FRIDAY, new LocalTimeInterval(LocalTime.of(9, 0), LocalTime.of(17, 0)));
put(DayOfWeek.SATURDAY, new LocalTimeInterval(LocalTime.of(10, 0), LocalTime.of(15, 0)));
put(DayOfWeek.SUNDAY, null);
}
};
private final ZoneId zone;
private final WorkingDaysToDaysConverter converter;
public WorkingTimeCalculator(WorkingDaysToDaysConverter converter) {
this.converter = converter;
zone = ZoneId.of("UTC");
}
public Duration workingTimeBetweenTwoTimestamps(Instant from, Instant to)
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));
}
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();
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;
}
private Duration calculateDurationOfOneWorkDay(DayOfWeek weekday, LocalDate date) {
if (WORKING_TIME.get(weekday) != null && !converter.isHoliday(date)) {
return addWorkingHoursOfOneDay(weekday);
}
return Duration.ZERO;
}
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;
}
}
}

View File

@ -0,0 +1,215 @@
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 org.junit.jupiter.api.Test;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
public class WorkingTimeCalculatorTest {
private final WorkingTimeCalculator calculator;
WorkingTimeCalculatorTest() {
WorkingDaysToDaysConverter converter = new WorkingDaysToDaysConverter(true, false);
calculator = new WorkingTimeCalculator(converter);
}
@Test
void should_throwInvalidArgumentException_WhenFromTimeIsAfterUntilTime() {
Instant from = Instant.parse("2021-09-30T13:00:00.000Z");
Instant to = Instant.parse("2021-09-30T10:00:00.000Z");
assertThatThrownBy(() -> calculator.workingTimeBetweenTwoTimestamps(from, to))
.isInstanceOf(InvalidArgumentException.class)
.hasMessage("Instants are invalid.");
}
@Test
void should_throwInvalidArgumentException_WhenFromIsNull() {
Instant to = Instant.parse("2021-09-30T10:00:00.000Z");
assertThatThrownBy(() -> calculator.workingTimeBetweenTwoTimestamps(null, to))
.isInstanceOf(InvalidArgumentException.class)
.hasMessage("Instants are invalid.");
}
@Test
void should_ReturnWorkingTime_When_InstantsWithinSameHour() throws Exception {
Instant from = Instant.parse("2021-09-30T10:02:00.000Z");
Instant to = Instant.parse("2021-09-30T10:38:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(from, to);
assertThat(duration).isEqualTo(Duration.of(36, ChronoUnit.MINUTES));
}
@Test
void should_ReturnWorkingTime_When_InstantsWithinTheSameDay() throws Exception {
Instant thursdayMorning = Instant.parse("2021-09-30T10:00:00.000Z");
Instant thursdayEvening = Instant.parse("2021-09-30T15:00:00.000Z");
Duration duration =
calculator.workingTimeBetweenTwoTimestamps(thursdayMorning, thursdayEvening);
assertThat(duration).isEqualTo(Duration.of(5, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_InstantsWithinTheSameDayStartBeforeHours() throws Exception {
Instant thursdayMorning = Instant.parse("2021-09-30T08:00:00.000Z");
Instant thursdayEvening = Instant.parse("2021-09-30T15:00:00.000Z");
Duration duration =
calculator.workingTimeBetweenTwoTimestamps(thursdayMorning, thursdayEvening);
assertThat(duration).isEqualTo(Duration.of(6, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_InstantsWithinTheSameDayEndAfterHours() throws Exception {
Instant thursdayMorning = Instant.parse("2021-09-30T10:00:00.000Z");
Instant thursdayEvening = Instant.parse("2021-09-30T20:00:00.000Z");
Duration duration =
calculator.workingTimeBetweenTwoTimestamps(thursdayMorning, thursdayEvening);
assertThat(duration).isEqualTo(Duration.of(7, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_InstantsWithinTheSameDayStartAndEndAfterHours()
throws Exception {
Instant thursdayMorning = Instant.parse("2021-09-30T19:00:00.000Z");
Instant thursdayEvening = Instant.parse("2021-09-30T20:00:00.000Z");
Duration duration =
calculator.workingTimeBetweenTwoTimestamps(thursdayMorning, thursdayEvening);
assertThat(duration).isEqualTo(Duration.of(0, ChronoUnit.MINUTES));
}
@Test
void should_ReturnWorkingTime_When_InstantsWithinTheSameDayStartAndEndBeforeHours()
throws Exception {
Instant thursdayMorning = Instant.parse("2021-09-30T03:00:00.000Z");
Instant thursdayEvening = Instant.parse("2021-09-30T04:00:00.000Z");
Duration duration =
calculator.workingTimeBetweenTwoTimestamps(thursdayMorning, thursdayEvening);
assertThat(duration).isEqualTo(Duration.of(0, ChronoUnit.MINUTES));
}
@Test
void should_ReturnWorkingTime_When_InstantsSameTime() throws Exception {
Instant thursdayMorning = Instant.parse("2021-09-30T15:00:00.000Z");
Instant thursdayEvening = Instant.parse("2021-09-30T15:00:00.000Z");
Duration duration =
calculator.workingTimeBetweenTwoTimestamps(thursdayMorning, thursdayEvening);
assertThat(duration).isEqualTo(Duration.of(0, ChronoUnit.MILLIS));
}
@Test
void should_ReturnWorkingTime_When_InstantsWithinTheSameWeek() throws Exception {
Instant fromMonday = Instant.parse("2021-09-27T10:00:00.000Z");
Instant toSaturday = Instant.parse("2021-10-02T15:00:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(fromMonday, toSaturday);
assertThat(duration).isEqualTo(Duration.of(44, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_UntilInstantIsBeforeWorkingHours() throws Exception {
Instant thursday = Instant.parse("2021-09-30T10:00:00.000Z");
Instant friday = Instant.parse("2021-10-01T06:00:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(thursday, friday);
assertThat(duration).isEqualTo(Duration.of(7, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_UntilInstantIsAfterWorkingHours() throws Exception {
Instant thursday = Instant.parse("2021-09-30T10:00:00.000Z");
Instant friday = Instant.parse("2021-10-01T19:00:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(thursday, friday);
assertThat(duration).isEqualTo(Duration.of(15, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_FromInstantIsBeforeWorkingHours() throws Exception {
Instant thursday = Instant.parse("2021-09-30T06:00:00.000Z");
Instant friday = Instant.parse("2021-10-01T10:00:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(thursday, friday);
assertThat(duration).isEqualTo(Duration.of(9, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_FromInstantIsAfterWorkingHours() throws Exception {
Instant thursday = Instant.parse("2021-09-30T19:00:00.000Z");
Instant friday = Instant.parse("2021-10-01T10:00:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(thursday, friday);
assertThat(duration).isEqualTo(Duration.of(1, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_InstantsSeparatedByWeekend() throws Exception {
Instant fromFriday = Instant.parse("2021-09-24T15:00:00.000Z");
Instant toMonday = Instant.parse("2021-09-27T10:00:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(fromFriday, toMonday);
assertThat(duration).isEqualTo(Duration.of(8, ChronoUnit.HOURS));
}
@Test
void should_ReturnWorkingTime_When_InstantsSeparatedByNewYearHoliday() throws Exception {
Instant fromThursday = Instant.parse("2020-12-31T14:00:00.000Z");
Instant toSaturday = Instant.parse("2021-01-02T11:00:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(fromThursday, toSaturday);
assertThat(duration).isEqualTo(Duration.of(4, ChronoUnit.HOURS));
}
@Test
void should_ReturnZeroAsTime_WhenInstantsWithinSameHoliday() throws Exception {
Instant fridayFrom = Instant.parse("2021-01-01T11:00:00.000Z");
Instant fridayTo = Instant.parse("2021-01-01T14:00:00.000Z");
Duration duration = calculator.workingTimeBetweenTwoTimestamps(fridayFrom, fridayTo);
assertThat(duration).isEqualTo(Duration.of(0, ChronoUnit.HOURS));
}
@Test
void should_ReturMultipleWorkingTimes_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(44, ChronoUnit.HOURS));
}
}

View File

@ -4,6 +4,9 @@ import java.time.Duration;
import java.time.Instant;
import java.util.OptionalInt;
import pro.taskana.common.api.WorkingDaysToDaysConverter;
import pro.taskana.common.api.WorkingTimeCalculator;
import pro.taskana.common.api.exceptions.InvalidArgumentException;
import pro.taskana.spi.priority.api.PriorityServiceProvider;
import pro.taskana.task.api.TaskCustomField;
import pro.taskana.task.api.models.TaskSummary;
@ -13,12 +16,25 @@ public class TestPriorityServiceProvider implements PriorityServiceProvider {
@Override
public OptionalInt calculatePriority(TaskSummary taskSummary) {
long diffInDays = Duration.between(taskSummary.getCreated(), Instant.now()).toDays();
int priority = diffInDays >= 1 ? Math.toIntExact(diffInDays) : 1;
WorkingDaysToDaysConverter converter = new WorkingDaysToDaysConverter(true, true);
WorkingTimeCalculator calculator = new WorkingTimeCalculator(converter);
int priority;
try {
priority =
Math.toIntExact(
calculator
.workingTimeBetweenTwoTimestamps(taskSummary.getCreated(), Instant.now())
.toMinutes())
+ 1;
} catch (InvalidArgumentException | ArithmeticException e) {
long diffInDays = Duration.between(taskSummary.getCreated(), Instant.now()).toDays();
priority = diffInDays >= 1 ? Math.toIntExact(diffInDays) : 1;
if ("true".equals(taskSummary.getCustomAttribute(TaskCustomField.CUSTOM_6))) {
priority *= MULTIPLIER;
if ("true".equals(taskSummary.getCustomAttribute(TaskCustomField.CUSTOM_6))) {
priority *= MULTIPLIER;
}
}
return OptionalInt.of(priority);
}
}