UNPKG

re.order

Version:

An extremely fast pathfinder tool/algorithm to determine optimal order execution sequence.

154 lines (137 loc) 5.93 kB
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 };