| 
									
										
										
										
											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 | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  |  * @template R | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  |  * @typedef {Object} ItemWithGroups | 
					
						
							|  |  |  |  * @property {T} item | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  |  * @property {Set<Group<T, R>>} groups | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @template T | 
					
						
							|  |  |  |  * @template R | 
					
						
							|  |  |  |  * @typedef {{ config: GroupConfig<T, R>, name: string, alreadyGrouped: boolean, items: Set<ItemWithGroups<T, R>> | undefined }} Group | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * @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) => { | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 	/** @type {Set<ItemWithGroups<T, R>>} */ | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 	const itemsWithGroups = new Set(); | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 	/** @type {Map<string, Group<T, R>>} */ | 
					
						
							|  |  |  | 	const allGroups = new Map(); | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 	for (const item of items) { | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 		/** @type {Set<Group<T, R>>} */ | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 		const groups = new Set(); | 
					
						
							|  |  |  | 		for (let i = 0; i < groupConfigs.length; i++) { | 
					
						
							|  |  |  | 			const groupConfig = groupConfigs[i]; | 
					
						
							|  |  |  | 			const keys = groupConfig.getKeys(item); | 
					
						
							|  |  |  | 			if (keys) { | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 				for (const name of keys) { | 
					
						
							|  |  |  | 					const key = `${i}:${name}`; | 
					
						
							|  |  |  | 					let group = allGroups.get(key); | 
					
						
							|  |  |  | 					if (group === undefined) { | 
					
						
							|  |  |  | 						allGroups.set( | 
					
						
							|  |  |  | 							key, | 
					
						
							|  |  |  | 							(group = { | 
					
						
							|  |  |  | 								config: groupConfig, | 
					
						
							|  |  |  | 								name, | 
					
						
							|  |  |  | 								alreadyGrouped: false, | 
					
						
							|  |  |  | 								items: undefined | 
					
						
							|  |  |  | 							}) | 
					
						
							|  |  |  | 						); | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 					groups.add(group); | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		itemsWithGroups.add({ | 
					
						
							|  |  |  | 			item, | 
					
						
							|  |  |  | 			groups | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	/** | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 	 * @param {Set<ItemWithGroups<T, R>>} itemsWithGroups input items with groups | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 	 * @returns {(T | R)[]} groups items | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	const runGrouping = itemsWithGroups => { | 
					
						
							|  |  |  | 		const totalSize = itemsWithGroups.size; | 
					
						
							|  |  |  | 		for (const entry of itemsWithGroups) { | 
					
						
							|  |  |  | 			for (const group of entry.groups) { | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 				if (group.alreadyGrouped) continue; | 
					
						
							|  |  |  | 				const items = group.items; | 
					
						
							|  |  |  | 				if (items === undefined) { | 
					
						
							|  |  |  | 					group.items = new Set([entry]); | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 				} else { | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 					items.add(entry); | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 		/** @type {Map<Group<T, R>, { items: Set<ItemWithGroups<T, R>>, options: GroupOptions | false | undefined, used: boolean }>} */ | 
					
						
							|  |  |  | 		const groupMap = new Map(); | 
					
						
							|  |  |  | 		for (const group of allGroups.values()) { | 
					
						
							|  |  |  | 			if (group.items) { | 
					
						
							|  |  |  | 				const items = group.items; | 
					
						
							|  |  |  | 				group.items = undefined; | 
					
						
							|  |  |  | 				groupMap.set(group, { | 
					
						
							|  |  |  | 					items, | 
					
						
							|  |  |  | 					options: undefined, | 
					
						
							|  |  |  | 					used: false | 
					
						
							|  |  |  | 				}); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 		/** @type {(T | R)[]} */ | 
					
						
							|  |  |  | 		const results = []; | 
					
						
							|  |  |  | 		for (;;) { | 
					
						
							| 
									
										
										
										
											2023-06-12 22:21:21 +08:00
										 |  |  | 			/** @type {Group<T, R> | undefined} */ | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 			let bestGroup = undefined; | 
					
						
							|  |  |  | 			let bestGroupSize = -1; | 
					
						
							|  |  |  | 			let bestGroupItems = undefined; | 
					
						
							|  |  |  | 			let bestGroupOptions = undefined; | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 			for (const [group, state] of groupMap) { | 
					
						
							|  |  |  | 				const { items, used } = state; | 
					
						
							|  |  |  | 				let options = state.options; | 
					
						
							|  |  |  | 				if (options === undefined) { | 
					
						
							|  |  |  | 					const groupConfig = group.config; | 
					
						
							|  |  |  | 					state.options = options = | 
					
						
							|  |  |  | 						(groupConfig.getOptions && | 
					
						
							|  |  |  | 							groupConfig.getOptions( | 
					
						
							|  |  |  | 								group.name, | 
					
						
							|  |  |  | 								Array.from(items, ({ item }) => item) | 
					
						
							|  |  |  | 							)) || | 
					
						
							|  |  |  | 						false; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 				const force = options && options.force; | 
					
						
							|  |  |  | 				if (!force) { | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 					if (bestGroupOptions && bestGroupOptions.force) continue; | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 					if (used) continue; | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 					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 | 
					
						
							| 
									
										
										
										
											2024-01-14 09:41:34 +08:00
										 |  |  | 						); | 
					
						
							| 
									
										
										
										
											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) { | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 					const state = groupMap.get(group); | 
					
						
							|  |  |  | 					if (state !== undefined) { | 
					
						
							|  |  |  | 						state.items.delete(item); | 
					
						
							|  |  |  | 						if (state.items.size === 0) { | 
					
						
							|  |  |  | 							groupMap.delete(group); | 
					
						
							|  |  |  | 						} else { | 
					
						
							|  |  |  | 							state.options = undefined; | 
					
						
							|  |  |  | 							if (groupChildren) { | 
					
						
							|  |  |  | 								state.used = true; | 
					
						
							|  |  |  | 							} | 
					
						
							|  |  |  | 						} | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 				} | 
					
						
							|  |  |  | 			} | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 			groupMap.delete(bestGroup); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 			const key = bestGroup.name; | 
					
						
							|  |  |  | 			const groupConfig = bestGroup.config; | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 
 | 
					
						
							|  |  |  | 			const allItems = Array.from(items, ({ item }) => item); | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 			bestGroup.alreadyGrouped = true; | 
					
						
							| 
									
										
										
										
											2020-09-02 00:08:09 +08:00
										 |  |  | 			const children = groupChildren ? runGrouping(items) : allItems; | 
					
						
							| 
									
										
										
										
											2021-04-23 19:47:18 +08:00
										 |  |  | 			bestGroup.alreadyGrouped = false; | 
					
						
							| 
									
										
										
										
											2020-08-27 15:59:12 +08:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											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; |