re.order
Version:
An extremely fast pathfinder tool/algorithm to determine optimal order execution sequence.
154 lines (137 loc) • 5.93 kB
JavaScript
const AS_MANY_AS_POSSIBLE = 'asmany';
const ALL_OR_NOTHING = 'allornothing';
const BASE_OPTIONS = {
mode: AS_MANY_AS_POSSIBLE,
startPosition: null,
startTime: null,
endTime: null,
loadCapacity: 0,
getDuration: (source, destination, startTime) => 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) {
const opts = Object.assign({}, BASE_OPTIONS, options);
return function (orders) {
const possibleChains = [];
function walk (chain = [], currentTime = opts.startTime || new Date()) {
const currentLoad = getCurrentLoad(chain);
// Get orders not currently in chain and whose dependencies are completed
const nextOrders = orders
.filter(order => !chain.includes(order))
.filter(order => isWithinTimeframe(order, currentTime, opts.endTime))
.filter(order => canHandleLoad(order, currentLoad, opts.loadCapacity))
.filter(order => areDependenciesMet(order, chain));
if (!nextOrders.length) {
return possibleChains.push({
endTime: currentTime,
orders: chain
})
}
// Get start position for duration calculations
const startPosition = chain.length ? chain.slice(-1)[0].destination : opts.startPosition;
// Walk for each possible candidate order, adding it to the chain
const chainedOrders = nextOrders.filter(order => {
const newTime = startPosition
? addSeconds(
currentTime,
opts.getDuration(startPosition, order.destination, currentTime) + (order.jobDuration || 0)
)
: currentTime;
if (isWithinTimeframe(order, newTime, opts.endTime)) {
walk([ ...chain, order ], newTime);
return order
}
});
if (!chainedOrders.length) {
return possibleChains.push({
endTime: currentTime,
orders: chain
})
}
}
// Walk everything
walk();
let candidateChains = [];
if (opts.mode === AS_MANY_AS_POSSIBLE) {
// As many as possible
const maxOrdersCount = Math.max(...possibleChains.map((chain) => chain.orders.length));
candidateChains = possibleChains.filter((chain) => chain.orders.length === maxOrdersCount);
} else {
// All or nothing
candidateChains = possibleChains.filter((chain) => chain.orders.length === orders.length);
}
return orderByDate(candidateChains, 'endTime')[0];
}
}
function getCurrentLoad (orders) {
return orders.reduce((total, order) => total + (order.load || 0), 0)
}
function addSeconds (date, seconds) {
const 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) {
const 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;
}
const completedOrdersIds = completedOrders.map(order => order.id);
return order.dependencies.filter(dependencyId => !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,
createReOrder,
AS_MANY_AS_POSSIBLE,
ALL_OR_NOTHING
};