| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 | /*	MIT License http://www.opensource.org/licenses/mit-license.php	Author Tobias Koppers @sokra*/"use strict";// Simulations show these probabilities for a single change// 93.1% that one group is invalidated// 4.8% that two groups are invalidated// 1.1% that 3 groups are invalidated// 0.1% that 4 or more groups are invalidated//// And these for removing/adding 10 lexically adjacent files// 64.5% that one group is invalidated// 24.8% that two groups are invalidated// 7.8% that 3 groups are invalidated// 2.7% that 4 or more groups are invalidated//// And these for removing/adding 3 random files// 0% that one group is invalidated// 3.7% that two groups are invalidated// 80.8% that 3 groups are invalidated// 12.3% that 4 groups are invalidated// 3.2% that 5 or more groups are invalidated/** * @param {string} a key * @param {string} b key * @returns {number} the similarity as number */const similarity = (a, b) => {	const l = Math.min(a.length, b.length);	let dist = 0;	for (let i = 0; i < l; i++) {		const ca = a.charCodeAt(i);		const cb = b.charCodeAt(i);		dist += Math.max(0, 10 - Math.abs(ca - cb));	}	return dist;};/** * @param {string} a key * @param {string} b key * @param {Set<string>} usedNames set of already used names * @returns {string} the common part and a single char for the difference */const getName = (a, b, usedNames) => {	const l = Math.min(a.length, b.length);	let i = 0;	while (i < l) {		if (a.charCodeAt(i) !== b.charCodeAt(i)) {			i++;			break;		}		i++;	}	while (i < l) {		const name = a.slice(0, i);		const lowerName = name.toLowerCase();		if (!usedNames.has(lowerName)) {			usedNames.add(lowerName);			return name;		}		i++;	}	// names always contain a hash, so this is always unique	// we don't need to check usedNames nor add it	return a;};/** * @param {Record<string, number>} total total size * @param {Record<string, number>} size single size * @returns {void} */const addSizeTo = (total, size) => {	for (const key of Object.keys(size)) {		total[key] = (total[key] || 0) + size[key];	}};/** * @param {Record<string, number>} total total size * @param {Record<string, number>} size single size * @returns {void} */const subtractSizeFrom = (total, size) => {	for (const key of Object.keys(size)) {		total[key] -= size[key];	}};/** * @template T * @param {Iterable<Node<T>>} nodes some nodes * @returns {Record<string, number>} total size */const sumSize = nodes => {	const sum = Object.create(null);	for (const node of nodes) {		addSizeTo(sum, node.size);	}	return sum;};/** * @param {Record<string, number>} size size * @param {Record<string, number>} maxSize minimum size * @returns {boolean} true, when size is too big */const isTooBig = (size, maxSize) => {	for (const key of Object.keys(size)) {		const s = size[key];		if (s === 0) continue;		const maxSizeValue = maxSize[key];		if (typeof maxSizeValue === "number" && s > maxSizeValue) return true;	}	return false;};/** * @param {Record<string, number>} size size * @param {Record<string, number>} minSize minimum size * @returns {boolean} true, when size is too small */const isTooSmall = (size, minSize) => {	for (const key of Object.keys(size)) {		const s = size[key];		if (s === 0) continue;		const minSizeValue = minSize[key];		if (typeof minSizeValue === "number" && s < minSizeValue) return true;	}	return false;};/** * @param {Record<string, number>} size size * @param {Record<string, number>} minSize minimum size * @returns {Set<string>} set of types that are too small */const getTooSmallTypes = (size, minSize) => {	const types = new Set();	for (const key of Object.keys(size)) {		const s = size[key];		if (s === 0) continue;		const minSizeValue = minSize[key];		if (typeof minSizeValue === "number" && s < minSizeValue) types.add(key);	}	return types;};/** * @template T * @param {TODO} size size * @param {Set<string>} types types * @returns {number} number of matching size types */const getNumberOfMatchingSizeTypes = (size, types) => {	let i = 0;	for (const key of Object.keys(size)) {		if (size[key] !== 0 && types.has(key)) i++;	}	return i;};/** * @param {Record<string, number>} size size * @param {Set<string>} types types * @returns {number} selective size sum */const selectiveSizeSum = (size, types) => {	let sum = 0;	for (const key of Object.keys(size)) {		if (size[key] !== 0 && types.has(key)) sum += size[key];	}	return sum;};/** * @template T */class Node {	/**	 * @param {T} item item	 * @param {string} key key	 * @param {Record<string, number>} size size	 */	constructor(item, key, size) {		this.item = item;		this.key = key;		this.size = size;	}}/** * @template T */class Group {	/**	 * @param {Node<T>[]} nodes nodes	 * @param {number[] | null} similarities similarities between the nodes (length = nodes.length - 1)	 * @param {Record<string, number>=} size size of the group	 */	constructor(nodes, similarities, size) {		this.nodes = nodes;		this.similarities = similarities;		this.size = size || sumSize(nodes);		/** @type {string | undefined} */		this.key = undefined;	}	/**	 * @param {function(Node<T>): boolean} filter filter function	 * @returns {Node<T>[] | undefined} removed nodes	 */	popNodes(filter) {		const newNodes = [];		const newSimilarities = [];		const resultNodes = [];		let lastNode;		for (let i = 0; i < this.nodes.length; i++) {			const node = this.nodes[i];			if (filter(node)) {				resultNodes.push(node);			} else {				if (newNodes.length > 0) {					newSimilarities.push(						lastNode === this.nodes[i - 1]							? /** @type {number[]} */ (this.similarities)[i - 1]							: similarity(/** @type {Node<T>} */ (lastNode).key, node.key)					);				}				newNodes.push(node);				lastNode = node;			}		}		if (resultNodes.length === this.nodes.length) return;		this.nodes = newNodes;		this.similarities = newSimilarities;		this.size = sumSize(newNodes);		return resultNodes;	}}/** * @template T * @param {Iterable<Node<T>>} nodes nodes * @returns {number[]} similarities */const getSimilarities = nodes => {	// calculate similarities between lexically adjacent nodes	/** @type {number[]} */	const similarities = [];	let last;	for (const node of nodes) {		if (last !== undefined) {			similarities.push(similarity(last.key, node.key));		}		last = node;	}	return similarities;};/** * @template T * @typedef {object} GroupedItems<T> * @property {string} key * @property {T[]} items * @property {Record<string, number>} size *//** * @template T * @typedef {object} Options * @property {Record<string, number>} maxSize maximum size of a group * @property {Record<string, number>} minSize minimum size of a group (preferred over maximum size) * @property {Iterable<T>} items a list of items * @property {function(T): Record<string, number>} getSize function to get size of an item * @property {function(T): string} getKey function to get the key of an item *//** * @template T * @param {Options<T>} options options object * @returns {GroupedItems<T>[]} grouped items */module.exports = ({ maxSize, minSize, items, getSize, getKey }) => {	/** @type {Group<T>[]} */	const result = [];	const nodes = Array.from(		items,		item => new Node(item, getKey(item), getSize(item))	);	/** @type {Node<T>[]} */	const initialNodes = [];	// lexically ordering of keys	nodes.sort((a, b) => {		if (a.key < b.key) return -1;		if (a.key > b.key) return 1;		return 0;	});	// return nodes bigger than maxSize directly as group	// But make sure that minSize is not violated	for (const node of nodes) {		if (isTooBig(node.size, maxSize) && !isTooSmall(node.size, minSize)) {			result.push(new Group([node], []));		} else {			initialNodes.push(node);		}	}	if (initialNodes.length > 0) {		const initialGroup = new Group(initialNodes, getSimilarities(initialNodes));		/**		 * @param {Group<T>} group group		 * @param {Record<string, number>} consideredSize size of the group to consider		 * @returns {boolean} true, if the group was modified		 */		const removeProblematicNodes = (group, consideredSize = group.size) => {			const problemTypes = getTooSmallTypes(consideredSize, minSize);			if (problemTypes.size > 0) {				// We hit an edge case where the working set is already smaller than minSize				// We merge problematic nodes with the smallest result node to keep minSize intact				const problemNodes = group.popNodes(					n => getNumberOfMatchingSizeTypes(n.size, problemTypes) > 0				);				if (problemNodes === undefined) return false;				// Only merge it with result nodes that have the problematic size type				const possibleResultGroups = result.filter(					n => getNumberOfMatchingSizeTypes(n.size, problemTypes) > 0				);				if (possibleResultGroups.length > 0) {					const bestGroup = possibleResultGroups.reduce((min, group) => {						const minMatches = getNumberOfMatchingSizeTypes(min, problemTypes);						const groupMatches = getNumberOfMatchingSizeTypes(							group,							problemTypes						);						if (minMatches !== groupMatches)							return minMatches < groupMatches ? group : min;						if (							selectiveSizeSum(min.size, problemTypes) >							selectiveSizeSum(group.size, problemTypes)						)							return group;						return min;					});					for (const node of problemNodes) bestGroup.nodes.push(node);					bestGroup.nodes.sort((a, b) => {						if (a.key < b.key) return -1;						if (a.key > b.key) return 1;						return 0;					});				} else {					// There are no other nodes with the same size types					// We create a new group and have to accept that it's smaller than minSize					result.push(new Group(problemNodes, null));				}				return true;			}			return false;		};		if (initialGroup.nodes.length > 0) {			const queue = [initialGroup];			while (queue.length) {				const group = /** @type {Group<T>} */ (queue.pop());				// only groups bigger than maxSize need to be splitted				if (!isTooBig(group.size, maxSize)) {					result.push(group);					continue;				}				// If the group is already too small				// we try to work only with the unproblematic nodes				if (removeProblematicNodes(group)) {					// This changed something, so we try this group again					queue.push(group);					continue;				}				// find unsplittable area from left and right				// going minSize from left and right				// at least one node need to be included otherwise we get stuck				let left = 1;				const leftSize = Object.create(null);				addSizeTo(leftSize, group.nodes[0].size);				while (left < group.nodes.length && isTooSmall(leftSize, minSize)) {					addSizeTo(leftSize, group.nodes[left].size);					left++;				}				let right = group.nodes.length - 2;				const rightSize = Object.create(null);				addSizeTo(rightSize, group.nodes[group.nodes.length - 1].size);				while (right >= 0 && isTooSmall(rightSize, minSize)) {					addSizeTo(rightSize, group.nodes[right].size);					right--;				}				//      left v   v right				// [ O O O ] O O O [ O O O ]				// ^^^^^^^^^ leftSize				//       rightSize ^^^^^^^^^				// leftSize > minSize				// rightSize > minSize				// Perfect split: [ O O O ] [ O O O ]				//                right === left - 1				if (left - 1 > right) {					// We try to remove some problematic nodes to "fix" that					let prevSize;					if (right < group.nodes.length - left) {						subtractSizeFrom(rightSize, group.nodes[right + 1].size);						prevSize = rightSize;					} else {						subtractSizeFrom(leftSize, group.nodes[left - 1].size);						prevSize = leftSize;					}					if (removeProblematicNodes(group, prevSize)) {						// This changed something, so we try this group again						queue.push(group);						continue;					}					// can't split group while holding minSize					// because minSize is preferred of maxSize we return					// the problematic nodes as result here even while it's too big					// To avoid this make sure maxSize > minSize * 3					result.push(group);					continue;				}				if (left <= right) {					// when there is a area between left and right					// we look for best split point					// we split at the minimum similarity					// here key space is separated the most					// But we also need to make sure to not create too small groups					let best = -1;					let bestSimilarity = Infinity;					let pos = left;					const rightSize = sumSize(group.nodes.slice(pos));					//       pos v   v right					// [ O O O ] O O O [ O O O ]					// ^^^^^^^^^ leftSize					// rightSize ^^^^^^^^^^^^^^^					while (pos <= right + 1) {						const similarity = /** @type {number[]} */ (group.similarities)[							pos - 1						];						if (							similarity < bestSimilarity &&							!isTooSmall(leftSize, minSize) &&							!isTooSmall(rightSize, minSize)						) {							best = pos;							bestSimilarity = similarity;						}						addSizeTo(leftSize, group.nodes[pos].size);						subtractSizeFrom(rightSize, group.nodes[pos].size);						pos++;					}					if (best < 0) {						// This can't happen						// but if that assumption is wrong						// fallback to a big group						result.push(group);						continue;					}					left = best;					right = best - 1;				}				// create two new groups for left and right area				// and queue them up				const rightNodes = [group.nodes[right + 1]];				/** @type {number[]} */				const rightSimilarities = [];				for (let i = right + 2; i < group.nodes.length; i++) {					rightSimilarities.push(						/** @type {number[]} */ (group.similarities)[i - 1]					);					rightNodes.push(group.nodes[i]);				}				queue.push(new Group(rightNodes, rightSimilarities));				const leftNodes = [group.nodes[0]];				/** @type {number[]} */				const leftSimilarities = [];				for (let i = 1; i < left; i++) {					leftSimilarities.push(						/** @type {number[]} */ (group.similarities)[i - 1]					);					leftNodes.push(group.nodes[i]);				}				queue.push(new Group(leftNodes, leftSimilarities));			}		}	}	// lexically ordering	result.sort((a, b) => {		if (a.nodes[0].key < b.nodes[0].key) return -1;		if (a.nodes[0].key > b.nodes[0].key) return 1;		return 0;	});	// give every group a name	const usedNames = new Set();	for (let i = 0; i < result.length; i++) {		const group = result[i];		if (group.nodes.length === 1) {			group.key = group.nodes[0].key;		} else {			const first = group.nodes[0];			const last = group.nodes[group.nodes.length - 1];			const name = getName(first.key, last.key, usedNames);			group.key = name;		}	}	// return the results	return result.map(		group =>			/** @type {GroupedItems<T>} */			({				key: group.key,				items: group.nodes.map(node => node.item),				size: group.size			})	);};
 |