| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | /* | 
					
						
							|  |  |  | 	MIT License http://www.opensource.org/licenses/mit-license.php
 | 
					
						
							|  |  |  | 	Author Tobias Koppers @sokra | 
					
						
							|  |  |  | */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | "use strict"; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @typedef {Object} GroupOptions | 
					
						
							|  |  |  |  * @property {boolean=} groupChildren | 
					
						
							|  |  |  |  * @property {boolean=} force | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  |  * @property {number=} targetGroupCount | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @template T | 
					
						
							|  |  |  |  * @template R | 
					
						
							|  |  |  |  * @typedef {Object} GroupConfig | 
					
						
							|  |  |  |  * @property {function(T): string[]} getKeys | 
					
						
							|  |  |  |  * @property {function(string, (R | T)[], T[]): R} createGroup | 
					
						
							|  |  |  |  * @property {function(string, T[]): GroupOptions=} getOptions | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @template T | 
					
						
							|  |  |  |  * @typedef {Object} ItemWithGroups | 
					
						
							|  |  |  |  * @property {T} item | 
					
						
							|  |  |  |  * @property {Set<string>} groups | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @template T | 
					
						
							|  |  |  |  * @template R | 
					
						
							|  |  |  |  * @param {T[]} items the list of items | 
					
						
							|  |  |  |  * @param {GroupConfig<T, R>[]} groupConfigs configuration | 
					
						
							|  |  |  |  * @returns {(R | T)[]} grouped items | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | const smartGrouping = (items, groupConfigs) => { | 
					
						
							|  |  |  | 	/** @type {Set<ItemWithGroups<T>>} */ | 
					
						
							|  |  |  | 	const itemsWithGroups = new Set(); | 
					
						
							|  |  |  | 	/** @type {Map<string, [GroupConfig<T, R>, string]>} */ | 
					
						
							|  |  |  | 	const groupConfigMap = new Map(); | 
					
						
							|  |  |  | 	for (const item of items) { | 
					
						
							|  |  |  | 		const groups = new Set(); | 
					
						
							|  |  |  | 		for (let i = 0; i < groupConfigs.length; i++) { | 
					
						
							|  |  |  | 			const groupConfig = groupConfigs[i]; | 
					
						
							|  |  |  | 			const keys = groupConfig.getKeys(item); | 
					
						
							|  |  |  | 			if (keys) { | 
					
						
							|  |  |  | 				for (const group of keys) { | 
					
						
							|  |  |  | 					const fullGroup = `${i}:${group}`; | 
					
						
							|  |  |  | 					if (!groupConfigMap.has(fullGroup)) { | 
					
						
							|  |  |  | 						groupConfigMap.set(fullGroup, [groupConfig, group]); | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 					groups.add(fullGroup); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		itemsWithGroups.add({ | 
					
						
							|  |  |  | 			item, | 
					
						
							|  |  |  | 			groups | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	const alreadyGrouped = new Set(); | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * @param {Set<ItemWithGroups<T>>} itemsWithGroups input items with groups | 
					
						
							|  |  |  | 	 * @returns {(T | R)[]} groups items | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	const runGrouping = itemsWithGroups => { | 
					
						
							|  |  |  | 		const totalSize = itemsWithGroups.size; | 
					
						
							|  |  |  | 		/** @type {Map<string, Set<ItemWithGroups<T>>>} */ | 
					
						
							|  |  |  | 		const groupMap = new Map(); | 
					
						
							|  |  |  | 		for (const entry of itemsWithGroups) { | 
					
						
							|  |  |  | 			for (const group of entry.groups) { | 
					
						
							|  |  |  | 				if (alreadyGrouped.has(group)) continue; | 
					
						
							|  |  |  | 				const list = groupMap.get(group); | 
					
						
							|  |  |  | 				if (list === undefined) { | 
					
						
							|  |  |  | 					groupMap.set(group, new Set([entry])); | 
					
						
							|  |  |  | 				} else { | 
					
						
							|  |  |  | 					list.add(entry); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		/** @type {Set<string>} */ | 
					
						
							|  |  |  | 		const usedGroups = new Set(); | 
					
						
							|  |  |  | 		/** @type {(T | R)[]} */ | 
					
						
							|  |  |  | 		const results = []; | 
					
						
							|  |  |  | 		for (;;) { | 
					
						
							|  |  |  | 			let bestGroup = undefined; | 
					
						
							|  |  |  | 			let bestGroupSize = -1; | 
					
						
							|  |  |  | 			let bestGroupItems = undefined; | 
					
						
							|  |  |  | 			let bestGroupOptions = undefined; | 
					
						
							|  |  |  | 			for (const [group, items] of groupMap) { | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 				if (items.size === 0) continue; | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 				const [groupConfig, groupKey] = groupConfigMap.get(group); | 
					
						
							|  |  |  | 				const options = | 
					
						
							|  |  |  | 					groupConfig.getOptions && | 
					
						
							|  |  |  | 					groupConfig.getOptions( | 
					
						
							|  |  |  | 						groupKey, | 
					
						
							|  |  |  | 						Array.from(items, ({ item }) => item) | 
					
						
							|  |  |  | 					); | 
					
						
							|  |  |  | 				const force = options && options.force; | 
					
						
							|  |  |  | 				if (!force) { | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 					if (bestGroupOptions && bestGroupOptions.force) continue; | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 					if (usedGroups.has(group)) continue; | 
					
						
							|  |  |  | 					if (items.size <= 1 || totalSize - items.size <= 1) { | 
					
						
							|  |  |  | 						continue; | 
					
						
							|  |  |  | 					} | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 				const targetGroupCount = (options && options.targetGroupCount) || 4; | 
					
						
							| 
									
										
										
										
											2020-09-02 20:09:29 +08:00
										 |  |  | 				let sizeValue = force | 
					
						
							|  |  |  | 					? items.size | 
					
						
							|  |  |  | 					: Math.min( | 
					
						
							|  |  |  | 							items.size, | 
					
						
							|  |  |  | 							(totalSize * 2) / targetGroupCount + | 
					
						
							|  |  |  | 								itemsWithGroups.size - | 
					
						
							|  |  |  | 								items.size | 
					
						
							|  |  |  | 					  ); | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 				if ( | 
					
						
							|  |  |  | 					sizeValue > bestGroupSize || | 
					
						
							|  |  |  | 					(force && (!bestGroupOptions || !bestGroupOptions.force)) | 
					
						
							|  |  |  | 				) { | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 					bestGroup = group; | 
					
						
							|  |  |  | 					bestGroupSize = sizeValue; | 
					
						
							|  |  |  | 					bestGroupItems = items; | 
					
						
							|  |  |  | 					bestGroupOptions = options; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			if (bestGroup === undefined) { | 
					
						
							|  |  |  | 				break; | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			const items = new Set(bestGroupItems); | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 			const options = bestGroupOptions; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			const groupChildren = !options || options.groupChildren !== false; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 			for (const item of items) { | 
					
						
							|  |  |  | 				itemsWithGroups.delete(item); | 
					
						
							|  |  |  | 				// Remove all groups that items have from the map to not select them again
 | 
					
						
							|  |  |  | 				for (const group of item.groups) { | 
					
						
							|  |  |  | 					const list = groupMap.get(group); | 
					
						
							|  |  |  | 					if (list !== undefined) list.delete(item); | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 					if (groupChildren) { | 
					
						
							|  |  |  | 						usedGroups.add(group); | 
					
						
							|  |  |  | 					} | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 			groupMap.delete(bestGroup); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 			const idx = bestGroup.indexOf(":"); | 
					
						
							|  |  |  | 			const configKey = bestGroup.slice(0, idx); | 
					
						
							|  |  |  | 			const key = bestGroup.slice(idx + 1); | 
					
						
							|  |  |  | 			const groupConfig = groupConfigs[+configKey]; | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			const allItems = Array.from(items, ({ item }) => item); | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			alreadyGrouped.add(bestGroup); | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 			const children = groupChildren ? runGrouping(items) : allItems; | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 			alreadyGrouped.delete(bestGroup); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 			results.push(groupConfig.createGroup(key, children, allItems)); | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 		} | 
					
						
							|  |  |  | 		for (const { item } of itemsWithGroups) { | 
					
						
							|  |  |  | 			results.push(item); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		return results; | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | 	return runGrouping(itemsWithGroups); | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | module.exports = smartGrouping; |