re.order
Version:
An extremely fast pathfinder tool/algorithm to determine optimal order execution sequence.
170 lines (152 loc) • 6.78 kB
JavaScript
;
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
var AS_MANY_AS_POSSIBLE = 'asmany';
var ALL_OR_NOTHING = 'allornothing';
var BASE_OPTIONS = {
mode: AS_MANY_AS_POSSIBLE,
startPosition: null,
startTime: null,
endTime: null,
loadCapacity: 0,
getDuration: function getDuration(source, destination, startTime) {
return 0;
}
};
/**
* Creates a reordering algorithm.
*
* @example
* const run = new createReOrder(options);
* run(orders);
*
* Options:
* - mode: 'asmany' or 'allornothing'. Complete as many as possible orders within given time frame, or "all or nothing"
* Default: allornothing.
* - startPosition: of the vehicle when starting the calculations. If not provided, supposes the vehicle is at first
* order's location.
* - startTime: Date object. When does the order chain start. If not provided = now.
* - endTime: Date object by what time latest should last order complete. Not provided = does not matter when.
* - loadCapacity: Integer maximal truck capacity. If not provided = unlimited, do not consider.
* - getDuration: NOT OPTIONAL. Given start and destination (as specified in the order and startPosition) and the time
* at the start position, return duration in seconds. You might need to use a routing table
* or callback to a routing backend to get that info.
*
* The getDuration is the only non-optional option. It accepts the following props (startTime is a Date object):
*
* getDuration(sourcePosition, destinationPosition, startTime) {
* return durationInSeconds;
* }
*
* Each order in the list of orders should have following structure:
* Order:
* - id: a unique order identifier
* - destination: an object that you use later as source/destination to calculate duration in getDuration
* - dependencies: (optional) array of order ids that have to be doe before this order
* - startLatest: (optional) maximal time that the order should be started by
* - finishLatest: (optional) maximal time that the order should be finished by
* - load: (optional) positive or negative value that changes the capacity of the vehicle
* - jobDuration: (optional) in seconds how long it takes to complete a job, once arrived
*
* @param options
* @returns {Function}
*/
function createReOrder(options) {
var opts = Object.assign({}, BASE_OPTIONS, options);
return function (orders) {
var possibleChains = [];
function walk() {
var chain = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
var currentTime = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : opts.startTime || new Date();
var currentLoad = getCurrentLoad(chain);
// Get orders not currently in chain and whose dependencies are completed
var nextOrders = orders.filter(function (order) {
return !chain.includes(order);
}).filter(function (order) {
return isWithinTimeframe(order, currentTime, opts.endTime);
}).filter(function (order) {
return canHandleLoad(order, currentLoad, opts.loadCapacity);
}).filter(function (order) {
return areDependenciesMet(order, chain);
});
if (!nextOrders.length) {
return possibleChains.push({
endTime: currentTime,
orders: chain
});
}
// Get start position for duration calculations
var startPosition = chain.length ? chain.slice(-1)[0].destination : opts.startPosition;
// Walk for each possible candidate order, adding it to the chain
var chainedOrders = nextOrders.filter(function (order) {
var newTime = startPosition ? addSeconds(currentTime, opts.getDuration(startPosition, order.destination, currentTime) + (order.jobDuration || 0)) : currentTime;
if (isWithinTimeframe(order, newTime, opts.endTime)) {
walk([].concat(_toConsumableArray(chain), [order]), newTime);
return order;
}
});
if (!chainedOrders.length) {
return possibleChains.push({
endTime: currentTime,
orders: chain
});
}
}
// Walk everything
walk();
var candidateChains = [];
if (opts.mode === AS_MANY_AS_POSSIBLE) {
// As many as possible
var maxOrdersCount = Math.max.apply(Math, _toConsumableArray(possibleChains.map(function (chain) {
return chain.orders.length;
})));
candidateChains = possibleChains.filter(function (chain) {
return chain.orders.length === maxOrdersCount;
});
} else {
// All or nothing
candidateChains = possibleChains.filter(function (chain) {
return chain.orders.length === orders.length;
});
}
return orderByDate(candidateChains, 'endTime')[0];
};
}
function getCurrentLoad(orders) {
return orders.reduce(function (total, order) {
return total + (order.load || 0);
}, 0);
}
function addSeconds(date, seconds) {
var d = new Date(date);
d.setSeconds(date.getSeconds() + seconds);
return d;
}
function canHandleLoad(order, currentCapacity, maxCapacity) {
return !maxCapacity || !order.load || currentCapacity + order.load <= maxCapacity;
}
function isWithinTimeframe(order, currentTime, endTime) {
var orderFinishTime = addSeconds(currentTime, order.jobDuration || 0);
return (!order.startLatest || order.startLatest >= currentTime) && (!order.finishLatest || order.finishLatest >= orderFinishTime) && (!endTime || endTime >= orderFinishTime);
}
function areDependenciesMet(order, completedOrders) {
if (!order.dependencies || !order.dependencies.length) {
return true;
}
var completedOrdersIds = completedOrders.map(function (order) {
return order.id;
});
return order.dependencies.filter(function (dependencyId) {
return !completedOrdersIds.includes(dependencyId);
}).length === 0;
}
function orderByDate(arr, dateProp) {
return arr.slice().sort(function (a, b) {
return a[dateProp] < b[dateProp] ? -1 : 1;
});
}
module.exports = {
BASE_OPTIONS: BASE_OPTIONS,
createReOrder: createReOrder,
AS_MANY_AS_POSSIBLE: AS_MANY_AS_POSSIBLE,
ALL_OR_NOTHING: ALL_OR_NOTHING
};