smartGrouping.js 5.1 KB

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