smartGrouping.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. /**
  7. * Defines the group options type used by this module.
  8. * @typedef {object} GroupOptions
  9. * @property {boolean=} groupChildren
  10. * @property {boolean=} force
  11. * @property {number=} targetGroupCount
  12. */
  13. /**
  14. * Defines the group config type used by this module.
  15. * @template I
  16. * @template G
  17. * @typedef {object} GroupConfig
  18. * @property {(item: I) => string[] | undefined} getKeys
  19. * @property {(name: string, items: I[]) => GroupOptions=} getOptions
  20. * @property {(key: string, children: I[], items: I[]) => G} createGroup
  21. */
  22. /**
  23. * Defines the group type used by this module.
  24. * @template I
  25. * @template G
  26. * @typedef {{ config: GroupConfig<I, G>, name: string, alreadyGrouped: boolean, items: Items<I, G> | undefined }} Group
  27. */
  28. /**
  29. * Defines the groups type used by this module.
  30. * @template I, G
  31. * @typedef {Set<Group<I, G>>} Groups
  32. */
  33. /**
  34. * Defines the item with groups type used by this module.
  35. * @template I
  36. * @template G
  37. * @typedef {object} ItemWithGroups
  38. * @property {I} item
  39. * @property {Groups<I, G>} groups
  40. */
  41. /**
  42. * Defines the items type used by this module.
  43. * @template T, G
  44. * @typedef {Set<ItemWithGroups<T, G>>} Items
  45. */
  46. /**
  47. * Returns grouped items.
  48. * @template I
  49. * @template G
  50. * @template R
  51. * @param {I[]} items the list of items
  52. * @param {GroupConfig<I, G>[]} groupConfigs configuration
  53. * @returns {(I | G)[]} grouped items
  54. */
  55. const smartGrouping = (items, groupConfigs) => {
  56. /** @type {Items<I, G>} */
  57. const itemsWithGroups = new Set();
  58. /** @type {Map<string, Group<I, G>>} */
  59. const allGroups = new Map();
  60. for (const item of items) {
  61. /** @type {Groups<I, G>} */
  62. const groups = new Set();
  63. for (let i = 0; i < groupConfigs.length; i++) {
  64. const groupConfig = groupConfigs[i];
  65. const keys = groupConfig.getKeys(item);
  66. if (keys) {
  67. for (const name of keys) {
  68. const key = `${i}:${name}`;
  69. let group = allGroups.get(key);
  70. if (group === undefined) {
  71. allGroups.set(
  72. key,
  73. (group = {
  74. config: groupConfig,
  75. name,
  76. alreadyGrouped: false,
  77. items: undefined
  78. })
  79. );
  80. }
  81. groups.add(group);
  82. }
  83. }
  84. }
  85. itemsWithGroups.add({
  86. item,
  87. groups
  88. });
  89. }
  90. /**
  91. * Returns groups items.
  92. * @param {Items<I, G>} itemsWithGroups input items with groups
  93. * @returns {(I | G)[]} groups items
  94. */
  95. const runGrouping = (itemsWithGroups) => {
  96. const totalSize = itemsWithGroups.size;
  97. for (const entry of itemsWithGroups) {
  98. for (const group of entry.groups) {
  99. if (group.alreadyGrouped) continue;
  100. const items = group.items;
  101. if (items === undefined) {
  102. group.items = new Set([entry]);
  103. } else {
  104. items.add(entry);
  105. }
  106. }
  107. }
  108. /** @type {Map<Group<I, G>, { items: Items<I, G>, options: GroupOptions | false | undefined, used: boolean }>} */
  109. const groupMap = new Map();
  110. for (const group of allGroups.values()) {
  111. if (group.items) {
  112. const items = group.items;
  113. group.items = undefined;
  114. groupMap.set(group, {
  115. items,
  116. options: undefined,
  117. used: false
  118. });
  119. }
  120. }
  121. /** @type {(I | G)[]} */
  122. const results = [];
  123. for (;;) {
  124. /** @type {Group<I, G> | undefined} */
  125. let bestGroup;
  126. let bestGroupSize = -1;
  127. /** @type {Items<I, G> | undefined} */
  128. let bestGroupItems;
  129. /** @type {GroupOptions | false | undefined} */
  130. let bestGroupOptions;
  131. for (const [group, state] of groupMap) {
  132. const { items, used } = state;
  133. let options = state.options;
  134. if (options === undefined) {
  135. const groupConfig = group.config;
  136. state.options = options =
  137. (groupConfig.getOptions &&
  138. groupConfig.getOptions(
  139. group.name,
  140. Array.from(items, ({ item }) => item)
  141. )) ||
  142. false;
  143. }
  144. const force = options && options.force;
  145. if (!force) {
  146. if (bestGroupOptions && bestGroupOptions.force) continue;
  147. if (used) continue;
  148. if (items.size <= 1 || totalSize - items.size <= 1) {
  149. continue;
  150. }
  151. }
  152. const targetGroupCount = (options && options.targetGroupCount) || 4;
  153. const sizeValue = force
  154. ? items.size
  155. : Math.min(
  156. items.size,
  157. (totalSize * 2) / targetGroupCount +
  158. itemsWithGroups.size -
  159. items.size
  160. );
  161. if (
  162. sizeValue > bestGroupSize ||
  163. (force && (!bestGroupOptions || !bestGroupOptions.force))
  164. ) {
  165. bestGroup = group;
  166. bestGroupSize = sizeValue;
  167. bestGroupItems = items;
  168. bestGroupOptions = options;
  169. }
  170. }
  171. if (bestGroup === undefined) {
  172. break;
  173. }
  174. const items = new Set(bestGroupItems);
  175. const options = bestGroupOptions;
  176. const groupChildren = !options || options.groupChildren !== false;
  177. for (const item of items) {
  178. itemsWithGroups.delete(item);
  179. // Remove all groups that items have from the map to not select them again
  180. for (const group of item.groups) {
  181. const state = groupMap.get(group);
  182. if (state !== undefined) {
  183. state.items.delete(item);
  184. if (state.items.size === 0) {
  185. groupMap.delete(group);
  186. } else {
  187. state.options = undefined;
  188. if (groupChildren) {
  189. state.used = true;
  190. }
  191. }
  192. }
  193. }
  194. }
  195. groupMap.delete(bestGroup);
  196. const key = bestGroup.name;
  197. const groupConfig = bestGroup.config;
  198. const allItems = Array.from(items, ({ item }) => item);
  199. bestGroup.alreadyGrouped = true;
  200. const children = groupChildren ? runGrouping(items) : allItems;
  201. bestGroup.alreadyGrouped = false;
  202. results.push(
  203. groupConfig.createGroup(key, /** @type {I[]} */ (children), allItems)
  204. );
  205. }
  206. for (const { item } of itemsWithGroups) {
  207. results.push(item);
  208. }
  209. return results;
  210. };
  211. return runGrouping(itemsWithGroups);
  212. };
  213. module.exports = smartGrouping;