UNPKG

re.order

Version:

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

170 lines (152 loc) 6.78 kB
'use strict'; 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 };