enhanceable.js
1 import emptyStack from '@iter-tools/imm-stack'; 2 3 const { isArray } = Array; 4 const { freeze: freezeObject, isFrozen } = Object; 5 const { isFinite } = Number; 6 7 export const buildModule = (NODE_SIZE = 8) => { 8 const sumNodes = (nodes) => { 9 return nodes.map(getSum).reduce((a, b) => a + b, 0); 10 }; 11 12 const treeFrom = (...trees) => [sumNodes(trees), trees]; 13 14 const from = (...values) => { 15 let tree = []; 16 for (const value of values) { 17 tree = push(tree, value); 18 } 19 return tree; 20 }; 21 22 const fromValues = (values) => { 23 let tree = []; 24 for (const value of values) { 25 tree = push(tree, value); 26 } 27 return tree; 28 }; 29 30 const findBalancePoint = (tree) => { 31 const values = isFinite(tree[0]) ? tree[1] : tree; 32 let leftSum = 0; 33 let rightSum = getSum(tree); 34 let balance = leftSum / rightSum; 35 36 if (!values.length) return null; 37 if (values.length === 1) return 1; 38 39 for (let i = 1; i < values.length; i++) { 40 const sum = getSum(values[i - 1]); 41 const lastBalance = balance; 42 leftSum += sum; 43 rightSum -= sum; 44 45 balance = leftSum / rightSum; 46 47 if (lastBalance < 1 && balance >= 1) { 48 return i; 49 } 50 } 51 return values.length - 1; 52 }; 53 54 const split = (tree) => { 55 const values = isFinite(tree[0]) ? tree[1] : tree; 56 const isLeaf = isLeafNode(tree); 57 58 let midIndex; 59 60 if (isLeaf) { 61 midIndex = Math.floor(values.length / 2 + 0.01); 62 } else { 63 midIndex = findBalancePoint(tree); 64 } 65 66 let leftValues = values.slice(0, midIndex); 67 let rightValues = values.slice(midIndex); 68 69 let left = isLeaf ? leftValues : [sumNodes(leftValues), leftValues]; 70 let right = isLeaf ? rightValues : [sumNodes(rightValues), rightValues]; 71 72 return { left, right }; 73 }; 74 75 const setValuesAt = (idx, node, value) => { 76 const isLeaf = isLeafNode(node); 77 const values = isLeaf ? node : node[1]; 78 79 if (!isLeaf && !isArray(value)) { 80 throw new Error(); 81 } 82 83 const oldSize = isLeaf ? 1 : values[idx] ? getSum(values[idx]) : 0; 84 const newSize = isLeaf ? 1 : getSum(value); 85 86 // TODO mutable sets? 87 88 const newValues = [...values]; 89 newValues[idx] = value; 90 freezeObject(newValues); 91 return isLeaf ? newValues : freezeObject([node[0] + newSize - oldSize, newValues]); 92 }; 93 94 const addAt = (idx, tree, value) => { 95 if (idx < 0) throw new Error('invalid argument'); 96 97 let path = findPath(idx, tree); 98 99 let { node, index } = path.value; 100 101 // left pushout vs right pushout? 102 let pushout = value; 103 104 let values = [...getValues(node)]; 105 106 for (;;) { 107 if (pushout) { 108 values = [...values]; 109 if (values.length + getValues(pushout).length > NODE_SIZE) { 110 values.splice(index, 0, pushout); 111 node = setValues(node, values); 112 const { left, right } = split(node); 113 114 pushout = left; 115 node = right; 116 } else { 117 values.splice(index, 0, pushout); 118 node = setValues(node, values); 119 pushout = null; 120 } 121 } 122 123 if (path.size === 1) { 124 if (pushout) { 125 return treeFrom(pushout, node); 126 } else { 127 return node; 128 } 129 } 130 131 const poppedNode = node; 132 path = path.pop(); 133 ({ node, index } = path.value); 134 135 node = setValuesAt(index, node, poppedNode); 136 values = getValues(node); 137 path = path.replace({ node, index }); 138 } 139 }; 140 141 const push = (tree, value) => { 142 return addAt(getSum(tree), tree, value); 143 }; 144 145 const collapses = (size) => { 146 return size > NODE_SIZE / 2 + 0.01; 147 }; 148 149 const nodeCollapses = (node) => { 150 return collapses(getValues(node).length); 151 }; 152 153 const nodeCanDonate = (node) => { 154 return collapses(getValues(node).length - 1); 155 }; 156 157 const pop = (tree) => { 158 let path = findPath(-1, tree); 159 160 let { node, index } = path.value; 161 162 const initialValues = [...getValues(node)].slice(0, -1); 163 let returnValue = isLeafNode(node) ? initialValues : [sumNodes(initialValues), initialValues]; 164 165 for (;;) { 166 let values = getValues(returnValue); 167 let adjustSibling = null; 168 169 if (path.size > 1 && nodeCollapses(returnValue)) { 170 let { node: parentNode, index: parentIndex } = path.prev.value; 171 const prevSibling = getValues(parentNode)[parentIndex - 1]; 172 const nextSibling = getValues(parentNode)[parentIndex + 1]; 173 let targetSibling = nodeCanDonate(prevSibling) 174 ? prevSibling 175 : nodeCanDonate(nextSibling) 176 ? nextSibling 177 : null; 178 let targetSiblingIndex = targetSibling && (prevSibling ? parentIndex - 1 : parentIndex + 1); 179 180 if (targetSibling) { 181 let targetValues = [...getValues(targetSibling)]; 182 183 const donationIdx = targetSibling === prevSibling ? targetValues.length - 1 : 0; 184 const donated = targetValues[donationIdx]; 185 targetValues.splice(donationIdx, 1); 186 187 adjustSibling = { 188 node: setValues(targetSibling, targetValues), 189 index: targetSiblingIndex, 190 }; 191 192 values = [...values]; 193 194 values.splice(targetSibling === prevSibling ? values.length : 0, 0, donated); 195 196 returnValue = setValues(returnValue, values); 197 } 198 } 199 200 if (path.size === 1) { 201 if (isFinite(returnValue[0]) && getSum(returnValue) <= NODE_SIZE) { 202 returnValue = returnValue[1].flat(); 203 } 204 return returnValue; 205 } 206 207 path = path.pop(); 208 ({ node, index } = path.value); 209 210 values = [...getValues(node)]; 211 212 values.splice(index, 1, returnValue); 213 214 if (adjustSibling) { 215 const { index, node } = adjustSibling; 216 values.splice(index, 1, ...(node ? [node] : [])); 217 } 218 219 returnValue = node = setValues(node, values); 220 } 221 }; 222 223 const isValidNode = (node) => { 224 if (!isArray(node)) return false; 225 const values = isFinite(node[0]) ? node[1] : node; 226 return isArray(values); // && values.length <= NODE_SIZE; 227 }; 228 229 const assertValidNode = (node) => { 230 if (!isValidNode(node)) throw new Error(); 231 return true; 232 }; 233 234 const getValues = (node) => { 235 return node ? (isArray(node) ? (isFinite(node[0]) ? node[1] : node) : [node]) : []; 236 }; 237 238 const setValues = (node, values) => { 239 return isFinite(node[0]) ? treeFrom(...values) : values; 240 }; 241 242 const isLeafNode = (node) => { 243 return isArray(node) && !isFinite(node[0]); 244 }; 245 246 function* traverse(tree) { 247 let states = emptyStack.push({ node: tree, i: 0 }); 248 249 assertValidNode(tree); 250 251 stack: while (states.size) { 252 const s = states.value; 253 const { node } = s; 254 const isLeaf = isLeafNode(node); 255 256 const values = isLeaf ? node : node[1]; 257 258 if (isLeaf) { 259 for (let i = 0; i < values.length; i++) { 260 yield values[i]; 261 } 262 } else { 263 for (let { i } = s; s.i < values.length; ) { 264 const node = values[i]; 265 assertValidNode(node); 266 states = states.push({ node, i: 0 }); 267 i = ++s.i; 268 continue stack; 269 } 270 } 271 states = states.pop(); 272 } 273 } 274 275 const getSum = (tree) => { 276 if (!isArray(tree)) { 277 return 1; 278 } else if (isFinite(tree[0])) { 279 return tree[0]; 280 } else { 281 return tree.length; 282 } 283 }; 284 285 function* indexes(count, backwards = false) { 286 const increment = backwards ? -1 : 1; 287 for (let i = backwards ? count - 1 : 0; backwards ? i >= 0 : i < count; i += increment) { 288 yield i; 289 } 290 } 291 292 const findPath = (idx, tree) => { 293 let path = emptyStack; 294 295 let treeSum = getSum(tree); 296 let currentIdx = idx < 0 ? treeSum : 0; 297 let direction = idx < 0 ? -1 : 1; 298 let targetIdx = idx < 0 ? treeSum + idx : idx; 299 300 let node = tree; 301 stack: while (node) { 302 assertValidNode(node); 303 304 if (isLeafNode(node)) { 305 const startIdx = idx < 0 ? currentIdx - getSum(node) : currentIdx; 306 let index = isFinite(currentIdx) ? targetIdx - startIdx : currentIdx; 307 if (index < 0) { 308 index = -Infinity; 309 } else if (index >= node.length) { 310 index = Infinity; 311 } 312 return path.push({ index, node }); 313 } else { 314 const values = node[1]; 315 let candidateNode; 316 let i; 317 318 for (i of indexes(values.length, idx < 0)) { 319 candidateNode = values[i]; 320 const sum = getSum(candidateNode); 321 const nextCount = currentIdx + sum * direction; 322 if (idx < 0 ? nextCount <= targetIdx : nextCount > targetIdx || nextCount >= treeSum) { 323 path = path.push({ index: i, node }); 324 node = candidateNode; 325 continue stack; 326 } else { 327 currentIdx += sum * direction; 328 } 329 } 330 } 331 } 332 333 return null; 334 }; 335 336 const getAt = (idx, tree) => { 337 const v = findPath(idx, tree)?.value; 338 return v && v.node[v.index]; 339 }; 340 341 const replaceAt = (idx, tree, value) => { 342 let path = findPath(idx, tree); 343 344 let { node, index } = path.value; 345 346 let returnValue = setValuesAt(index, node, value); 347 348 for (;;) { 349 ({ node, index } = path.value); 350 351 if (path.size > 1) { 352 path = path.pop(); 353 ({ node, index } = path.value); 354 355 returnValue = setValuesAt(index, node, returnValue); 356 } else { 357 return returnValue; 358 } 359 } 360 }; 361 362 const freeze = (tree) => { 363 let states = emptyStack.push({ node: tree, i: 0 }); 364 365 stack: while (states.size) { 366 const s = states.value; 367 const { node } = s; 368 const isLeaf = isLeafNode(node); 369 370 assertValidNode(node); 371 372 freezeObject(node); 373 374 const values = isLeaf ? node : node[1]; 375 376 if (!isLeaf) { 377 freezeObject(values); 378 379 for (let { i } = s; s.i < values.length; ) { 380 const node = values[i]; 381 assertValidNode(node); 382 states = states.push({ node, i: 0 }); 383 i = ++s.i; 384 continue stack; 385 } 386 } 387 states = states.pop(); 388 } 389 return tree; 390 }; 391 392 return { 393 buildModule, 394 from, 395 fromValues, 396 findBalancePoint, 397 split, 398 collapses, 399 nodeCollapses, 400 nodeCanDonate, 401 pop, 402 push, 403 addAt, 404 isValidNode, 405 assertValidNode, 406 getValues, 407 setValues, 408 isLeafNode, 409 traverse, 410 getSum, 411 findPath, 412 getAt, 413 replaceAt, 414 freeze, 415 }; 416 };