import { DiffChange } from './diffChange.js';
import { stringHash } from '../hash.js';
import { Constants } from '../uint.js';
export class StringDiffSequence implements ISequence {
constructor(private source: string) { }
getElements(): Int32Array | number[] | string[] {
const source = this.source;
const characters = new Int32Array(source.length);
for (let i = 0, len = source.length; i < len; i++) {
characters[i] = source.charCodeAt(i);
}
return characters;
}
}
export function stringDiff(original: string, modified: string, pretty: boolean): IDiffChange[] {
return new LcsDiff(new StringDiffSequence(original), new StringDiffSequence(modified)).ComputeDiff(pretty).changes;
}
export interface ISequence {
getElements(): Int32Array | number[] | string[];
getStrictElement?(index: number): string;
}
export interface IDiffChange {
originalStart: number;
originalLength: number;
modifiedStart: number;
modifiedLength: number;
}
export interface IContinueProcessingPredicate {
(furthestOriginalIndex: number, matchLengthOfLongest: number): boolean;
}
export interface IDiffResult {
quitEarly: boolean;
changes: IDiffChange[];
}
class Debug {
public static Assert(condition: boolean, message: string): void {
if (!condition) {
throw new Error(message);
}
}
}
class MyArray {
public static Copy(sourceArray: unknown[], sourceIndex: number, destinationArray: unknown[], destinationIndex: number, length: number) {
for (let i = 0; i < length; i++) {
destinationArray[destinationIndex + i] = sourceArray[sourceIndex + i];
}
}
public static Copy2(sourceArray: Int32Array, sourceIndex: number, destinationArray: Int32Array, destinationIndex: number, length: number) {
for (let i = 0; i < length; i++) {
destinationArray[destinationIndex + i] = sourceArray[sourceIndex + i];
}
}
}
const enum LocalConstants {
MaxDifferencesHistory = 1447
}
class DiffChangeHelper {
private m_changes: DiffChange[];
private m_originalStart: number;
private m_modifiedStart: number;
private m_originalCount: number;
private m_modifiedCount: number;
constructor() {
this.m_changes = [];
this.m_originalStart = Constants.MAX_SAFE_SMALL_INTEGER;
this.m_modifiedStart = Constants.MAX_SAFE_SMALL_INTEGER;
this.m_originalCount = 0;
this.m_modifiedCount = 0;
}
public MarkNextChange(): void {
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
this.m_changes.push(new DiffChange(this.m_originalStart, this.m_originalCount,
this.m_modifiedStart, this.m_modifiedCount));
}
this.m_originalCount = 0;
this.m_modifiedCount = 0;
this.m_originalStart = Constants.MAX_SAFE_SMALL_INTEGER;
this.m_modifiedStart = Constants.MAX_SAFE_SMALL_INTEGER;
}
public AddOriginalElement(originalIndex: number, modifiedIndex: number) {
this.m_originalStart = Math.min(this.m_originalStart, originalIndex);
this.m_modifiedStart = Math.min(this.m_modifiedStart, modifiedIndex);
this.m_originalCount++;
}
public AddModifiedElement(originalIndex: number, modifiedIndex: number): void {
this.m_originalStart = Math.min(this.m_originalStart, originalIndex);
this.m_modifiedStart = Math.min(this.m_modifiedStart, modifiedIndex);
this.m_modifiedCount++;
}
public getChanges(): DiffChange[] {
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
this.MarkNextChange();
}
return this.m_changes;
}
public getReverseChanges(): DiffChange[] {
if (this.m_originalCount > 0 || this.m_modifiedCount > 0) {
this.MarkNextChange();
}
this.m_changes.reverse();
return this.m_changes;
}
}
export class LcsDiff {
private readonly ContinueProcessingPredicate: IContinueProcessingPredicate | null;
private readonly _originalSequence: ISequence;
private readonly _modifiedSequence: ISequence;
private readonly _hasStrings: boolean;
private readonly _originalStringElements: string[];
private readonly _originalElementsOrHash: Int32Array;
private readonly _modifiedStringElements: string[];
private readonly _modifiedElementsOrHash: Int32Array;
private m_forwardHistory: Int32Array[];
private m_reverseHistory: Int32Array[];
constructor(originalSequence: ISequence, modifiedSequence: ISequence, continueProcessingPredicate: IContinueProcessingPredicate | null = null) {
this.ContinueProcessingPredicate = continueProcessingPredicate;
this._originalSequence = originalSequence;
this._modifiedSequence = modifiedSequence;
const [originalStringElements, originalElementsOrHash, originalHasStrings] = LcsDiff._getElements(originalSequence);
const [modifiedStringElements, modifiedElementsOrHash, modifiedHasStrings] = LcsDiff._getElements(modifiedSequence);
this._hasStrings = (originalHasStrings && modifiedHasStrings);
this._originalStringElements = originalStringElements;
this._originalElementsOrHash = originalElementsOrHash;
this._modifiedStringElements = modifiedStringElements;
this._modifiedElementsOrHash = modifiedElementsOrHash;
this.m_forwardHistory = [];
this.m_reverseHistory = [];
}
private static _isStringArray(arr: Int32Array | number[] | string[]): arr is string[] {
return (arr.length > 0 && typeof arr[0] === 'string');
}
private static _getElements(sequence: ISequence): [string[], Int32Array, boolean] {
const elements = sequence.getElements();
if (LcsDiff._isStringArray(elements)) {
const hashes = new Int32Array(elements.length);
for (let i = 0, len = elements.length; i < len; i++) {
hashes[i] = stringHash(elements[i], 0);
}
return [elements, hashes, true];
}
if (elements instanceof Int32Array) {
return [[], elements, false];
}
return [[], new Int32Array(elements), false];
}
private ElementsAreEqual(originalIndex: number, newIndex: number): boolean {
if (this._originalElementsOrHash[originalIndex] !== this._modifiedElementsOrHash[newIndex]) {
return false;
}
return (this._hasStrings ? this._originalStringElements[originalIndex] === this._modifiedStringElements[newIndex] : true);
}
private ElementsAreStrictEqual(originalIndex: number, newIndex: number): boolean {
if (!this.ElementsAreEqual(originalIndex, newIndex)) {
return false;
}
const originalElement = LcsDiff._getStrictElement(this._originalSequence, originalIndex);
const modifiedElement = LcsDiff._getStrictElement(this._modifiedSequence, newIndex);
return (originalElement === modifiedElement);
}
private static _getStrictElement(sequence: ISequence, index: number): string | null {
if (typeof sequence.getStrictElement === 'function') {
return sequence.getStrictElement(index);
}
return null;
}
private OriginalElementsAreEqual(index1: number, index2: number): boolean {
if (this._originalElementsOrHash[index1] !== this._originalElementsOrHash[index2]) {
return false;
}
return (this._hasStrings ? this._originalStringElements[index1] === this._originalStringElements[index2] : true);
}
private ModifiedElementsAreEqual(index1: number, index2: number): boolean {
if (this._modifiedElementsOrHash[index1] !== this._modifiedElementsOrHash[index2]) {
return false;
}
return (this._hasStrings ? this._modifiedStringElements[index1] === this._modifiedStringElements[index2] : true);
}
public ComputeDiff(pretty: boolean): IDiffResult {
return this._ComputeDiff(0, this._originalElementsOrHash.length - 1, 0, this._modifiedElementsOrHash.length - 1, pretty);
}
private _ComputeDiff(originalStart: number, originalEnd: number, modifiedStart: number, modifiedEnd: number, pretty: boolean): IDiffResult {
const quitEarlyArr = [false];
let changes = this.ComputeDiffRecursive(originalStart, originalEnd, modifiedStart, modifiedEnd, quitEarlyArr);
if (pretty) {
changes = this.PrettifyChanges(changes);
}
return {
quitEarly: quitEarlyArr[0],
changes: changes
};
}
private ComputeDiffRecursive(originalStart: number, originalEnd: number, modifiedStart: number, modifiedEnd: number, quitEarlyArr: boolean[]): DiffChange[] {
quitEarlyArr[0] = false;
while (originalStart <= originalEnd && modifiedStart <= modifiedEnd && this.ElementsAreEqual(originalStart, modifiedStart)) {
originalStart++;
modifiedStart++;
}
while (originalEnd >= originalStart && modifiedEnd >= modifiedStart && this.ElementsAreEqual(originalEnd, modifiedEnd)) {
originalEnd--;
modifiedEnd--;
}
if (originalStart > originalEnd || modifiedStart > modifiedEnd) {
let changes: DiffChange[];
if (modifiedStart <= modifiedEnd) {
Debug.Assert(originalStart === originalEnd + 1, 'originalStart should only be one more than originalEnd');
changes = [
new DiffChange(originalStart, 0, modifiedStart, modifiedEnd - modifiedStart + 1)
];
} else if (originalStart <= originalEnd) {
Debug.Assert(modifiedStart === modifiedEnd + 1, 'modifiedStart should only be one more than modifiedEnd');
changes = [
new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, 0)
];
} else {
Debug.Assert(originalStart === originalEnd + 1, 'originalStart should only be one more than originalEnd');
Debug.Assert(modifiedStart === modifiedEnd + 1, 'modifiedStart should only be one more than modifiedEnd');
changes = [];
}
return changes;
}
const midOriginalArr = [0];
const midModifiedArr = [0];
const result = this.ComputeRecursionPoint(originalStart, originalEnd, modifiedStart, modifiedEnd, midOriginalArr, midModifiedArr, quitEarlyArr);
const midOriginal = midOriginalArr[0];
const midModified = midModifiedArr[0];
if (result !== null) {
return result;
} else if (!quitEarlyArr[0]) {
const leftChanges = this.ComputeDiffRecursive(originalStart, midOriginal, modifiedStart, midModified, quitEarlyArr);
let rightChanges: DiffChange[] = [];
if (!quitEarlyArr[0]) {
rightChanges = this.ComputeDiffRecursive(midOriginal + 1, originalEnd, midModified + 1, modifiedEnd, quitEarlyArr);
} else {
rightChanges = [
new DiffChange(midOriginal + 1, originalEnd - (midOriginal + 1) + 1, midModified + 1, modifiedEnd - (midModified + 1) + 1)
];
}
return this.ConcatenateChanges(leftChanges, rightChanges);
}
return [
new DiffChange(originalStart, originalEnd - originalStart + 1, modifiedStart, modifiedEnd - modifiedStart + 1)
];
}
private WALKTRACE(diagonalForwardBase: number, diagonalForwardStart: number, diagonalForwardEnd: number, diagonalForwardOffset: number,
diagonalReverseBase: number, diagonalReverseStart: number, diagonalReverseEnd: number, diagonalReverseOffset: number,
forwardPoints: Int32Array, reversePoints: Int32Array,
originalIndex: number, originalEnd: number, midOriginalArr: number[],
modifiedIndex: number, modifiedEnd: number, midModifiedArr: number[],
deltaIsEven: boolean, quitEarlyArr: boolean[]
): DiffChange[] {
let forwardChanges: DiffChange[] | null = null;
let reverseChanges: DiffChange[] | null = null;
let changeHelper = new DiffChangeHelper();
let diagonalMin = diagonalForwardStart;
let diagonalMax = diagonalForwardEnd;
let diagonalRelative = (midOriginalArr[0] - midModifiedArr[0]) - diagonalForwardOffset;
let lastOriginalIndex = Constants.MIN_SAFE_SMALL_INTEGER;
let historyIndex = this.m_forwardHistory.length - 1;
do {
const diagonal = diagonalRelative + diagonalForwardBase;
if (diagonal === diagonalMin || (diagonal < diagonalMax && forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])) {
originalIndex = forwardPoints[diagonal + 1];
modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset;
if (originalIndex < lastOriginalIndex) {
changeHelper.MarkNextChange();
}
lastOriginalIndex = originalIndex;
changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex);
diagonalRelative = (diagonal + 1) - diagonalForwardBase;
} else {
originalIndex = forwardPoints[diagonal - 1] + 1;
modifiedIndex = originalIndex - diagonalRelative - diagonalForwardOffset;
if (originalIndex < lastOriginalIndex) {
changeHelper.MarkNextChange();
}
lastOriginalIndex = originalIndex - 1;
changeHelper.AddOriginalElement(originalIndex, modifiedIndex + 1);
diagonalRelative = (diagonal - 1) - diagonalForwardBase;
}
if (historyIndex >= 0) {
forwardPoints = this.m_forwardHistory[historyIndex];
diagonalForwardBase = forwardPoints[0];
diagonalMin = 1;
diagonalMax = forwardPoints.length - 1;
}
} while (--historyIndex >= -1);
forwardChanges = changeHelper.getReverseChanges();
if (quitEarlyArr[0]) {
let originalStartPoint = midOriginalArr[0] + 1;
let modifiedStartPoint = midModifiedArr[0] + 1;
if (forwardChanges !== null && forwardChanges.length > 0) {
const lastForwardChange = forwardChanges[forwardChanges.length - 1];
originalStartPoint = Math.max(originalStartPoint, lastForwardChange.getOriginalEnd());
modifiedStartPoint = Math.max(modifiedStartPoint, lastForwardChange.getModifiedEnd());
}
reverseChanges = [
new DiffChange(originalStartPoint, originalEnd - originalStartPoint + 1,
modifiedStartPoint, modifiedEnd - modifiedStartPoint + 1)
];
} else {
changeHelper = new DiffChangeHelper();
diagonalMin = diagonalReverseStart;
diagonalMax = diagonalReverseEnd;
diagonalRelative = (midOriginalArr[0] - midModifiedArr[0]) - diagonalReverseOffset;
lastOriginalIndex = Constants.MAX_SAFE_SMALL_INTEGER;
historyIndex = (deltaIsEven) ? this.m_reverseHistory.length - 1 : this.m_reverseHistory.length - 2;
do {
const diagonal = diagonalRelative + diagonalReverseBase;
if (diagonal === diagonalMin || (diagonal < diagonalMax && reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])) {
originalIndex = reversePoints[diagonal + 1] - 1;
modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset;
if (originalIndex > lastOriginalIndex) {
changeHelper.MarkNextChange();
}
lastOriginalIndex = originalIndex + 1;
changeHelper.AddOriginalElement(originalIndex + 1, modifiedIndex + 1);
diagonalRelative = (diagonal + 1) - diagonalReverseBase;
} else {
originalIndex = reversePoints[diagonal - 1];
modifiedIndex = originalIndex - diagonalRelative - diagonalReverseOffset;
if (originalIndex > lastOriginalIndex) {
changeHelper.MarkNextChange();
}
lastOriginalIndex = originalIndex;
changeHelper.AddModifiedElement(originalIndex + 1, modifiedIndex + 1);
diagonalRelative = (diagonal - 1) - diagonalReverseBase;
}
if (historyIndex >= 0) {
reversePoints = this.m_reverseHistory[historyIndex];
diagonalReverseBase = reversePoints[0];
diagonalMin = 1;
diagonalMax = reversePoints.length - 1;
}
} while (--historyIndex >= -1);
reverseChanges = changeHelper.getChanges();
}
return this.ConcatenateChanges(forwardChanges, reverseChanges);
}
private ComputeRecursionPoint(originalStart: number, originalEnd: number, modifiedStart: number, modifiedEnd: number, midOriginalArr: number[], midModifiedArr: number[], quitEarlyArr: boolean[]) {
let originalIndex = 0, modifiedIndex = 0;
let diagonalForwardStart = 0, diagonalForwardEnd = 0;
let diagonalReverseStart = 0, diagonalReverseEnd = 0;
originalStart--;
modifiedStart--;
midOriginalArr[0] = 0;
midModifiedArr[0] = 0;
this.m_forwardHistory = [];
this.m_reverseHistory = [];
const maxDifferences = (originalEnd - originalStart) + (modifiedEnd - modifiedStart);
const numDiagonals = maxDifferences + 1;
const forwardPoints = new Int32Array(numDiagonals);
const reversePoints = new Int32Array(numDiagonals);
const diagonalForwardBase = (modifiedEnd - modifiedStart);
const diagonalReverseBase = (originalEnd - originalStart);
const diagonalForwardOffset = (originalStart - modifiedStart);
const diagonalReverseOffset = (originalEnd - modifiedEnd);
const delta = diagonalReverseBase - diagonalForwardBase;
const deltaIsEven = (delta % 2 === 0);
forwardPoints[diagonalForwardBase] = originalStart;
reversePoints[diagonalReverseBase] = originalEnd;
quitEarlyArr[0] = false;
for (let numDifferences = 1; numDifferences <= (maxDifferences / 2) + 1; numDifferences++) {
let furthestOriginalIndex = 0;
let furthestModifiedIndex = 0;
diagonalForwardStart = this.ClipDiagonalBound(diagonalForwardBase - numDifferences, numDifferences, diagonalForwardBase, numDiagonals);
diagonalForwardEnd = this.ClipDiagonalBound(diagonalForwardBase + numDifferences, numDifferences, diagonalForwardBase, numDiagonals);
for (let diagonal = diagonalForwardStart; diagonal <= diagonalForwardEnd; diagonal += 2) {
if (diagonal === diagonalForwardStart || (diagonal < diagonalForwardEnd && forwardPoints[diagonal - 1] < forwardPoints[diagonal + 1])) {
originalIndex = forwardPoints[diagonal + 1];
} else {
originalIndex = forwardPoints[diagonal - 1] + 1;
}
modifiedIndex = originalIndex - (diagonal - diagonalForwardBase) - diagonalForwardOffset;
const tempOriginalIndex = originalIndex;
while (originalIndex < originalEnd && modifiedIndex < modifiedEnd && this.ElementsAreEqual(originalIndex + 1, modifiedIndex + 1)) {
originalIndex++;
modifiedIndex++;
}
forwardPoints[diagonal] = originalIndex;
if (originalIndex + modifiedIndex > furthestOriginalIndex + furthestModifiedIndex) {
furthestOriginalIndex = originalIndex;
furthestModifiedIndex = modifiedIndex;
}
if (!deltaIsEven && Math.abs(diagonal - diagonalReverseBase) <= (numDifferences - 1)) {
if (originalIndex >= reversePoints[diagonal]) {
midOriginalArr[0] = originalIndex;
midModifiedArr[0] = modifiedIndex;
if (tempOriginalIndex <= reversePoints[diagonal] && LocalConstants.MaxDifferencesHistory > 0 && numDifferences <= (LocalConstants.MaxDifferencesHistory + 1)) {
return this.WALKTRACE(diagonalForwardBase, diagonalForwardStart, diagonalForwardEnd, diagonalForwardOffset,
diagonalReverseBase, diagonalReverseStart, diagonalReverseEnd, diagonalReverseOffset,
forwardPoints, reversePoints,
originalIndex, originalEnd, midOriginalArr,
modifiedIndex, modifiedEnd, midModifiedArr,
deltaIsEven, quitEarlyArr
);
} else {
return null;
}
}
}
}
const matchLengthOfLongest = ((furthestOriginalIndex - originalStart) + (furthestModifiedIndex - modifiedStart) - numDifferences) / 2;
if (this.ContinueProcessingPredicate !== null && !this.ContinueProcessingPredicate(furthestOriginalIndex, matchLengthOfLongest)) {
quitEarlyArr[0] = true;
midOriginalArr[0] = furthestOriginalIndex;
midModifiedArr[0] = furthestModifiedIndex;
if (matchLengthOfLongest > 0 && LocalConstants.MaxDifferencesHistory > 0 && numDifferences <= (LocalConstants.MaxDifferencesHistory + 1)) {
return this.WALKTRACE(diagonalForwardBase, diagonalForwardStart, diagonalForwardEnd, diagonalForwardOffset,
diagonalReverseBase, diagonalReverseStart, diagonalReverseEnd, diagonalReverseOffset,
forwardPoints, reversePoints,
originalIndex, originalEnd, midOriginalArr,
modifiedIndex, modifiedEnd, midModifiedArr,
deltaIsEven, quitEarlyArr
);
} else {
originalStart++;
modifiedStart++;
return [
new DiffChange(originalStart, originalEnd - originalStart + 1,
modifiedStart, modifiedEnd - modifiedStart + 1)
];
}
}
diagonalReverseStart = this.ClipDiagonalBound(diagonalReverseBase - numDifferences, numDifferences, diagonalReverseBase, numDiagonals);
diagonalReverseEnd = this.ClipDiagonalBound(diagonalReverseBase + numDifferences, numDifferences, diagonalReverseBase, numDiagonals);
for (let diagonal = diagonalReverseStart; diagonal <= diagonalReverseEnd; diagonal += 2) {
if (diagonal === diagonalReverseStart || (diagonal < diagonalReverseEnd && reversePoints[diagonal - 1] >= reversePoints[diagonal + 1])) {
originalIndex = reversePoints[diagonal + 1] - 1;
} else {
originalIndex = reversePoints[diagonal - 1];
}
modifiedIndex = originalIndex - (diagonal - diagonalReverseBase) - diagonalReverseOffset;
const tempOriginalIndex = originalIndex;
while (originalIndex > originalStart && modifiedIndex > modifiedStart && this.ElementsAreEqual(originalIndex, modifiedIndex)) {
originalIndex--;
modifiedIndex--;
}
reversePoints[diagonal] = originalIndex;
if (deltaIsEven && Math.abs(diagonal - diagonalForwardBase) <= numDifferences) {
if (originalIndex <= forwardPoints[diagonal]) {
midOriginalArr[0] = originalIndex;
midModifiedArr[0] = modifiedIndex;
if (tempOriginalIndex >= forwardPoints[diagonal] && LocalConstants.MaxDifferencesHistory > 0 && numDifferences <= (LocalConstants.MaxDifferencesHistory + 1)) {
return this.WALKTRACE(diagonalForwardBase, diagonalForwardStart, diagonalForwardEnd, diagonalForwardOffset,
diagonalReverseBase, diagonalReverseStart, diagonalReverseEnd, diagonalReverseOffset,
forwardPoints, reversePoints,
originalIndex, originalEnd, midOriginalArr,
modifiedIndex, modifiedEnd, midModifiedArr,
deltaIsEven, quitEarlyArr
);
} else {
return null;
}
}
}
}
if (numDifferences <= LocalConstants.MaxDifferencesHistory) {
let temp = new Int32Array(diagonalForwardEnd - diagonalForwardStart + 2);
temp[0] = diagonalForwardBase - diagonalForwardStart + 1;
MyArray.Copy2(forwardPoints, diagonalForwardStart, temp, 1, diagonalForwardEnd - diagonalForwardStart + 1);
this.m_forwardHistory.push(temp);
temp = new Int32Array(diagonalReverseEnd - diagonalReverseStart + 2);
temp[0] = diagonalReverseBase - diagonalReverseStart + 1;
MyArray.Copy2(reversePoints, diagonalReverseStart, temp, 1, diagonalReverseEnd - diagonalReverseStart + 1);
this.m_reverseHistory.push(temp);
}
}
return this.WALKTRACE(diagonalForwardBase, diagonalForwardStart, diagonalForwardEnd, diagonalForwardOffset,
diagonalReverseBase, diagonalReverseStart, diagonalReverseEnd, diagonalReverseOffset,
forwardPoints, reversePoints,
originalIndex, originalEnd, midOriginalArr,
modifiedIndex, modifiedEnd, midModifiedArr,
deltaIsEven, quitEarlyArr
);
}
private PrettifyChanges(changes: DiffChange[]): DiffChange[] {
for (let i = 0; i < changes.length; i++) {
const change = changes[i];
const originalStop = (i < changes.length - 1) ? changes[i + 1].originalStart : this._originalElementsOrHash.length;
const modifiedStop = (i < changes.length - 1) ? changes[i + 1].modifiedStart : this._modifiedElementsOrHash.length;
const checkOriginal = change.originalLength > 0;
const checkModified = change.modifiedLength > 0;
while (
change.originalStart + change.originalLength < originalStop
&& change.modifiedStart + change.modifiedLength < modifiedStop
&& (!checkOriginal || this.OriginalElementsAreEqual(change.originalStart, change.originalStart + change.originalLength))
&& (!checkModified || this.ModifiedElementsAreEqual(change.modifiedStart, change.modifiedStart + change.modifiedLength))
) {
const startStrictEqual = this.ElementsAreStrictEqual(change.originalStart, change.modifiedStart);
const endStrictEqual = this.ElementsAreStrictEqual(change.originalStart + change.originalLength, change.modifiedStart + change.modifiedLength);
if (endStrictEqual && !startStrictEqual) {
break;
}
change.originalStart++;
change.modifiedStart++;
}
const mergedChangeArr: Array<DiffChange | null> = [null];
if (i < changes.length - 1 && this.ChangesOverlap(changes[i], changes[i + 1], mergedChangeArr)) {
changes[i] = mergedChangeArr[0]!;
changes.splice(i + 1, 1);
i--;
continue;
}
}
for (let i = changes.length - 1; i >= 0; i--) {
const change = changes[i];
let originalStop = 0;
let modifiedStop = 0;
if (i > 0) {
const prevChange = changes[i - 1];
originalStop = prevChange.originalStart + prevChange.originalLength;
modifiedStop = prevChange.modifiedStart + prevChange.modifiedLength;
}
const checkOriginal = change.originalLength > 0;
const checkModified = change.modifiedLength > 0;
let bestDelta = 0;
let bestScore = this._boundaryScore(change.originalStart, change.originalLength, change.modifiedStart, change.modifiedLength);
for (let delta = 1; ; delta++) {
const originalStart = change.originalStart - delta;
const modifiedStart = change.modifiedStart - delta;
if (originalStart < originalStop || modifiedStart < modifiedStop) {
break;
}
if (checkOriginal && !this.OriginalElementsAreEqual(originalStart, originalStart + change.originalLength)) {
break;
}
if (checkModified && !this.ModifiedElementsAreEqual(modifiedStart, modifiedStart + change.modifiedLength)) {
break;
}
const touchingPreviousChange = (originalStart === originalStop && modifiedStart === modifiedStop);
const score = (
(touchingPreviousChange ? 5 : 0)
+ this._boundaryScore(originalStart, change.originalLength, modifiedStart, change.modifiedLength)
);
if (score > bestScore) {
bestScore = score;
bestDelta = delta;
}
}
change.originalStart -= bestDelta;
change.modifiedStart -= bestDelta;
const mergedChangeArr: Array<DiffChange | null> = [null];
if (i > 0 && this.ChangesOverlap(changes[i - 1], changes[i], mergedChangeArr)) {
changes[i - 1] = mergedChangeArr[0]!;
changes.splice(i, 1);
i++;
continue;
}
}
if (this._hasStrings) {
for (let i = 1, len = changes.length; i < len; i++) {
const aChange = changes[i - 1];
const bChange = changes[i];
const matchedLength = bChange.originalStart - aChange.originalStart - aChange.originalLength;
const aOriginalStart = aChange.originalStart;
const bOriginalEnd = bChange.originalStart + bChange.originalLength;
const abOriginalLength = bOriginalEnd - aOriginalStart;
const aModifiedStart = aChange.modifiedStart;
const bModifiedEnd = bChange.modifiedStart + bChange.modifiedLength;
const abModifiedLength = bModifiedEnd - aModifiedStart;
if (matchedLength < 5 && abOriginalLength < 20 && abModifiedLength < 20) {
const t = this._findBetterContiguousSequence(
aOriginalStart, abOriginalLength,
aModifiedStart, abModifiedLength,
matchedLength
);
if (t) {
const [originalMatchStart, modifiedMatchStart] = t;
if (originalMatchStart !== aChange.originalStart + aChange.originalLength || modifiedMatchStart !== aChange.modifiedStart + aChange.modifiedLength) {
aChange.originalLength = originalMatchStart - aChange.originalStart;
aChange.modifiedLength = modifiedMatchStart - aChange.modifiedStart;
bChange.originalStart = originalMatchStart + matchedLength;
bChange.modifiedStart = modifiedMatchStart + matchedLength;
bChange.originalLength = bOriginalEnd - bChange.originalStart;
bChange.modifiedLength = bModifiedEnd - bChange.modifiedStart;
}
}
}
}
}
return changes;
}
private _findBetterContiguousSequence(originalStart: number, originalLength: number, modifiedStart: number, modifiedLength: number, desiredLength: number): [number, number] | null {
if (originalLength < desiredLength || modifiedLength < desiredLength) {
return null;
}
const originalMax = originalStart + originalLength - desiredLength + 1;
const modifiedMax = modifiedStart + modifiedLength - desiredLength + 1;
let bestScore = 0;
let bestOriginalStart = 0;
let bestModifiedStart = 0;
for (let i = originalStart; i < originalMax; i++) {
for (let j = modifiedStart; j < modifiedMax; j++) {
const score = this._contiguousSequenceScore(i, j, desiredLength);
if (score > 0 && score > bestScore) {
bestScore = score;
bestOriginalStart = i;
bestModifiedStart = j;
}
}
}
if (bestScore > 0) {
return [bestOriginalStart, bestModifiedStart];
}
return null;
}
private _contiguousSequenceScore(originalStart: number, modifiedStart: number, length: number): number {
let score = 0;
for (let l = 0; l < length; l++) {
if (!this.ElementsAreEqual(originalStart + l, modifiedStart + l)) {
return 0;
}
score += this._originalStringElements[originalStart + l].length;
}
return score;
}
private _OriginalIsBoundary(index: number): boolean {
if (index <= 0 || index >= this._originalElementsOrHash.length - 1) {
return true;
}
return (this._hasStrings && /^\s*$/.test(this._originalStringElements[index]));
}
private _OriginalRegionIsBoundary(originalStart: number, originalLength: number): boolean {
if (this._OriginalIsBoundary(originalStart) || this._OriginalIsBoundary(originalStart - 1)) {
return true;
}
if (originalLength > 0) {
const originalEnd = originalStart + originalLength;
if (this._OriginalIsBoundary(originalEnd - 1) || this._OriginalIsBoundary(originalEnd)) {
return true;
}
}
return false;
}
private _ModifiedIsBoundary(index: number): boolean {
if (index <= 0 || index >= this._modifiedElementsOrHash.length - 1) {
return true;
}
return (this._hasStrings && /^\s*$/.test(this._modifiedStringElements[index]));
}
private _ModifiedRegionIsBoundary(modifiedStart: number, modifiedLength: number): boolean {
if (this._ModifiedIsBoundary(modifiedStart) || this._ModifiedIsBoundary(modifiedStart - 1)) {
return true;
}
if (modifiedLength > 0) {
const modifiedEnd = modifiedStart + modifiedLength;
if (this._ModifiedIsBoundary(modifiedEnd - 1) || this._ModifiedIsBoundary(modifiedEnd)) {
return true;
}
}
return false;
}
private _boundaryScore(originalStart: number, originalLength: number, modifiedStart: number, modifiedLength: number): number {
const originalScore = (this._OriginalRegionIsBoundary(originalStart, originalLength) ? 1 : 0);
const modifiedScore = (this._ModifiedRegionIsBoundary(modifiedStart, modifiedLength) ? 1 : 0);
return (originalScore + modifiedScore);
}
private ConcatenateChanges(left: DiffChange[], right: DiffChange[]): DiffChange[] {
const mergedChangeArr: DiffChange[] = [];
if (left.length === 0 || right.length === 0) {
return (right.length > 0) ? right : left;
} else if (this.ChangesOverlap(left[left.length - 1], right[0], mergedChangeArr)) {
const result = new Array<DiffChange>(left.length + right.length - 1);
MyArray.Copy(left, 0, result, 0, left.length - 1);
result[left.length - 1] = mergedChangeArr[0];
MyArray.Copy(right, 1, result, left.length, right.length - 1);
return result;
} else {
const result = new Array<DiffChange>(left.length + right.length);
MyArray.Copy(left, 0, result, 0, left.length);
MyArray.Copy(right, 0, result, left.length, right.length);
return result;
}
}
private ChangesOverlap(left: DiffChange, right: DiffChange, mergedChangeArr: Array<DiffChange | null>): boolean {
Debug.Assert(left.originalStart <= right.originalStart, 'Left change is not less than or equal to right change');
Debug.Assert(left.modifiedStart <= right.modifiedStart, 'Left change is not less than or equal to right change');
if (left.originalStart + left.originalLength >= right.originalStart || left.modifiedStart + left.modifiedLength >= right.modifiedStart) {
const originalStart = left.originalStart;
let originalLength = left.originalLength;
const modifiedStart = left.modifiedStart;
let modifiedLength = left.modifiedLength;
if (left.originalStart + left.originalLength >= right.originalStart) {
originalLength = right.originalStart + right.originalLength - left.originalStart;
}
if (left.modifiedStart + left.modifiedLength >= right.modifiedStart) {
modifiedLength = right.modifiedStart + right.modifiedLength - left.modifiedStart;
}
mergedChangeArr[0] = new DiffChange(originalStart, originalLength, modifiedStart, modifiedLength);
return true;
} else {
mergedChangeArr[0] = null;
return false;
}
}
private ClipDiagonalBound(diagonal: number, numDifferences: number, diagonalBaseIndex: number, numDiagonals: number): number {
if (diagonal >= 0 && diagonal < numDiagonals) {
return diagonal;
}
const diagonalsBelow = diagonalBaseIndex;
const diagonalsAbove = numDiagonals - diagonalBaseIndex - 1;
const diffEven = (numDifferences % 2 === 0);
if (diagonal < 0) {
const lowerBoundEven = (diagonalsBelow % 2 === 0);
return (diffEven === lowerBoundEven) ? 0 : 1;
} else {
const upperBoundEven = (diagonalsAbove % 2 === 0);
return (diffEven === upperBoundEven) ? numDiagonals - 1 : numDiagonals - 2;
}
}
}
const precomputedEqualityArray = new Uint32Array(0x10000);
const computeLevenshteinDistanceForShortStrings = (firstString: string, secondString: string): number => {
const firstStringLength = firstString.length;
const secondStringLength = secondString.length;
const lastBitMask = 1 << (firstStringLength - 1);
let positiveVector = -1;
let negativeVector = 0;
let distance = firstStringLength;
let index = firstStringLength;
while (index--) {
precomputedEqualityArray[firstString.charCodeAt(index)] |= 1 << index;
}
for (index = 0; index < secondStringLength; index++) {
let equalityMask = precomputedEqualityArray[secondString.charCodeAt(index)];
const combinedVector = equalityMask | negativeVector;
equalityMask |= ((equalityMask & positiveVector) + positiveVector) ^ positiveVector;
negativeVector |= ~(equalityMask | positiveVector);
positiveVector &= equalityMask;
if (negativeVector & lastBitMask) {
distance++;
}
if (positiveVector & lastBitMask) {
distance--;
}
negativeVector = (negativeVector << 1) | 1;
positiveVector = (positiveVector << 1) | ~(combinedVector | negativeVector);
negativeVector &= combinedVector;
}
index = firstStringLength;
while (index--) {
precomputedEqualityArray[firstString.charCodeAt(index)] = 0;
}
return distance;
};
function computeLevenshteinDistanceForLongStrings(firstString: string, secondString: string): number {
const firstStringLength = firstString.length;
const secondStringLength = secondString.length;
const horizontalBitArray = [];
const verticalBitArray = [];
const horizontalSize = Math.ceil(firstStringLength / 32);
const verticalSize = Math.ceil(secondStringLength / 32);
for (let i = 0; i < horizontalSize; i++) {
horizontalBitArray[i] = -1;
verticalBitArray[i] = 0;
}
let verticalIndex = 0;
for (; verticalIndex < verticalSize - 1; verticalIndex++) {
let negativeVector = 0;
let positiveVector = -1;
const start = verticalIndex * 32;
const verticalLength = Math.min(32, secondStringLength) + start;
for (let k = start; k < verticalLength; k++) {
precomputedEqualityArray[secondString.charCodeAt(k)] |= 1 << k;
}
for (let i = 0; i < firstStringLength; i++) {
const equalityMask = precomputedEqualityArray[firstString.charCodeAt(i)];
const previousBit = (horizontalBitArray[(i / 32) | 0] >>> i) & 1;
const matchBit = (verticalBitArray[(i / 32) | 0] >>> i) & 1;
const combinedVector = equalityMask | negativeVector;
const combinedHorizontalVector = ((((equalityMask | matchBit) & positiveVector) + positiveVector) ^ positiveVector) | equalityMask | matchBit;
let positiveHorizontalVector = negativeVector | ~(combinedHorizontalVector | positiveVector);
let negativeHorizontalVector = positiveVector & combinedHorizontalVector;
if ((positiveHorizontalVector >>> 31) ^ previousBit) {
horizontalBitArray[(i / 32) | 0] ^= 1 << i;
}
if ((negativeHorizontalVector >>> 31) ^ matchBit) {
verticalBitArray[(i / 32) | 0] ^= 1 << i;
}
positiveHorizontalVector = (positiveHorizontalVector << 1) | previousBit;
negativeHorizontalVector = (negativeHorizontalVector << 1) | matchBit;
positiveVector = negativeHorizontalVector | ~(combinedVector | positiveHorizontalVector);
negativeVector = positiveHorizontalVector & combinedVector;
}
for (let k = start; k < verticalLength; k++) {
precomputedEqualityArray[secondString.charCodeAt(k)] = 0;
}
}
let negativeVector = 0;
let positiveVector = -1;
const start = verticalIndex * 32;
const verticalLength = Math.min(32, secondStringLength - start) + start;
for (let k = start; k < verticalLength; k++) {
precomputedEqualityArray[secondString.charCodeAt(k)] |= 1 << k;
}
let distance = secondStringLength;
for (let i = 0; i < firstStringLength; i++) {
const equalityMask = precomputedEqualityArray[firstString.charCodeAt(i)];
const previousBit = (horizontalBitArray[(i / 32) | 0] >>> i) & 1;
const matchBit = (verticalBitArray[(i / 32) | 0] >>> i) & 1;
const combinedVector = equalityMask | negativeVector;
const combinedHorizontalVector = ((((equalityMask | matchBit) & positiveVector) + positiveVector) ^ positiveVector) | equalityMask | matchBit;
let positiveHorizontalVector = negativeVector | ~(combinedHorizontalVector | positiveVector);
let negativeHorizontalVector = positiveVector & combinedHorizontalVector;
distance += (positiveHorizontalVector >>> (secondStringLength - 1)) & 1;
distance -= (negativeHorizontalVector >>> (secondStringLength - 1)) & 1;
if ((positiveHorizontalVector >>> 31) ^ previousBit) {
horizontalBitArray[(i / 32) | 0] ^= 1 << i;
}
if ((negativeHorizontalVector >>> 31) ^ matchBit) {
verticalBitArray[(i / 32) | 0] ^= 1 << i;
}
positiveHorizontalVector = (positiveHorizontalVector << 1) | previousBit;
negativeHorizontalVector = (negativeHorizontalVector << 1) | matchBit;
positiveVector = negativeHorizontalVector | ~(combinedVector | positiveHorizontalVector);
negativeVector = positiveHorizontalVector & combinedVector;
}
for (let k = start; k < verticalLength; k++) {
precomputedEqualityArray[secondString.charCodeAt(k)] = 0;
}
return distance;
}
export function computeLevenshteinDistance(firstString: string, secondString: string): number {
if (firstString.length < secondString.length) {
const temp = secondString;
secondString = firstString;
firstString = temp;
}
if (secondString.length === 0) {
return firstString.length;
}
if (firstString.length <= 32) {
return computeLevenshteinDistanceForShortStrings(firstString, secondString);
}
return computeLevenshteinDistanceForLongStrings(firstString, secondString);
}