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:
parent
48ed6da956
commit
2bf90fd0fe
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue