mirror of
https://github.com/morten-olsen/bob-the-algorithm.git
synced 2026-02-08 00:46:25 +01:00
init
This commit is contained in:
144
src/features/planner/algorithm/build-graph.ts
Normal file
144
src/features/planner/algorithm/build-graph.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
import { Context, GraphNode } from "#/types/graph";
|
||||
import { UserLocation } from "#/types/location";
|
||||
import { Task } from "#/types/task";
|
||||
import { getNext } from "./get-next";
|
||||
|
||||
enum Strategies {
|
||||
all = 'all',
|
||||
allValid = 'all-valid',
|
||||
firstValid = 'first-valid',
|
||||
firstComplet = 'first-complete',
|
||||
}
|
||||
type RunningStatus = {
|
||||
current: 'running';
|
||||
nodes: number;
|
||||
start: Date;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
type CompletedStatus = {
|
||||
current: 'completed';
|
||||
start: Date;
|
||||
end: Date;
|
||||
nodes: number;
|
||||
}
|
||||
|
||||
type Status = RunningStatus | CompletedStatus;
|
||||
|
||||
type BuildGraphOptions = {
|
||||
location: UserLocation;
|
||||
time: Date;
|
||||
tasks: Task[];
|
||||
context: Context;
|
||||
strategy?: Strategies;
|
||||
batchSize?: number;
|
||||
sleepTime?: number;
|
||||
callback?: (status: Status) => void;
|
||||
};
|
||||
|
||||
const sleep = (time: number) => new Promise(resolve => setTimeout(resolve, time));
|
||||
|
||||
const fil = <T>(
|
||||
fn: ((item: T) => boolean)[],
|
||||
input: T[],
|
||||
): T[][] => {
|
||||
const output: T[][] = new Array(fn.length).fill(undefined).map(() => []);
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
for (let b = 0; b < fn.length; b++) {
|
||||
if (fn[b](input[i])) {
|
||||
output[b].push(input[i]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const buildGraph = async ({
|
||||
location,
|
||||
time,
|
||||
tasks,
|
||||
context,
|
||||
strategy = Strategies.allValid,
|
||||
callback,
|
||||
batchSize = 1000,
|
||||
sleepTime = 10,
|
||||
}: BuildGraphOptions) => {
|
||||
const start = new Date();
|
||||
let leafs: GraphNode[] = [{
|
||||
location,
|
||||
time: {
|
||||
end: time,
|
||||
start: time,
|
||||
},
|
||||
score: 0,
|
||||
remainingTasks: tasks,
|
||||
impossibeTasks: [],
|
||||
status: {
|
||||
dead: false,
|
||||
completed: false,
|
||||
},
|
||||
}];
|
||||
let nodes = 0;
|
||||
let running = true;
|
||||
const final: GraphNode[] = [];
|
||||
|
||||
while (true) {
|
||||
nodes++;
|
||||
if (!running) {
|
||||
return [];
|
||||
}
|
||||
const node = leafs.pop();
|
||||
if (!node) {
|
||||
break;
|
||||
}
|
||||
if (nodes % batchSize === 1) {
|
||||
if (callback) {
|
||||
callback({
|
||||
current: 'running',
|
||||
nodes,
|
||||
start,
|
||||
cancel: () => {
|
||||
running = false;
|
||||
}
|
||||
})
|
||||
}
|
||||
await sleep(sleepTime);
|
||||
}
|
||||
const next = await getNext(node, context);
|
||||
const [alive, completed] = fil([
|
||||
n => !n.status.dead && !n.status.completed,
|
||||
n => !!n.status.completed && !n.status.dead
|
||||
], next);
|
||||
leafs.push(...alive);
|
||||
if (strategy === Strategies.firstValid && completed.length > 0) {
|
||||
if (callback) {
|
||||
callback({ current: 'completed', nodes, start, end: new Date() })
|
||||
}
|
||||
return completed;
|
||||
}
|
||||
if (completed.length > 0) {
|
||||
final.push(...completed)
|
||||
}
|
||||
if (strategy === Strategies.firstComplet) {
|
||||
const fullComplete = completed.find(c => c.impossibeTasks.length === 0);
|
||||
if (fullComplete) {
|
||||
if (callback) {
|
||||
callback({ current: 'completed', nodes, start, end: new Date() })
|
||||
}
|
||||
return [fullComplete];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('nodes', nodes);
|
||||
if (callback) {
|
||||
callback({ current: 'completed', nodes, start, end: new Date() })
|
||||
}
|
||||
return final
|
||||
.filter(n => n.status.completed)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
}
|
||||
|
||||
export type { Status, BuildGraphOptions };
|
||||
export { buildGraph, Strategies };
|
||||
40
src/features/planner/algorithm/construct-day.ts
Normal file
40
src/features/planner/algorithm/construct-day.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { GraphNode } from "#/types/graph";
|
||||
import { PlanItem } from "#/types/plans";
|
||||
|
||||
const constructDay = (node: GraphNode) => {
|
||||
let current: GraphNode | undefined = node;
|
||||
const plans: PlanItem[] = [];
|
||||
|
||||
while(current) {
|
||||
if (current.task) {
|
||||
plans.push({
|
||||
type: 'task',
|
||||
name: current.task?.name || 'start',
|
||||
external: current.task?.external,
|
||||
start: new Date(
|
||||
current.time.start.getTime()
|
||||
+ (current.transition?.time || 0),
|
||||
),
|
||||
end: current.time.end,
|
||||
score: current.score,
|
||||
})
|
||||
}
|
||||
if (current.transition) {
|
||||
plans.push({
|
||||
type: 'transition',
|
||||
start: current.time.start,
|
||||
end: new Date(
|
||||
current.time.start.getTime()
|
||||
+ current.transition.time,
|
||||
),
|
||||
from: current.transition.from,
|
||||
to: current.transition.to,
|
||||
})
|
||||
}
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
return plans.reverse();
|
||||
}
|
||||
|
||||
export { constructDay };
|
||||
146
src/features/planner/algorithm/get-next.ts
Normal file
146
src/features/planner/algorithm/get-next.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { GraphNode, Context } from '#/types/graph';
|
||||
import { Transition } from '#/types/location';
|
||||
import { Task } from '#/types/task';
|
||||
import { getRemainingLocations, listContainLocation } from './utils';
|
||||
|
||||
const isDead = (impossible: Task[]) => {
|
||||
const missingRequered = impossible.find(t => t.required);
|
||||
return !!missingRequered;
|
||||
}
|
||||
|
||||
type GetImpossibleResult = {
|
||||
remaining: Task[];
|
||||
impossible: Task[];
|
||||
}
|
||||
|
||||
const getImpossible = (
|
||||
tasks: Task[],
|
||||
time: Date,
|
||||
) => {
|
||||
const result: GetImpossibleResult = {
|
||||
remaining: [],
|
||||
impossible: [],
|
||||
}
|
||||
|
||||
for (let task of tasks) {
|
||||
if (time > task.start.max) {
|
||||
result.impossible.push(task);
|
||||
} else {
|
||||
result.remaining.push(task);
|
||||
}
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
type CalculateScoreOptions = {
|
||||
tasks?: Task[];
|
||||
transition?: Transition;
|
||||
impossible: Task[];
|
||||
}
|
||||
|
||||
const calculateScore = ({
|
||||
tasks,
|
||||
transition,
|
||||
impossible,
|
||||
}: CalculateScoreOptions) => {
|
||||
let score = 0;
|
||||
|
||||
tasks?.forEach((task) => {
|
||||
score += task.priority * 10;
|
||||
impossible.forEach((task) => {
|
||||
if (task.required) {
|
||||
score -= 1000;
|
||||
} else {
|
||||
score -= task.priority;
|
||||
}
|
||||
});
|
||||
});
|
||||
if (transition) {
|
||||
const minutes = transition.time / 1000 / 60
|
||||
score -= minutes;
|
||||
}
|
||||
return score;
|
||||
}
|
||||
const getNext = async (
|
||||
currentNode: GraphNode,
|
||||
context: Context,
|
||||
): Promise<GraphNode[]> => {
|
||||
const nextNodes: GraphNode[] = [];
|
||||
if (!currentNode.transition) {
|
||||
const remainingLocations = getRemainingLocations(currentNode.remainingTasks, currentNode.location);
|
||||
await Promise.all(remainingLocations.map(async(location) => {
|
||||
const transition = await context.getTransition(currentNode.location, location, currentNode.time.end);
|
||||
const endTime = new Date(currentNode.time.end.getTime() + transition.time);
|
||||
const { remaining, impossible } = getImpossible(currentNode.remainingTasks, endTime);
|
||||
const score = calculateScore({
|
||||
transition,
|
||||
impossible,
|
||||
});
|
||||
nextNodes.push({
|
||||
parent: currentNode,
|
||||
location: transition.to,
|
||||
remainingTasks: remaining,
|
||||
transition,
|
||||
impossibeTasks: [
|
||||
...impossible,
|
||||
...currentNode.impossibeTasks,
|
||||
],
|
||||
score: currentNode.score + score,
|
||||
status: {
|
||||
completed: false,
|
||||
dead: isDead(impossible),
|
||||
},
|
||||
time: {
|
||||
start: currentNode.time.end,
|
||||
end: endTime,
|
||||
},
|
||||
})
|
||||
}));
|
||||
}
|
||||
const possibleTasks = currentNode.remainingTasks.filter(task => !task.locations || listContainLocation(task.locations, currentNode.location))
|
||||
await Promise.all(possibleTasks.map(async (orgTask) => {
|
||||
const task = {...orgTask};
|
||||
task.count = (task.count || 1) - 1
|
||||
let startTime = new Date(
|
||||
Math.max(
|
||||
currentNode.time.end.getTime(),
|
||||
task.start.min.getTime(),
|
||||
),
|
||||
);
|
||||
const parentRemainging = currentNode.remainingTasks.filter(t => t !== orgTask);
|
||||
let endTime = new Date(startTime.getTime() + task.duration.min);
|
||||
const { remaining, impossible } = getImpossible(
|
||||
task.count > 0
|
||||
? [...parentRemainging, task]
|
||||
: parentRemainging,
|
||||
endTime,
|
||||
);
|
||||
const score = calculateScore({
|
||||
tasks: [task],
|
||||
impossible,
|
||||
});
|
||||
nextNodes.push({
|
||||
parent: currentNode,
|
||||
location: currentNode.location,
|
||||
task,
|
||||
remainingTasks: remaining,
|
||||
impossibeTasks: [
|
||||
...impossible,
|
||||
...currentNode.impossibeTasks,
|
||||
],
|
||||
score: currentNode.score + score,
|
||||
status: {
|
||||
completed: remaining.length === 0,
|
||||
dead: isDead(impossible),
|
||||
},
|
||||
time: {
|
||||
start: startTime,
|
||||
end: endTime,
|
||||
},
|
||||
})
|
||||
}));
|
||||
return nextNodes;
|
||||
};
|
||||
|
||||
export { getNext };
|
||||
38
src/features/planner/algorithm/utils.ts
Normal file
38
src/features/planner/algorithm/utils.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { UserLocation } from "#/types/location";
|
||||
import { Task } from "#/types/task";
|
||||
|
||||
export const locationEqual = (a: UserLocation, b: UserLocation) => {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
if (a.location === b.location) {
|
||||
return true;
|
||||
}
|
||||
if (a.location && b.location && a.location.latitude === b.location.latitude && a.location.longitute === b.location.longitute) {
|
||||
return true;
|
||||
}
|
||||
if (a.title === b.title) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export const listContainLocation = (list: UserLocation[], target: UserLocation) => {
|
||||
return !!list.find(l => locationEqual(l, target));
|
||||
}
|
||||
|
||||
export const getRemainingLocations = (tasks: Task[], current: UserLocation) => {
|
||||
const result: UserLocation[] = [];
|
||||
tasks.forEach((task) => {
|
||||
if (!task.locations) {
|
||||
return;
|
||||
}
|
||||
for (let location of task.locations) {
|
||||
if (!listContainLocation(result, location) && !locationEqual(current, location)) {
|
||||
result.push(location)
|
||||
}
|
||||
}
|
||||
})
|
||||
return result;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user