UNPKG

@openui5/sap.m

Version:

OpenUI5 UI Library sap.m

270 lines (250 loc) 8.51 kB
/*! * OpenUI5 * (c) Copyright 2026 SAP SE or an SAP affiliate company. * Licensed under the Apache License, Version 2.0 - see LICENSE.txt. */ sap.ui.define([ 'sap/base/util/deepEqual', 'sap/base/strings/hash' ], (deepEqual, hash) => { "use strict"; /** * Calculates delta of old list and new list. * * Note: This function is a modified copy of {@link module:sap/base/util/array/diff} * * See <SAP Wiki>/UI/SelectionController%3A+Move+Change+Creation+Algorithm * * This function implements the algorithm described in "A Technique for Isolating Differences Between Files" * (Commun. ACM, April 1978, Volume 21, Number 4, Pages 264-268). * * Items in the arrays are not compared directly. Instead, a substitute symbol is determined for each item * by applying the provided function <code>fnSymbol</code> to it. Items with strictly equal symbols are * assumed to represent the same logical item: * <pre> * fnSymbol(a) === fnSymbol(b) <=> a 'is logically the same as' b * </pre> * As an additional constraint, casting the symbols to string should not modify the comparison result. * If this second constraint is not met, this method might report more diffs than necessary. * * If no symbol function is provided, a default implementation is used which applies <code>JSON.stringify</code> * to non-string items and reduces the strings to a hash code. It is not guaranteed that this default * implementation fulfills the above constraint in all cases, but it is a compromise between implementation * effort, generality and performance. If items are known to be non-stringifiable (e.g. because they may * contain cyclic references) or when hash collisions are likely, an own <code>symbol</code> function * must be provided via the configuration object. * * The result of the diff is a sequence of update operations, each consisting of a <code>type</code> * (either <code>"insert"</code>, <code>"delete"</code> or <code>"replace"</code> when enabled via configuration object) * and an <code>index</code>. By applying the operations one after the other to the old array, it can be transformed to an * array whose items are equal to the new array. * * @example <caption>Sample implementation of the update</caption> * function update(aOldArray, aNewArray) { * * // calculate the diff * var aDiff = diff(aOldArray, aNewArray, __provide_your_configuration_object_here__); * * // apply update operations * aDiff.forEach( function(op) { * * // invariant: aOldArray and aNewArray now are equal up to (excluding) op.index * * switch ( op.type ) { * case 'insert': * // new array contains a new (or otherwise unmapped) item, add it here * aOldArray.splice(op.index, 0, aNewArray[op.index]); * break; * case 'delete': * // an item is no longer part of the array (or has been moved to another position), remove it * aOldArray.splice(op.index, 1); * break; * case 'replace': // available only when replace flag of the configuration object is set to true * // an item within the array has been changed, replace it * aOldArray[op.index] = aNewArray[op.index]; * break; * default: * throw new Error('unexpected diff operation type'); * } * * }); * } * * * @function * @since 1.137 * @param {Array} aOld Old Array * @param {Array} aNew New Array * @param {object|function} [vConfigOrSymbol] Configuration object or a function to calculate substitute symbols for array items * @param {function} [vConfigOrSymbol.symbol] Function to calculate substitute symbols for array items * @param {boolean} [vConfigOrSymbol.replace=false] Switch for the <code>replace</code> type which specifies that an item within the array has been changed * @param {boolean} [vConfigOrSymbol.includeReference=false] Includes the array reference and index to determine which item is affected * @alias module:sap/m/p13n/util/diff * @return {Array.<{type:string,index:int}>} List of update operations * @private * @ui5-restricted sap.m.p13n */ function fnDiff(aOld, aNew, vConfigOrSymbol) { const mSymbols = {}; const aOldRefs = []; const aNewRefs = []; let iOldLine; let vSymbol; let oSymbol; let fnSymbol; let iOld = 0; let iNew = 0; let iOldRefLine; let iNewRefLine; let iOldDistance; let iNewDistance; let bReplaceEnabled; let bIncludeReference; const aDiff = []; // If arrays are equal, don't try to diff them if (aOld === aNew || deepEqual(aOld, aNew)) { return aDiff; } // Assign configurations if (!vConfigOrSymbol || typeof vConfigOrSymbol == "function") { fnSymbol = vConfigOrSymbol; } else { fnSymbol = vConfigOrSymbol.symbol; bReplaceEnabled = vConfigOrSymbol.replace; bIncludeReference = vConfigOrSymbol.includeReference; } // If no symbol function is provided, we stringify, if it is not type string, and create a hash from it fnSymbol = fnSymbol || function(vValue) { if (typeof vValue !== "string") { vValue = JSON.stringify(vValue) || ""; } return hash(vValue); }; // Pass 1 for (let i = 0; i < aNew.length; i++) { vSymbol = fnSymbol(aNew[i]); oSymbol = mSymbols[vSymbol]; if (!oSymbol) { oSymbol = mSymbols[vSymbol] = { iNewCount: 0, iOldCount: 0 }; } oSymbol.iNewCount++; aNewRefs[i] = { symbol: oSymbol }; } // Pass 2 for (let i = 0; i < aOld.length; i++) { vSymbol = fnSymbol(aOld[i]); oSymbol = mSymbols[vSymbol]; if (!oSymbol) { oSymbol = mSymbols[vSymbol] = { iNewCount: 0, iOldCount: 0 }; } oSymbol.iOldCount++; oSymbol.iOldLine = i; aOldRefs[i] = { symbol: oSymbol }; } // Pass 3 for (let i = 0; i < aNewRefs.length; i++) { oSymbol = aNewRefs[i].symbol; if (oSymbol.iNewCount === 1 && oSymbol.iOldCount === 1) { aNewRefs[i].line = oSymbol.iOldLine; aOldRefs[oSymbol.iOldLine].line = i; } } // Pass 4 for (let i = 0; i < aNewRefs.length - 1; i++) { iOldLine = aNewRefs[i].line; if (iOldLine !== undefined && iOldLine < aOldRefs.length - 1) { if (aOldRefs[iOldLine + 1].symbol === aNewRefs[i + 1].symbol) { aOldRefs[iOldLine + 1].line = i + 1; aNewRefs[i + 1].line = iOldLine + 1; } } } // Pass 5 for (let i = aNewRefs.length - 1; i > 0; i--) { iOldLine = aNewRefs[i].line; if (iOldLine !== undefined && iOldLine > 0) { if (aOldRefs[iOldLine - 1].symbol === aNewRefs[i - 1].symbol) { aOldRefs[iOldLine - 1].line = i - 1; aNewRefs[i - 1].line = iOldLine - 1; } } } // Create diff while (iOld < aOld.length || iNew < aNew.length) { iNewRefLine = aOldRefs[iOld] && aOldRefs[iOld].line; iOldRefLine = aNewRefs[iNew] && aNewRefs[iNew].line; if (bReplaceEnabled && iNewRefLine === undefined && iOldRefLine === undefined && iOld < aOld.length && iNew < aNew.length) { aDiff.push({ index: iNew, type: "replace" }); iOld++; iNew++; } else if (iOld < aOld.length && (iNewRefLine === undefined || iNewRefLine < iNew)) { const oDiffEntry = { index: iNew, type: "delete" }; if (bIncludeReference) { oDiffEntry.affectedIndex = aOldRefs[iOld].symbol.iOldLine; oDiffEntry.affectedReference = aOld; } aDiff.push(oDiffEntry); iOld++; } else if (iNew < aNew.length && (iOldRefLine === undefined || iOldRefLine < iOld)) { const oDiffEntry = { index: iNew, type: "insert" }; if (bIncludeReference) { oDiffEntry.affectedIndex = iNew; oDiffEntry.affectedReference = aNew; } aDiff.push(oDiffEntry); iNew++; } else if (iNew === iNewRefLine) { iNew++; iOld++; } else { iNewDistance = iNewRefLine - iNew; iOldDistance = iOldRefLine - iOld; if (iNewDistance <= iOldDistance) { const oDiffEntry = { index: iNew, type: "insert" }; if (bIncludeReference) { oDiffEntry.affectedIndex = iNew; oDiffEntry.affectedReference = aNew; } aDiff.push(oDiffEntry); iNew++; } else { const oDiffEntry = { index: iNew, type: "delete" }; if (bIncludeReference) { oDiffEntry.affectedIndex = aOldRefs[iOld].symbol.iOldLine; oDiffEntry.affectedReference = aOld; } aDiff.push(oDiffEntry); iOld++; } } } return aDiff; } return fnDiff; });