/* * Copyright [2014] [Christian Loehnert, krampenschiesser@gmail.com] * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package de.ks.idnadrev.task.choosenext; import de.ks.idnadrev.entity.Context; import de.ks.idnadrev.entity.Task; import de.ks.idnadrev.entity.TaskState; import de.ks.option.Options; import de.ks.persistence.PersistentWork; import de.ks.reflection.PropertyPath; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.persistence.criteria.Join; import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import java.time.Duration; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.stream.Collectors; /** * Chooses the next best task according to the following rules: * * . extimated time is less than given parameter * . context matches * . task is not delegated * . task is not masked as later * . task is not finshed * * The resulting tasks are then sorted by comparison. * The task with the highest priority is returned. * The priority is calculated as follows: * * . each day will add a priority of 10 ( so one week ago, prio=10) * . if the task is marked as asap a prio of 100 will be added * . the prio for estimated time vs. the max time is calculated with the following formula: * +++ * $$prio = ((maxTime - estimatedTime) * 10) / (maxTime)$$ * +++ * it is only calculated when the estimated time is larger than the NextTaskChooserOptions.timeThreshold * * The 3 calulated priorities (age,asap,estimatedTime) are then multiplied by the factors given in NextTaskChooserOptions * * If the setting NextTaskChooserOptions.completlyRandom() is set, everything is bypassed. */ public class NextTaskChooser { private static final Logger log = LoggerFactory.getLogger(NextTaskChooser.class); static final String KEY_ESTIMATED_DURATION = PropertyPath.property(Task.class, t -> t.getEstimatedTime()); static final String KEY_STATE = PropertyPath.property(Task.class, t -> t.getState()); static final String KEY_CONTEXT = PropertyPath.property(Task.class, t -> t.getContext()); static final String KEY_CONTEXT_NAME = PropertyPath.property(Context.class, t -> t.getName()); static final String KEY_FINISHTIME = PropertyPath.property(Task.class, t -> t.getFinishTime()); protected NextTaskChooserOptions options; public NextTaskChooser() { options = Options.get(NextTaskChooserOptions.class); } public List<Task> getTasksSorted(int minutes, String selectedContext) { List<Task> allPossibleTasks = getAllPossibleTasks(minutes, selectedContext); if (allPossibleTasks.isEmpty()) { log.info("No tasks to choose from."); return Collections.emptyList(); } else { List<Task> tasks = sortTasksByPrio(minutes, allPossibleTasks); return tasks; } } private String tasks2NameString(List<Task> tasks) { return tasks.stream().map(t -> t.getName()).reduce("", (o1, o2) -> o1 + ", " + o2); } protected List<Task> sortTasksByPrio(int maxTime, List<Task> allPossibleTasks) { log.debug("Got {} tasks to choose from: {}", allPossibleTasks.size(), tasks2NameString(allPossibleTasks)); allPossibleTasks = new ArrayList<>(allPossibleTasks); Collections.sort(allPossibleTasks, Comparator.comparing(task -> { long ageInDays = Duration.between(task.getCreationTime(), LocalDateTime.now()).toDays(); long agePrio = ageInDays * 10; long asapPrio = task.getState() == TaskState.ASAP ? 100 : 0; long estimatedMinutes = task.getEstimatedTime().toMinutes(); long matchingTimePrio; if (estimatedMinutes > options.getTimeThreshold()) { matchingTimePrio = (long) ((maxTime - estimatedMinutes) * 10D / maxTime); } else { matchingTimePrio = 0; } agePrio *= options.getAgeFactor(); asapPrio *= options.getAsapFactor(); matchingTimePrio *= options.getTimeFactor(); long totalPrio = agePrio + asapPrio + matchingTimePrio; log.debug("Task {} has prio {}", task.getName(), totalPrio); return totalPrio; })); Collections.reverse(allPossibleTasks); log.debug("Sorted tasks to the following order: {}", tasks2NameString(allPossibleTasks)); return allPossibleTasks; } protected List<Task> getAllPossibleTasks(int minutes, String selectedContext) { List<Task> retval = PersistentWork.wrap(() -> { List<Task> tasks = PersistentWork.from(Task.class, (root, query, builder) -> { ArrayList<Predicate> predicates = new ArrayList<>(); if (selectedContext != null) { Join<Task, Context> join = root.join(KEY_CONTEXT); join.on(builder.equal(join.get(KEY_CONTEXT_NAME), selectedContext)); } else { predicates.add(builder.isNull(root.get(KEY_CONTEXT))); } Path<Object> state = root.get(KEY_STATE); predicates.add(builder.notEqual(state, TaskState.LATER)); predicates.add(builder.notEqual(state, TaskState.DELEGATED)); predicates.add(builder.isNull(root.get(KEY_FINISHTIME))); query.where(predicates.toArray(new Predicate[predicates.size()])); }, null); return tasks.stream()//super ugly, need to evict to save heap .sorted(Comparator.comparing(c -> c.getEstimatedTime().toMinutes() - c.getSpentMinutes()))// .filter(t -> { long timeRemaining = t.getRemainingTime().toMinutes(); log.info("Remaining time: {}", timeRemaining); return timeRemaining < minutes && timeRemaining > 2; })// .collect(Collectors.toList()); }); return retval; } }