schedulejs
Version:
Schedule tasks, meetings, appointments, etc
641 lines (640 loc) • 21.7 kB
JavaScript
if (typeof module !== "undefined" && module.exports) {
this.later = require("later");
}
schedule = function(later) {
"use strict";
var schedule = {
version: "0.6.3"
};
if (!later) throw new Error("Laterjs must be included before Schedulejs.");
if (!Array.isArray) {
Array.isArray = function(vArg) {
return Object.prototype.toString.call(vArg) === "[object Array]";
};
}
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function(searchElement) {
"use strict";
if (this == null) {
throw new TypeError();
}
var t = Object(this);
var len = t.length >>> 0;
if (len === 0) {
return -1;
}
var n = 0;
if (arguments.length > 1) {
n = Number(arguments[1]);
if (n != n) {
n = 0;
} else if (n != 0 && n != Infinity && n != -Infinity) {
n = (n > 0 || -1) * Math.floor(Math.abs(n));
}
}
if (n >= len) {
return -1;
}
var k = n >= 0 ? n : Math.max(len - Math.abs(n), 0);
for (;k < len; k++) {
if (k in t && t[k] === searchElement) {
return k;
}
}
return -1;
};
}
schedule.date = {};
schedule.date.UTC = function() {
later.date.UTC();
};
schedule.date.localTime = function() {
later.date.localTime();
};
schedule.functor = function(v) {
return typeof v === "function" ? v : function() {
return v;
};
};
schedule.memoizedRangeFn = function(fn) {
var cache = {};
return function(start) {
if (!cache[start]) {
var result = fn(1, start);
cache[start] = [ result[0] ? result[0].getTime() : 41024448e5, result[1] ? result[1].getTime() : 41024448e5 ];
}
return cache[start];
};
};
schedule.sort = {};
schedule.sort.tasks = function(taskGraph, readyTasks) {
readyTasks.sort(function(a, b) {
var ta = taskGraph.tasks[a], tb = taskGraph.tasks[b];
if (tb.priority && (!ta.priority || tb.priority > ta.priority)) {
return -1;
}
if (ta.priority && (!tb.priority || ta.priority > tb.priority)) {
return 1;
}
return taskGraph.tasks[b].floatAmt > taskGraph.tasks[a].floatAmt;
});
};
schedule.resources = function() {
var id = resourcesId, available = resourcesAvailable, isNotReservable = resourcesIsNotReservable;
function resources(data) {
var items = [], fid = schedule.functor(id), favailable = schedule.functor(available), freserve = schedule.functor(isNotReservable);
for (var i = 0, len = data.length; i < len; i++) {
var resource = data[i], rId = fid.call(this, resource, i), rAvailable = favailable.call(this, resource, i), rReserve = freserve.call(this, resource, i);
items.push({
id: rId,
available: rAvailable,
isNotReservable: rReserve
});
}
return items;
}
resources.id = function(_) {
if (!arguments.length) return id;
id = _;
return resources;
};
resources.available = function(_) {
if (!arguments.length) return available;
available = _;
return resources;
};
resources.isNotReservable = function(_) {
if (!arguments.length) return isNotReservable;
isNotReservable = _;
return resources;
};
return resources;
};
function resourcesId(d) {
return d.id;
}
function resourcesAvailable(d) {
return d.available;
}
function resourcesIsNotReservable(d) {
return d.isNotReservable || false;
}
schedule.tasks = function() {
var id = tasksId, duration = tasksDuration, available = tasksAvailable, resources = tasksResources, dependsOn = tasksDependsOn, minSchedule = tasksMinSchedule, priority = tasksPriority;
function tasks(data) {
var items = [], fid = schedule.functor(id), fduration = schedule.functor(duration), favailable = schedule.functor(available), fresources = schedule.functor(resources), fdependsOn = schedule.functor(dependsOn), fminschedule = schedule.functor(minSchedule), fpriority = schedule.functor(priority);
for (var i = 0, len = data.length; i < len; i++) {
var task = data[i], item = {
id: fid.call(this, task, i),
duration: fduration.call(this, task, i),
available: favailable.call(this, task, i),
resources: fresources.call(this, task, i),
dependsOn: fdependsOn.call(this, task, i),
minSchedule: fminschedule.call(this, task, i),
priority: fpriority.call(this, task, i)
};
items.push(item);
}
return items;
}
tasks.id = function(_) {
if (!arguments.length) return id;
id = _;
return tasks;
};
tasks.duration = function(_) {
if (!arguments.length) return duration;
duration = _;
return tasks;
};
tasks.available = function(_) {
if (!arguments.length) return available;
available = _;
return tasks;
};
tasks.resources = function(_) {
if (!arguments.length) return resources;
resources = _;
return tasks;
};
tasks.dependsOn = function(_) {
if (!arguments.length) return dependsOn;
dependsOn = _;
return tasks;
};
tasks.minSchedule = function(_) {
if (!arguments.length) return minSchedule;
minSchedule = _;
return tasks;
};
tasks.priority = function(_) {
if (!arguments.length) return priority;
priority = _;
return tasks;
};
return tasks;
};
function tasksId(d) {
return d.id;
}
function tasksDuration(d) {
return d.duration;
}
function tasksAvailable(d) {
return d.available;
}
function tasksResources(d) {
return d.resources;
}
function tasksDependsOn(d) {
return d.dependsOn;
}
function tasksMinSchedule(d) {
return d.minSchedule;
}
function tasksPriority(d) {
return d.priority;
}
schedule.dependencyGraph = function(taskArr) {
function createDependencyGraph(tasks) {
var graph = {
tasks: {},
roots: [],
leaves: [],
resources: [],
depth: 0,
end: 0
};
for (var i = 0, len = tasks.length; i < len; i++) {
var t = tasks[i];
graph.tasks[t.id] = {
id: t.id,
duration: t.duration,
priority: t.priority,
schedule: t.schedule,
minSchedule: t.minSchedule,
dependsOn: t.dependsOn,
resources: t.resources
};
}
setResources(graph);
setRequiredBy(graph.tasks);
setRootsAndLeaves(graph);
setDepth(graph, graph.leaves, 0);
graph.depth += 1;
forwardPass(graph, {}, graph.roots, 0);
setEnd(graph, graph.leaves);
backwardPass(graph, {}, graph.leaves, graph.end);
return graph;
}
function setResources(graph) {
for (var id in graph.tasks) {
var task = graph.tasks[id];
if (!isEmpty(task.resources)) {
for (var i = 0, len = task.resources.length; i < len; i++) {
var resId = task.resources[i];
if (graph.resources.indexOf(resId) === -1) {
graph.resources.push(resId);
}
}
}
}
}
function setRequiredBy(tasks) {
for (var id in tasks) {
var child = tasks[id], dependsOn = child.dependsOn;
if (!isEmpty(dependsOn)) {
for (var i = 0, len = dependsOn.length; i < len; i++) {
var parent = tasks[dependsOn[i]];
(parent.requiredBy || (parent.requiredBy = [])).push(child.id);
}
}
}
}
function setRootsAndLeaves(graph) {
for (var id in graph.tasks) {
var task = graph.tasks[id];
if (isEmpty(task.dependsOn)) {
graph.roots.push(task.id);
}
if (isEmpty(task.requiredBy)) {
graph.leaves.push(task.id);
}
}
}
function setDepth(graph, tasks, depth) {
for (var i = 0, len = tasks.length; i < len; i++) {
var task = graph.tasks[tasks[i]], dependsOn = task.dependsOn;
task.depth = !task.depth || depth > task.depth ? depth : task.depth;
graph.depth = depth > graph.depth ? depth : graph.depth;
if (!isEmpty(dependsOn)) {
setDepth(graph, dependsOn, task.depth + 1);
}
}
}
function forwardPass(graph, depEnds, tasks, start) {
updateDependencies(depEnds, tasks, start);
for (var i = 0, len = tasks.length; i < len; i++) {
var tid = tasks[i], task = graph.tasks[tid], dependsOn = task.dependsOn, dep = depEnds[tid];
if (!task.earlyFinish && (isEmpty(dependsOn) || dep && dep[0] === dependsOn.length)) {
task.earlyStart = dep[1];
task.earlyFinish = dep[1] + task.duration;
if (!isEmpty(task.requiredBy)) {
forwardPass(graph, depEnds, task.requiredBy, task.earlyFinish);
}
}
}
}
function setEnd(graph, tasks) {
for (var i = 0, len = tasks.length; i < len; i++) {
var finish = graph.tasks[tasks[i]].earlyFinish;
graph.end = finish > graph.end ? finish : graph.end;
}
}
function backwardPass(graph, depEnds, tasks, end) {
updateDependencies(depEnds, tasks, end, true);
for (var i = 0, len = tasks.length; i < len; i++) {
var tid = tasks[i], task = graph.tasks[tid], requiredBy = task.requiredBy, dep = depEnds[tid];
if (isEmpty(requiredBy) || dep && dep[0] === requiredBy.length) {
task.lateStart = dep[1] - task.duration;
task.lateFinish = dep[1];
task.floatAmt = task.lateFinish - task.earlyFinish;
if (!isEmpty(task.dependsOn)) {
backwardPass(graph, depEnds, task.dependsOn, task.lateStart);
}
}
}
}
function updateDependencies(deps, tasks, start, rev) {
var compare = rev ? function(a, b) {
return b > a;
} : function(a, b) {
return a > b;
};
for (var i = 0, len = tasks.length; i < len; i++) {
var id = tasks[i];
if (deps[id]) {
deps[id][0] = deps[id][0] + 1;
deps[id][1] = compare(start, deps[id][1]) ? start : deps[id][1];
} else {
deps[id] = [ 1, start ];
}
}
}
function isEmpty(arr) {
return !arr || arr.length === 0;
}
return createDependencyGraph(taskArr);
};
schedule.resourceManager = function(resourceDefinitions, startDate) {
var defaultSched = {
schedules: [ {
fd_a: [ startDate.getTime() ]
} ]
}, rMap = buildResourceMap(resourceDefinitions, startDate);
function buildResourceMap(resourceDefinitions, start) {
var map = {};
if (resourceDefinitions) {
for (var i = 0, len = resourceDefinitions.length; i < len; i++) {
addResourceToMap(map, resourceDefinitions[i], start);
}
}
return map;
}
function addResourceToMap(map, def, start) {
var sched = JSON.parse(JSON.stringify(def.available || defaultSched)), nextFn = schedule.memoizedRangeFn(later.schedule(sched).nextRange);
map[def.id] = {
schedule: sched,
next: nextFn,
nextAvail: nextFn(start)
};
}
function getReservation(resources, start, min, max) {
var reservation, schedules = [], delays = {}, maxTries = 50;
initRanges(resources, start, schedules, delays);
while (!(reservation = tryReservation(schedules, min, max)).success && --maxTries) {
updateRanges(schedules, nextValidStart(schedules), delays);
}
reservation.delays = delays;
return reservation;
}
function initRanges(resources, start, ranges, delays) {
for (var i = 0, len = resources.length; i < len; i++) {
var resId = resources[i];
if (Array.isArray(resId)) {
var subRanges = [], subDelays = {};
initRanges(resId, start, subRanges, subDelays);
var longDelay = getLongestDelay(subDelays);
if (longDelay) {
delays[longDelay] = subDelays[longDelay];
}
var schedule = {
subRanges: subRanges
};
setEarliestSubRange(schedule);
ranges.push(schedule);
} else {
var res = rMap[resId], range = res.nextAvail[0] >= start ? res.nextAvail : res.next(start);
if (range[0] > start && resId !== "_proj") {
delays[resId] = {
needed: start,
available: range[0]
};
}
ranges.push({
id: resId,
range: range
});
}
}
}
function tryReservation(schedules, min, max) {
var reservation = {
success: false
}, resources = [], start, end;
for (var i = 0, len = schedules.length; i < len; i++) {
var schedule = schedules[i], range = schedule.range;
if (!isInternal(schedule)) {
resources.push(schedule.id);
}
start = !start || range[0] > start ? range[0] : start;
end = !end || range[1] < end ? range[1] : end;
}
var duration = (end - start) / later.MIN;
if (duration >= min || duration >= max) {
duration = max && duration > max ? max : duration;
reservation = createReservation(resources, start, duration);
}
return reservation;
}
function createReservation(resources, start, duration) {
var end = start + duration * later.MIN, reservation = {
resources: resources,
start: start,
end: end,
duration: duration,
success: true
};
applyReservation(resources, start, end);
return reservation;
}
function updateRanges(resources, start, delays) {
for (var i = 0, len = resources.length; i < len; i++) {
var res = resources[i];
if (res.range[1] > start) continue;
if (res.subRanges) {
updateRanges(res.subRanges, start, {});
setEarliestSubRange(res);
} else {
res.range = rMap[res.id].next(start);
if (res.id !== "_proj" && !delays[res.id]) {
delays[res.id] = {
needed: start,
available: res.range[0]
};
}
}
}
}
function applyReservation(resources, start, end) {
for (var i = 0, len = resources.length; i < len; i++) {
var res = rMap[resources[i]];
if (res.isNotReservable) continue;
if (start !== res.nextAvail[0]) {
if (!res.schedule.exceptions) res.schedule.exceptions = [];
res.schedule.exceptions.push({
fd_a: [ start ],
fd_b: [ end ]
});
res.next = schedule.memoizedRangeFn(later.schedule(res.schedule).nextRange);
end = res.nextAvail[0];
}
res.nextAvail = res.next(end);
}
}
function nextValidStart(schedules) {
var latest;
for (var i = 0, len = schedules.length; i < len; i++) {
var end = schedules[i].range[1];
latest = !latest || end < latest ? end : latest;
}
return latest;
}
function setEarliestSubRange(schedule) {
var minId, minRange;
for (var i = 0, len = schedule.subRanges.length; i < len; i++) {
var sub = schedule.subRanges[i];
if (!minId || sub.range[0] < minRange[0]) {
minId = sub.id;
minRange = sub.range;
}
}
schedule.id = minId;
schedule.range = minRange;
}
function getLongestDelay(delays) {
var latest, lid;
for (var id in delays) {
var available = delays[id].available;
if (!latest || available < latest) {
latest = available;
lid = id;
}
}
return lid;
}
function isInternal(resource) {
return resource.id[0] === "_";
}
return {
getResource: function(id) {
return rMap[id];
},
addResource: function(arr, prefix, start) {
for (var i = 0, len = arr.length; i < len; i++) {
var def = typeof arr[i] !== "object" ? {
id: prefix + arr[i]
} : {
id: prefix + arr[i].id,
available: arr[i].available,
isNotReservable: arr[i].isNotReservable
};
if (!rMap[def.id]) {
addResourceToMap(rMap, def, start);
}
}
},
makeReservation: function(resources, start, min, max) {
start = start ? new Date(start) : new Date();
return getReservation(resources, start.getTime(), min || 1, max);
},
optimize: function(start) {
for (var id in rMap) {
var res = rMap[id];
if (res.schedule.exceptions) {
var curExceptions = res.schedule.exceptions;
res.schedule.exceptions = [];
for (var i = 0, len = curExceptions.length; i < len; i++) {
if (!curExceptions[i].fd_b || curExceptions[i].fd_b > start) {
res.schedule.exceptions.push(curExceptions[i]);
}
}
res.next = schedule.memoizedRangeFn(later.schedule(res.schedule).nextRange);
}
if (res.nextAvail[0] < start) {
res.nextAvail = res.next(start);
}
}
}
};
};
schedule.create = function(tasks, resources, sched, scheduleStart) {
if (!Array.isArray(tasks)) {
throw new Error("Tasks are required and must be passed in as an array.");
}
if (resources && !Array.isArray(resources)) {
throw new Error("Resources must be passed in as an array.");
}
var startDate = scheduleStart ? new Date(scheduleStart) : new Date();
if (!startDate || !startDate.getTime()) {
throw new Error("Invalid start date specified.");
}
var taskGraph = schedule.dependencyGraph(tasks), resMgr = schedule.resourceManager(resources, startDate), scheduledTasks = {};
function generateSchedule() {
var range, failedTasks = [];
resMgr.addResource(taskGraph.resources, "", startDate);
resMgr.addResource([ {
id: "_proj",
available: sched
} ], "", startDate);
resMgr.addResource(tasks, "_task", startDate);
forwardPass(taskGraph.roots);
range = getSummary(tasks, failedTasks);
backwardPass(taskGraph.leaves, range[1]);
return {
scheduledTasks: scheduledTasks,
failedTasks: failedTasks.length ? failedTasks : null,
success: failedTasks.length === 0,
start: range[0],
end: range[1]
};
}
function forwardPass(roots) {
var readyTasks = roots.slice(0), dependencies = {};
for (var i = 0, len = roots.length; i < len; i++) {
dependencies[roots[i]] = [ 0, startDate.getTime() ];
}
while (readyTasks.length) {
schedule.sort.tasks(taskGraph, readyTasks);
var task = taskGraph.tasks[readyTasks.pop()], start = dependencies[task.id][1], end = forwardPassTask(task, start);
if (end && task.requiredBy) {
updateDependencies(readyTasks, dependencies, task.requiredBy, end);
resMgr.optimize(getMinStart(dependencies));
}
}
}
function forwardPassTask(task, start) {
var resAll = [ "_proj", "_task" + task.id ], resources = task.resources ? resAll.concat(task.resources) : resAll, duration = task.duration, next = start, scheduledTask = {
schedule: [],
duration: task.duration
};
while (duration) {
var r = resMgr.makeReservation(resources, next, task.minSchedule || 1, duration);
if (!r.success) return undefined;
scheduledTask.earlyStart = scheduledTask.earlyStart || r.start;
scheduledTask.schedule.push(r);
duration -= r.duration;
next = r.end;
}
scheduledTask.earlyFinish = next;
scheduledTasks[task.id] = scheduledTask;
return next;
}
function getSummary(tasks, failedTasks) {
var start, end;
for (var i = 0, len = tasks.length; i < len; i++) {
var t = scheduledTasks[tasks[i].id];
if (t) {
start = !start || t.earlyStart < start ? t.earlyStart : start;
end = !end || t.earlyFinish > end ? t.earlyFinish : end;
} else {
failedTasks.push(tasks[i].id);
}
}
return [ start, end ];
}
function updateDependencies(readyTasks, dependencies, tasks, end) {
for (var i = 0, len = tasks.length; i < len; i++) {
var tid = tasks[i], dependsOn = taskGraph.tasks[tid].dependsOn, metDeps = dependencies[tid] || (dependencies[tid] = [ 0, 0 ]);
metDeps[0] += 1;
metDeps[1] = end > metDeps[1] ? end : metDeps[1];
if (!dependsOn || metDeps[0] >= dependsOn.length) {
readyTasks.push(tid);
}
}
}
function getMinStart(dependencies) {
var min;
for (var id in dependencies) {
if (!min || min > dependencies[id][1]) {
min = dependencies[id][1];
}
}
return min;
}
function backwardPass(tasks, finishDate) {
for (var i = 0, len = tasks.length; i < len; i++) {
var sTask = scheduledTasks[tasks[i]], dependsOn = taskGraph.tasks[tasks[i]].dependsOn;
if (sTask) {
sTask.lateFinish = finishDate;
sTask.floatAmt = (sTask.lateFinish - sTask.earlyFinish) / later.MIN;
if (dependsOn) {
backwardPass(dependsOn, sTask.earlyStart);
}
}
}
}
return generateSchedule();
};
return schedule;
}(this.later);