| 1 | /*
|
|---|
| 2 | MIT License http://www.opensource.org/licenses/mit-license.php
|
|---|
| 3 | Author Tobias Koppers @sokra
|
|---|
| 4 | */
|
|---|
| 5 |
|
|---|
| 6 | "use strict";
|
|---|
| 7 |
|
|---|
| 8 | // Simulations show these probabilities for a single change
|
|---|
| 9 | // 93.1% that one group is invalidated
|
|---|
| 10 | // 4.8% that two groups are invalidated
|
|---|
| 11 | // 1.1% that 3 groups are invalidated
|
|---|
| 12 | // 0.1% that 4 or more groups are invalidated
|
|---|
| 13 | //
|
|---|
| 14 | // And these for removing/adding 10 lexically adjacent files
|
|---|
| 15 | // 64.5% that one group is invalidated
|
|---|
| 16 | // 24.8% that two groups are invalidated
|
|---|
| 17 | // 7.8% that 3 groups are invalidated
|
|---|
| 18 | // 2.7% that 4 or more groups are invalidated
|
|---|
| 19 | //
|
|---|
| 20 | // And these for removing/adding 3 random files
|
|---|
| 21 | // 0% that one group is invalidated
|
|---|
| 22 | // 3.7% that two groups are invalidated
|
|---|
| 23 | // 80.8% that 3 groups are invalidated
|
|---|
| 24 | // 12.3% that 4 groups are invalidated
|
|---|
| 25 | // 3.2% that 5 or more groups are invalidated
|
|---|
| 26 |
|
|---|
| 27 | /**
|
|---|
| 28 | * Returns the similarity as number.
|
|---|
| 29 | * @param {string} a key
|
|---|
| 30 | * @param {string} b key
|
|---|
| 31 | * @returns {number} the similarity as number
|
|---|
| 32 | */
|
|---|
| 33 | const similarity = (a, b) => {
|
|---|
| 34 | const l = Math.min(a.length, b.length);
|
|---|
| 35 | let dist = 0;
|
|---|
| 36 | for (let i = 0; i < l; i++) {
|
|---|
| 37 | const ca = a.charCodeAt(i);
|
|---|
| 38 | const cb = b.charCodeAt(i);
|
|---|
| 39 | dist += Math.max(0, 10 - Math.abs(ca - cb));
|
|---|
| 40 | }
|
|---|
| 41 | return dist;
|
|---|
| 42 | };
|
|---|
| 43 |
|
|---|
| 44 | /**
|
|---|
| 45 | * Returns the common part and a single char for the difference.
|
|---|
| 46 | * @param {string} a key
|
|---|
| 47 | * @param {string} b key
|
|---|
| 48 | * @param {Set<string>} usedNames set of already used names
|
|---|
| 49 | * @returns {string} the common part and a single char for the difference
|
|---|
| 50 | */
|
|---|
| 51 | const getName = (a, b, usedNames) => {
|
|---|
| 52 | const l = Math.min(a.length, b.length);
|
|---|
| 53 | let i = 0;
|
|---|
| 54 | while (i < l) {
|
|---|
| 55 | if (a.charCodeAt(i) !== b.charCodeAt(i)) {
|
|---|
| 56 | i++;
|
|---|
| 57 | break;
|
|---|
| 58 | }
|
|---|
| 59 | i++;
|
|---|
| 60 | }
|
|---|
| 61 | while (i < l) {
|
|---|
| 62 | const name = a.slice(0, i);
|
|---|
| 63 | const lowerName = name.toLowerCase();
|
|---|
| 64 | if (!usedNames.has(lowerName)) {
|
|---|
| 65 | usedNames.add(lowerName);
|
|---|
| 66 | return name;
|
|---|
| 67 | }
|
|---|
| 68 | i++;
|
|---|
| 69 | }
|
|---|
| 70 | // names always contain a hash, so this is always unique
|
|---|
| 71 | // we don't need to check usedNames nor add it
|
|---|
| 72 | return a;
|
|---|
| 73 | };
|
|---|
| 74 |
|
|---|
| 75 | /** @typedef {Record<string, number>} Sizes */
|
|---|
| 76 |
|
|---|
| 77 | /**
|
|---|
| 78 | * Adds the provided total to this object.
|
|---|
| 79 | * @param {Sizes} total total size
|
|---|
| 80 | * @param {Sizes} size single size
|
|---|
| 81 | * @returns {void}
|
|---|
| 82 | */
|
|---|
| 83 | const addSizeTo = (total, size) => {
|
|---|
| 84 | for (const key of Object.keys(size)) {
|
|---|
| 85 | total[key] = (total[key] || 0) + size[key];
|
|---|
| 86 | }
|
|---|
| 87 | };
|
|---|
| 88 |
|
|---|
| 89 | /**
|
|---|
| 90 | * Subtract size from.
|
|---|
| 91 | * @param {Sizes} total total size
|
|---|
| 92 | * @param {Sizes} size single size
|
|---|
| 93 | * @returns {void}
|
|---|
| 94 | */
|
|---|
| 95 | const subtractSizeFrom = (total, size) => {
|
|---|
| 96 | for (const key of Object.keys(size)) {
|
|---|
| 97 | total[key] -= size[key];
|
|---|
| 98 | }
|
|---|
| 99 | };
|
|---|
| 100 |
|
|---|
| 101 | /**
|
|---|
| 102 | * Returns total size.
|
|---|
| 103 | * @template T
|
|---|
| 104 | * @param {Iterable<Node<T>>} nodes some nodes
|
|---|
| 105 | * @returns {Sizes} total size
|
|---|
| 106 | */
|
|---|
| 107 | const sumSize = (nodes) => {
|
|---|
| 108 | /** @type {Sizes} */
|
|---|
| 109 | const sum = Object.create(null);
|
|---|
| 110 | for (const node of nodes) {
|
|---|
| 111 | addSizeTo(sum, node.size);
|
|---|
| 112 | }
|
|---|
| 113 | return sum;
|
|---|
| 114 | };
|
|---|
| 115 |
|
|---|
| 116 | /**
|
|---|
| 117 | * Checks whether this object is too big.
|
|---|
| 118 | * @param {Sizes} size size
|
|---|
| 119 | * @param {Sizes} maxSize minimum size
|
|---|
| 120 | * @returns {boolean} true, when size is too big
|
|---|
| 121 | */
|
|---|
| 122 | const isTooBig = (size, maxSize) => {
|
|---|
| 123 | for (const key of Object.keys(size)) {
|
|---|
| 124 | const s = size[key];
|
|---|
| 125 | if (s === 0) continue;
|
|---|
| 126 | const maxSizeValue = maxSize[key];
|
|---|
| 127 | if (typeof maxSizeValue === "number" && s > maxSizeValue) return true;
|
|---|
| 128 | }
|
|---|
| 129 | return false;
|
|---|
| 130 | };
|
|---|
| 131 |
|
|---|
| 132 | /**
|
|---|
| 133 | * Checks whether this object is too small.
|
|---|
| 134 | * @param {Sizes} size size
|
|---|
| 135 | * @param {Sizes} minSize minimum size
|
|---|
| 136 | * @returns {boolean} true, when size is too small
|
|---|
| 137 | */
|
|---|
| 138 | const isTooSmall = (size, minSize) => {
|
|---|
| 139 | for (const key of Object.keys(size)) {
|
|---|
| 140 | const s = size[key];
|
|---|
| 141 | if (s === 0) continue;
|
|---|
| 142 | const minSizeValue = minSize[key];
|
|---|
| 143 | if (typeof minSizeValue === "number" && s < minSizeValue) return true;
|
|---|
| 144 | }
|
|---|
| 145 | return false;
|
|---|
| 146 | };
|
|---|
| 147 |
|
|---|
| 148 | /** @typedef {Set<string>} Types */
|
|---|
| 149 |
|
|---|
| 150 | /**
|
|---|
| 151 | * Gets too small types.
|
|---|
| 152 | * @param {Sizes} size size
|
|---|
| 153 | * @param {Sizes} minSize minimum size
|
|---|
| 154 | * @returns {Types} set of types that are too small
|
|---|
| 155 | */
|
|---|
| 156 | const getTooSmallTypes = (size, minSize) => {
|
|---|
| 157 | /** @type {Types} */
|
|---|
| 158 | const types = new Set();
|
|---|
| 159 | for (const key of Object.keys(size)) {
|
|---|
| 160 | const s = size[key];
|
|---|
| 161 | if (s === 0) continue;
|
|---|
| 162 | const minSizeValue = minSize[key];
|
|---|
| 163 | if (typeof minSizeValue === "number" && s < minSizeValue) types.add(key);
|
|---|
| 164 | }
|
|---|
| 165 | return types;
|
|---|
| 166 | };
|
|---|
| 167 |
|
|---|
| 168 | /**
|
|---|
| 169 | * Gets number of matching size types.
|
|---|
| 170 | * @template {object} T
|
|---|
| 171 | * @param {T} size size
|
|---|
| 172 | * @param {Types} types types
|
|---|
| 173 | * @returns {number} number of matching size types
|
|---|
| 174 | */
|
|---|
| 175 | const getNumberOfMatchingSizeTypes = (size, types) => {
|
|---|
| 176 | let i = 0;
|
|---|
| 177 | for (const key of Object.keys(size)) {
|
|---|
| 178 | if (size[/** @type {keyof T} */ (key)] !== 0 && types.has(key)) i++;
|
|---|
| 179 | }
|
|---|
| 180 | return i;
|
|---|
| 181 | };
|
|---|
| 182 |
|
|---|
| 183 | /**
|
|---|
| 184 | * Selective size sum.
|
|---|
| 185 | * @param {Sizes} size size
|
|---|
| 186 | * @param {Types} types types
|
|---|
| 187 | * @returns {number} selective size sum
|
|---|
| 188 | */
|
|---|
| 189 | const selectiveSizeSum = (size, types) => {
|
|---|
| 190 | let sum = 0;
|
|---|
| 191 | for (const key of Object.keys(size)) {
|
|---|
| 192 | if (size[key] !== 0 && types.has(key)) sum += size[key];
|
|---|
| 193 | }
|
|---|
| 194 | return sum;
|
|---|
| 195 | };
|
|---|
| 196 |
|
|---|
| 197 | /**
|
|---|
| 198 | * Represents the node runtime component.
|
|---|
| 199 | * @template T
|
|---|
| 200 | */
|
|---|
| 201 | class Node {
|
|---|
| 202 | /**
|
|---|
| 203 | * Creates an instance of Node.
|
|---|
| 204 | * @param {T} item item
|
|---|
| 205 | * @param {string} key key
|
|---|
| 206 | * @param {Sizes} size size
|
|---|
| 207 | */
|
|---|
| 208 | constructor(item, key, size) {
|
|---|
| 209 | this.item = item;
|
|---|
| 210 | this.key = key;
|
|---|
| 211 | this.size = size;
|
|---|
| 212 | }
|
|---|
| 213 | }
|
|---|
| 214 |
|
|---|
| 215 | /** @typedef {number[]} Similarities */
|
|---|
| 216 |
|
|---|
| 217 | /**
|
|---|
| 218 | * Represents the group runtime component.
|
|---|
| 219 | * @template T
|
|---|
| 220 | */
|
|---|
| 221 | class Group {
|
|---|
| 222 | /**
|
|---|
| 223 | * Creates an instance of Group.
|
|---|
| 224 | * @param {Node<T>[]} nodes nodes
|
|---|
| 225 | * @param {Similarities | null} similarities similarities between the nodes (length = nodes.length - 1)
|
|---|
| 226 | * @param {Sizes=} size size of the group
|
|---|
| 227 | */
|
|---|
| 228 | constructor(nodes, similarities, size) {
|
|---|
| 229 | this.nodes = nodes;
|
|---|
| 230 | this.similarities = similarities;
|
|---|
| 231 | this.size = size || sumSize(nodes);
|
|---|
| 232 | /** @type {string | undefined} */
|
|---|
| 233 | this.key = undefined;
|
|---|
| 234 | }
|
|---|
| 235 |
|
|---|
| 236 | /**
|
|---|
| 237 | * Returns removed nodes.
|
|---|
| 238 | * @param {(node: Node<T>) => boolean} filter filter function
|
|---|
| 239 | * @returns {Node<T>[] | undefined} removed nodes
|
|---|
| 240 | */
|
|---|
| 241 | popNodes(filter) {
|
|---|
| 242 | /** @type {Node<T>[]} */
|
|---|
| 243 | const newNodes = [];
|
|---|
| 244 | /** @type {Similarities} */
|
|---|
| 245 | const newSimilarities = [];
|
|---|
| 246 | /** @type {Node<T>[]} */
|
|---|
| 247 | const resultNodes = [];
|
|---|
| 248 | /** @type {undefined | Node<T>} */
|
|---|
| 249 | let lastNode;
|
|---|
| 250 | for (let i = 0; i < this.nodes.length; i++) {
|
|---|
| 251 | const node = this.nodes[i];
|
|---|
| 252 | if (filter(node)) {
|
|---|
| 253 | resultNodes.push(node);
|
|---|
| 254 | } else {
|
|---|
| 255 | if (newNodes.length > 0) {
|
|---|
| 256 | newSimilarities.push(
|
|---|
| 257 | lastNode === this.nodes[i - 1]
|
|---|
| 258 | ? /** @type {Similarities} */ (this.similarities)[i - 1]
|
|---|
| 259 | : similarity(/** @type {Node<T>} */ (lastNode).key, node.key)
|
|---|
| 260 | );
|
|---|
| 261 | }
|
|---|
| 262 | newNodes.push(node);
|
|---|
| 263 | lastNode = node;
|
|---|
| 264 | }
|
|---|
| 265 | }
|
|---|
| 266 | if (resultNodes.length === this.nodes.length) return;
|
|---|
| 267 | this.nodes = newNodes;
|
|---|
| 268 | this.similarities = newSimilarities;
|
|---|
| 269 | this.size = sumSize(newNodes);
|
|---|
| 270 | return resultNodes;
|
|---|
| 271 | }
|
|---|
| 272 | }
|
|---|
| 273 |
|
|---|
| 274 | /**
|
|---|
| 275 | * Returns similarities.
|
|---|
| 276 | * @template T
|
|---|
| 277 | * @param {Iterable<Node<T>>} nodes nodes
|
|---|
| 278 | * @returns {Similarities} similarities
|
|---|
| 279 | */
|
|---|
| 280 | const getSimilarities = (nodes) => {
|
|---|
| 281 | // calculate similarities between lexically adjacent nodes
|
|---|
| 282 | /** @type {Similarities} */
|
|---|
| 283 | const similarities = [];
|
|---|
| 284 | /** @type {undefined | Node<T>} */
|
|---|
| 285 | let last;
|
|---|
| 286 | for (const node of nodes) {
|
|---|
| 287 | if (last !== undefined) {
|
|---|
| 288 | similarities.push(similarity(last.key, node.key));
|
|---|
| 289 | }
|
|---|
| 290 | last = node;
|
|---|
| 291 | }
|
|---|
| 292 | return similarities;
|
|---|
| 293 | };
|
|---|
| 294 |
|
|---|
| 295 | /**
|
|---|
| 296 | * Defines the shared type used by this module.
|
|---|
| 297 | * @template T
|
|---|
| 298 | * @typedef {object} GroupedItems<T>
|
|---|
| 299 | * @property {string} key
|
|---|
| 300 | * @property {T[]} items
|
|---|
| 301 | * @property {Sizes} size
|
|---|
| 302 | */
|
|---|
| 303 |
|
|---|
| 304 | /**
|
|---|
| 305 | * Defines the options type used by this module.
|
|---|
| 306 | * @template T
|
|---|
| 307 | * @typedef {object} Options
|
|---|
| 308 | * @property {Sizes} maxSize maximum size of a group
|
|---|
| 309 | * @property {Sizes} minSize minimum size of a group (preferred over maximum size)
|
|---|
| 310 | * @property {Iterable<T>} items a list of items
|
|---|
| 311 | * @property {(item: T) => Sizes} getSize function to get size of an item
|
|---|
| 312 | * @property {(item: T) => string} getKey function to get the key of an item
|
|---|
| 313 | */
|
|---|
| 314 |
|
|---|
| 315 | /**
|
|---|
| 316 | * Returns grouped items.
|
|---|
| 317 | * @template T
|
|---|
| 318 | * @param {Options<T>} options options object
|
|---|
| 319 | * @returns {GroupedItems<T>[]} grouped items
|
|---|
| 320 | */
|
|---|
| 321 | module.exports = ({ maxSize, minSize, items, getSize, getKey }) => {
|
|---|
| 322 | /** @type {Group<T>[]} */
|
|---|
| 323 | const result = [];
|
|---|
| 324 |
|
|---|
| 325 | const nodes = Array.from(
|
|---|
| 326 | items,
|
|---|
| 327 | (item) => new Node(item, getKey(item), getSize(item))
|
|---|
| 328 | );
|
|---|
| 329 |
|
|---|
| 330 | /** @type {Node<T>[]} */
|
|---|
| 331 | const initialNodes = [];
|
|---|
| 332 |
|
|---|
| 333 | // lexically ordering of keys
|
|---|
| 334 | nodes.sort((a, b) => {
|
|---|
| 335 | if (a.key < b.key) return -1;
|
|---|
| 336 | if (a.key > b.key) return 1;
|
|---|
| 337 | return 0;
|
|---|
| 338 | });
|
|---|
| 339 |
|
|---|
| 340 | // return nodes bigger than maxSize directly as group
|
|---|
| 341 | // But make sure that minSize is not violated
|
|---|
| 342 | for (const node of nodes) {
|
|---|
| 343 | if (isTooBig(node.size, maxSize) && !isTooSmall(node.size, minSize)) {
|
|---|
| 344 | result.push(new Group([node], []));
|
|---|
| 345 | } else {
|
|---|
| 346 | initialNodes.push(node);
|
|---|
| 347 | }
|
|---|
| 348 | }
|
|---|
| 349 |
|
|---|
| 350 | if (initialNodes.length > 0) {
|
|---|
| 351 | const initialGroup = new Group(initialNodes, getSimilarities(initialNodes));
|
|---|
| 352 |
|
|---|
| 353 | /**
|
|---|
| 354 | * Removes problematic nodes.
|
|---|
| 355 | * @param {Group<T>} group group
|
|---|
| 356 | * @param {Sizes} consideredSize size of the group to consider
|
|---|
| 357 | * @returns {boolean} true, if the group was modified
|
|---|
| 358 | */
|
|---|
| 359 | const removeProblematicNodes = (group, consideredSize = group.size) => {
|
|---|
| 360 | const problemTypes = getTooSmallTypes(consideredSize, minSize);
|
|---|
| 361 | if (problemTypes.size > 0) {
|
|---|
| 362 | // We hit an edge case where the working set is already smaller than minSize
|
|---|
| 363 | // We merge problematic nodes with the smallest result node to keep minSize intact
|
|---|
| 364 | const problemNodes = group.popNodes(
|
|---|
| 365 | (n) => getNumberOfMatchingSizeTypes(n.size, problemTypes) > 0
|
|---|
| 366 | );
|
|---|
| 367 | if (problemNodes === undefined) return false;
|
|---|
| 368 | // Only merge it with result nodes that have the problematic size type
|
|---|
| 369 | const possibleResultGroups = result.filter(
|
|---|
| 370 | (n) => getNumberOfMatchingSizeTypes(n.size, problemTypes) > 0
|
|---|
| 371 | );
|
|---|
| 372 | if (possibleResultGroups.length > 0) {
|
|---|
| 373 | const bestGroup = possibleResultGroups.reduce((min, group) => {
|
|---|
| 374 | const minMatches = getNumberOfMatchingSizeTypes(min, problemTypes);
|
|---|
| 375 | const groupMatches = getNumberOfMatchingSizeTypes(
|
|---|
| 376 | group,
|
|---|
| 377 | problemTypes
|
|---|
| 378 | );
|
|---|
| 379 | if (minMatches !== groupMatches) {
|
|---|
| 380 | return minMatches < groupMatches ? group : min;
|
|---|
| 381 | }
|
|---|
| 382 | if (
|
|---|
| 383 | selectiveSizeSum(min.size, problemTypes) >
|
|---|
| 384 | selectiveSizeSum(group.size, problemTypes)
|
|---|
| 385 | ) {
|
|---|
| 386 | return group;
|
|---|
| 387 | }
|
|---|
| 388 | return min;
|
|---|
| 389 | });
|
|---|
| 390 | for (const node of problemNodes) bestGroup.nodes.push(node);
|
|---|
| 391 | bestGroup.nodes.sort((a, b) => {
|
|---|
| 392 | if (a.key < b.key) return -1;
|
|---|
| 393 | if (a.key > b.key) return 1;
|
|---|
| 394 | return 0;
|
|---|
| 395 | });
|
|---|
| 396 | } else {
|
|---|
| 397 | // There are no other nodes with the same size types
|
|---|
| 398 | // We create a new group and have to accept that it's smaller than minSize
|
|---|
| 399 | result.push(new Group(problemNodes, null));
|
|---|
| 400 | }
|
|---|
| 401 | return true;
|
|---|
| 402 | }
|
|---|
| 403 | return false;
|
|---|
| 404 | };
|
|---|
| 405 |
|
|---|
| 406 | if (initialGroup.nodes.length > 0) {
|
|---|
| 407 | const queue = [initialGroup];
|
|---|
| 408 |
|
|---|
| 409 | while (queue.length) {
|
|---|
| 410 | const group = /** @type {Group<T>} */ (queue.pop());
|
|---|
| 411 | // only groups bigger than maxSize need to be splitted
|
|---|
| 412 | if (!isTooBig(group.size, maxSize)) {
|
|---|
| 413 | result.push(group);
|
|---|
| 414 | continue;
|
|---|
| 415 | }
|
|---|
| 416 | // If the group is already too small
|
|---|
| 417 | // we try to work only with the unproblematic nodes
|
|---|
| 418 | if (removeProblematicNodes(group)) {
|
|---|
| 419 | // This changed something, so we try this group again
|
|---|
| 420 | queue.push(group);
|
|---|
| 421 | continue;
|
|---|
| 422 | }
|
|---|
| 423 |
|
|---|
| 424 | // find unsplittable area from left and right
|
|---|
| 425 | // going minSize from left and right
|
|---|
| 426 | // at least one node need to be included otherwise we get stuck
|
|---|
| 427 | let left = 1;
|
|---|
| 428 | /** @type {Sizes} */
|
|---|
| 429 | const leftSize = Object.create(null);
|
|---|
| 430 | addSizeTo(leftSize, group.nodes[0].size);
|
|---|
| 431 | while (left < group.nodes.length && isTooSmall(leftSize, minSize)) {
|
|---|
| 432 | addSizeTo(leftSize, group.nodes[left].size);
|
|---|
| 433 | left++;
|
|---|
| 434 | }
|
|---|
| 435 | let right = group.nodes.length - 2;
|
|---|
| 436 | /** @type {Sizes} */
|
|---|
| 437 | const rightSize = Object.create(null);
|
|---|
| 438 | addSizeTo(rightSize, group.nodes[group.nodes.length - 1].size);
|
|---|
| 439 | while (right >= 0 && isTooSmall(rightSize, minSize)) {
|
|---|
| 440 | addSizeTo(rightSize, group.nodes[right].size);
|
|---|
| 441 | right--;
|
|---|
| 442 | }
|
|---|
| 443 |
|
|---|
| 444 | // left v v right
|
|---|
| 445 | // [ O O O ] O O O [ O O O ]
|
|---|
| 446 | // ^^^^^^^^^ leftSize
|
|---|
| 447 | // rightSize ^^^^^^^^^
|
|---|
| 448 | // leftSize > minSize
|
|---|
| 449 | // rightSize > minSize
|
|---|
| 450 |
|
|---|
| 451 | // Perfect split: [ O O O ] [ O O O ]
|
|---|
| 452 | // right === left - 1
|
|---|
| 453 |
|
|---|
| 454 | if (left - 1 > right) {
|
|---|
| 455 | // We try to remove some problematic nodes to "fix" that
|
|---|
| 456 | /** @type {Sizes} */
|
|---|
| 457 | let prevSize;
|
|---|
| 458 | if (right < group.nodes.length - left) {
|
|---|
| 459 | subtractSizeFrom(rightSize, group.nodes[right + 1].size);
|
|---|
| 460 | prevSize = rightSize;
|
|---|
| 461 | } else {
|
|---|
| 462 | subtractSizeFrom(leftSize, group.nodes[left - 1].size);
|
|---|
| 463 | prevSize = leftSize;
|
|---|
| 464 | }
|
|---|
| 465 | if (removeProblematicNodes(group, prevSize)) {
|
|---|
| 466 | // This changed something, so we try this group again
|
|---|
| 467 | queue.push(group);
|
|---|
| 468 | continue;
|
|---|
| 469 | }
|
|---|
| 470 | // can't split group while holding minSize
|
|---|
| 471 | // because minSize is preferred of maxSize we return
|
|---|
| 472 | // the problematic nodes as result here even while it's too big
|
|---|
| 473 | // To avoid this make sure maxSize > minSize * 3
|
|---|
| 474 | result.push(group);
|
|---|
| 475 | continue;
|
|---|
| 476 | }
|
|---|
| 477 | if (left <= right) {
|
|---|
| 478 | // when there is a area between left and right
|
|---|
| 479 | // we look for best split point
|
|---|
| 480 | // we split at the minimum similarity
|
|---|
| 481 | // here key space is separated the most
|
|---|
| 482 | // But we also need to make sure to not create too small groups
|
|---|
| 483 | let best = -1;
|
|---|
| 484 | let bestSimilarity = Infinity;
|
|---|
| 485 | let pos = left;
|
|---|
| 486 | const rightSize = sumSize(group.nodes.slice(pos));
|
|---|
| 487 |
|
|---|
| 488 | // pos v v right
|
|---|
| 489 | // [ O O O ] O O O [ O O O ]
|
|---|
| 490 | // ^^^^^^^^^ leftSize
|
|---|
| 491 | // rightSize ^^^^^^^^^^^^^^^
|
|---|
| 492 |
|
|---|
| 493 | while (pos <= right + 1) {
|
|---|
| 494 | const similarity =
|
|---|
| 495 | /** @type {Similarities} */
|
|---|
| 496 | (group.similarities)[pos - 1];
|
|---|
| 497 | if (
|
|---|
| 498 | similarity < bestSimilarity &&
|
|---|
| 499 | !isTooSmall(leftSize, minSize) &&
|
|---|
| 500 | !isTooSmall(rightSize, minSize)
|
|---|
| 501 | ) {
|
|---|
| 502 | best = pos;
|
|---|
| 503 | bestSimilarity = similarity;
|
|---|
| 504 | }
|
|---|
| 505 | addSizeTo(leftSize, group.nodes[pos].size);
|
|---|
| 506 | subtractSizeFrom(rightSize, group.nodes[pos].size);
|
|---|
| 507 | pos++;
|
|---|
| 508 | }
|
|---|
| 509 | if (best < 0) {
|
|---|
| 510 | // This can't happen
|
|---|
| 511 | // but if that assumption is wrong
|
|---|
| 512 | // fallback to a big group
|
|---|
| 513 | result.push(group);
|
|---|
| 514 | continue;
|
|---|
| 515 | }
|
|---|
| 516 | left = best;
|
|---|
| 517 | right = best - 1;
|
|---|
| 518 | }
|
|---|
| 519 |
|
|---|
| 520 | // create two new groups for left and right area
|
|---|
| 521 | // and queue them up
|
|---|
| 522 | /** @type {Node<T>[]} */
|
|---|
| 523 | const rightNodes = [group.nodes[right + 1]];
|
|---|
| 524 | /** @type {Similarities} */
|
|---|
| 525 | const rightSimilarities = [];
|
|---|
| 526 | for (let i = right + 2; i < group.nodes.length; i++) {
|
|---|
| 527 | rightSimilarities.push(
|
|---|
| 528 | /** @type {Similarities} */ (group.similarities)[i - 1]
|
|---|
| 529 | );
|
|---|
| 530 | rightNodes.push(group.nodes[i]);
|
|---|
| 531 | }
|
|---|
| 532 | queue.push(new Group(rightNodes, rightSimilarities));
|
|---|
| 533 |
|
|---|
| 534 | /** @type {Node<T>[]} */
|
|---|
| 535 | const leftNodes = [group.nodes[0]];
|
|---|
| 536 | /** @type {Similarities} */
|
|---|
| 537 | const leftSimilarities = [];
|
|---|
| 538 | for (let i = 1; i < left; i++) {
|
|---|
| 539 | leftSimilarities.push(
|
|---|
| 540 | /** @type {Similarities} */ (group.similarities)[i - 1]
|
|---|
| 541 | );
|
|---|
| 542 | leftNodes.push(group.nodes[i]);
|
|---|
| 543 | }
|
|---|
| 544 | queue.push(new Group(leftNodes, leftSimilarities));
|
|---|
| 545 | }
|
|---|
| 546 | }
|
|---|
| 547 | }
|
|---|
| 548 |
|
|---|
| 549 | // lexically ordering
|
|---|
| 550 | result.sort((a, b) => {
|
|---|
| 551 | if (a.nodes[0].key < b.nodes[0].key) return -1;
|
|---|
| 552 | if (a.nodes[0].key > b.nodes[0].key) return 1;
|
|---|
| 553 | return 0;
|
|---|
| 554 | });
|
|---|
| 555 |
|
|---|
| 556 | // give every group a name
|
|---|
| 557 | /** @type {Set<string>} */
|
|---|
| 558 | const usedNames = new Set();
|
|---|
| 559 | for (let i = 0; i < result.length; i++) {
|
|---|
| 560 | const group = result[i];
|
|---|
| 561 | if (group.nodes.length === 1) {
|
|---|
| 562 | group.key = group.nodes[0].key;
|
|---|
| 563 | } else {
|
|---|
| 564 | const first = group.nodes[0];
|
|---|
| 565 | const last = group.nodes[group.nodes.length - 1];
|
|---|
| 566 | const name = getName(first.key, last.key, usedNames);
|
|---|
| 567 | group.key = name;
|
|---|
| 568 | }
|
|---|
| 569 | }
|
|---|
| 570 |
|
|---|
| 571 | // return the results
|
|---|
| 572 | return result.map(
|
|---|
| 573 | (group) =>
|
|---|
| 574 | /** @type {GroupedItems<T>} */
|
|---|
| 575 | ({
|
|---|
| 576 | key: group.key,
|
|---|
| 577 | items: group.nodes.map((node) => node.item),
|
|---|
| 578 | size: group.size
|
|---|
| 579 | })
|
|---|
| 580 | );
|
|---|
| 581 | };
|
|---|