diff --git a/src/diff.spec.ts b/src/diff.spec.ts index 755ddd6..97c9106 100644 --- a/src/diff.spec.ts +++ b/src/diff.spec.ts @@ -192,6 +192,100 @@ describe.only("getChanges", () => expect(getChanges(a, b)).toStrictEqual(changes); }); }); + + describe("When given strings", () => + { + it.each([ + [ "", "" ], + [ "a", "a" ], + [ "hello, world!", "hello, world!" ] + ])("Returns undefined for identical sequences", (a, b) => + { + expect(getChanges(a, b)).toStrictEqual([]); + }); + + it.each([ + [ "a", "", [ [ ChangeType.DELETE, 0, undefined ] ] ], + [ "", "a", [ [ ChangeType.INSERT, 0, "a" ] ] ], + [ "a", "ab", [ [ ChangeType.INSERT, 1, "b" ] ] ], + [ "ab", "a", [ [ ChangeType.DELETE, 1, undefined ] ] ], + [ + "ab", + "ac", + [ + [ ChangeType.DELETE, 1, undefined ], + [ ChangeType.INSERT, 1, "c" ] + ] + ], + [ + "ac", + "bc", + [ + [ ChangeType.DELETE, 0, undefined ], + [ ChangeType.INSERT, 0, "b" ] + ] + ], + [ + "ab", + "", + [ + [ ChangeType.DELETE, 0, undefined ], + [ ChangeType.DELETE, 0, undefined ] + ] + ], + [ + "", + "ab", + [ + [ ChangeType.INSERT, 0, "a" ], + [ ChangeType.INSERT, 1, "b" ] + ] + ] + ])("Returns a change tuple for sequences that are different", (a, b, diff) => + { + expect(getChanges(a, b)).toStrictEqual(diff); + }); + + it.each([ + [ + "hello", + "goodbye", + [ + [ ChangeType.INSERT, 0, "g" ], + [ ChangeType.INSERT, 1, "o" ], + [ ChangeType.INSERT, 2, "o" ], + [ ChangeType.INSERT, 3, "d" ], + [ ChangeType.INSERT, 4, "b" ], + [ ChangeType.INSERT, 5, "y" ], + [ ChangeType.DELETE, 6, undefined ], + [ ChangeType.DELETE, 7, undefined ], + [ ChangeType.DELETE, 7, undefined ], + [ ChangeType.DELETE, 7, undefined ] + ] + ], + [ + "hello, world!", + "goodbye, world.", + [ + [ ChangeType.INSERT, 0, "g" ], + [ ChangeType.INSERT, 1, "o" ], + [ ChangeType.INSERT, 2, "o" ], + [ ChangeType.INSERT, 3, "d" ], + [ ChangeType.INSERT, 4, "b" ], + [ ChangeType.INSERT, 5, "y" ], + [ ChangeType.DELETE, 6, undefined ], + [ ChangeType.DELETE, 7, undefined ], + [ ChangeType.DELETE, 7, undefined ], + [ ChangeType.DELETE, 7, undefined ], + [ ChangeType.INSERT, 14, "." ], + [ ChangeType.DELETE, 15, undefined ] + ] + ] + ])("Adjusts indices to account for previous changes.", (a, b, diff) => + { + expect(getChanges(a, b)).toStrictEqual(diff); + }); + }); }); describe("diff", () => diff --git a/src/diff.ts b/src/diff.ts index 8b5f571..db09986 100644 --- a/src/diff.ts +++ b/src/diff.ts @@ -169,6 +169,154 @@ export const diffText = (a: string, b: string): any => } }; +type Diffable = Record | Array | string; + +const isArray = (d: Diffable): d is Array => + d instanceof Array; + +const isString = (d: Diffable): d is string => + typeof d === "string"; + +const isRecord = (d: Diffable): d is Record => + !isArray(d) && !isString(d); + +export const getChanges = (a: Diffable, b: Diffable): Change[] => +{ + if (isString(a) && isString(b)) + return getStringChanges(a, b); + else if (isArray(a) && isArray(b)) + return getArrayChanges(a, b); + else if (isRecord(a) && isRecord(b)) + return getRecordChanges(a, b); + else + return []; +}; + +const getStringChanges = (a: string, b: string): Change[] => +{ + if (a === b) + return []; + else if (a.length === 0) + { + return b.split("").map((character, index) => + [ ChangeType.INSERT, index, character ]); + } + else if (b.length === 0) + { + return a.split("").map(() => + [ ChangeType.DELETE, 0, undefined ]); + } + else + { + const m = a.length, n = b.length; + const reverse = m >= n; + + return reverse + ? _diffText(b, a, reverse) + : _diffText(a, b, reverse); + } +}; + +const getArrayChanges = (a: Array, b: Array): Change[] => +{ + const changeList: Change[] = []; + + let finalIndices = 0; + let bOffset = 0; + + for (let index = 0; index < a.length; index++) + { + const value = a[index]; + + const bIndex = index + bOffset; + + if (b[bIndex] === undefined) + changeList.push([ ChangeType.DELETE, index, undefined ]); + + else if (value instanceof Object && b[bIndex] instanceof Object) + { + const currentDiff = getChanges(value, b[bIndex]); + const nextDiff = typeof b[bIndex + 1] === "undefined" + ? [] + : getChanges(value, b[bIndex+1]); + + if (typeof b[bIndex+1] !== "undefined" && nextDiff.length === 0) + { + changeList.push([ ChangeType.INSERT, index, b[bIndex] ]); + finalIndices += 2; + bOffset++; + } + + else if (currentDiff.length !== 0) + { + changeList.push([ ChangeType.PENDING, index, currentDiff ]); + finalIndices++; + } + + else + finalIndices++; + } + + else if (value !== b[bIndex] && value === b[bIndex+1]) + { + changeList.push([ ChangeType.INSERT, bIndex, b[bIndex] ]); + finalIndices += 2; + bOffset++; + } + + else if (value !== b[bIndex] && value !== b[bIndex+1]) + { + changeList.push([ ChangeType.UPDATE, bIndex, b[bIndex] ]); + finalIndices++; + } + + else + finalIndices++; + } + + if (finalIndices < b.length) + { + b.slice(a.length).forEach((value, index) => + changeList.push([ ChangeType.INSERT, finalIndices + index, value ])); + } + + return changeList; +}; + +const getRecordChanges = ( + a: Record, + b: Record +): Change[] => +{ + const changeList: Change[] = []; + + Object.entries(a).forEach(([ property ]) => + { + if (!(property in b)) + changeList.push([ ChangeType.DELETE, property, undefined ]); + }); + + Object.entries(b).forEach(([ property, value ]) => + { + if (!(property in a)) + changeList.push([ ChangeType.INSERT, property, value ]); + + else if (a[property] instanceof Object && value instanceof Object + ) + { + const d = getChanges(a[property], value); + + if (d.length !== 0) + changeList.push([ ChangeType.PENDING, property, d ]); + } + + else if (a[property] !== value) + changeList.push([ ChangeType.UPDATE, property, value ]); + }); + + return changeList; +}; + /** * An adaptation of Wu et al. O(NP) text diff. (See docs/text-diff) * @@ -179,7 +327,7 @@ export const diffText = (a: string, b: string): any => * @param isReversed Whether or not a or b have been swapped. * @returns A list of changes that that turn a into b. */ -const _diffText = (a: string, b: string, isReversed: boolean): any => +const _diffText = (a: string, b: string, isReversed: boolean): Change[] => { const m = a.length, n = b.length; const offset = m; @@ -257,7 +405,7 @@ const _diffText = (a: string, b: string, isReversed: boolean): any => k = pathPositions[k].k; // eslint-disable-line prefer-destructuring } - const changeList: [ "add" | "delete", number, string | undefined ][] = []; + const changeList: Change[] = []; let x = 0, y = 0, index = -1; for (let i = editPath.length - 1; i >= 0; i--) @@ -269,7 +417,7 @@ const _diffText = (a: string, b: string, isReversed: boolean): any => if (isReversed) { changeList[changeList.length] = [ - "delete", + ChangeType.DELETE, index, undefined ]; @@ -277,7 +425,7 @@ const _diffText = (a: string, b: string, isReversed: boolean): any => else { changeList[changeList.length] = [ - "add", + ChangeType.INSERT, index, b[y - 1] ]; @@ -292,7 +440,7 @@ const _diffText = (a: string, b: string, isReversed: boolean): any => if (isReversed) { changeList[changeList.length] = [ - "add", + ChangeType.INSERT, index, a[x - 1] ]; @@ -302,7 +450,7 @@ const _diffText = (a: string, b: string, isReversed: boolean): any => else { changeList[changeList.length] = [ - "delete", + ChangeType.DELETE, index, undefined ]; @@ -317,128 +465,5 @@ const _diffText = (a: string, b: string, isReversed: boolean): any => } } - return changeList; -}; - -type Diffable = Record | Array | string; - -const isArray = (d: Diffable): d is Array => - d instanceof Array; - -const isString = (d: Diffable): d is string => - typeof d === "string"; - -const isRecord = (d: Diffable): d is Record => - !isArray(d) && !isString(d); - -export const getChanges = (a: Diffable, b: Diffable): Change[] => -{ - if (isString(a) && isString(b)) - return []; - else if (isArray(a) && isArray(b)) - return getArrayChanges(a, b); - else if (isRecord(a) && isRecord(b)) - return getRecordChanges(a, b); - else - return []; -}; - -const getArrayChanges = (a: Array, b: Array): Change[] => -{ - const changeList: Change[] = []; - - let finalIndices = 0; - let bOffset = 0; - - for (let index = 0; index < a.length; index++) - { - const value = a[index]; - - const bIndex = index + bOffset; - - if (b[bIndex] === undefined) - changeList.push([ ChangeType.DELETE, index, undefined ]); - - else if (value instanceof Object && b[bIndex] instanceof Object) - { - const currentDiff = getChanges(value, b[bIndex]); - const nextDiff = typeof b[bIndex + 1] === "undefined" - ? [] - : getChanges(value, b[bIndex+1]); - - if (typeof b[bIndex+1] !== "undefined" && nextDiff.length === 0) - { - changeList.push([ ChangeType.INSERT, index, b[bIndex] ]); - finalIndices += 2; - bOffset++; - } - - else if (currentDiff.length !== 0) - { - changeList.push([ ChangeType.PENDING, index, currentDiff ]); - finalIndices++; - } - - else - finalIndices++; - } - - else if (value !== b[bIndex] && value === b[bIndex+1]) - { - changeList.push([ ChangeType.INSERT, bIndex, b[bIndex] ]); - finalIndices += 2; - bOffset++; - } - - else if (value !== b[bIndex] && value !== b[bIndex+1]) - { - changeList.push([ ChangeType.UPDATE, bIndex, b[bIndex] ]); - finalIndices++; - } - - else - finalIndices++; - } - - if (finalIndices < b.length) - { - b.slice(a.length).forEach((value, index) => - changeList.push([ ChangeType.INSERT, finalIndices + index, value ])); - } - - return changeList; -}; - -const getRecordChanges = ( - a: Record, - b: Record -): Change[] => -{ - const changeList: Change[] = []; - - Object.entries(a).forEach(([ property ]) => - { - if (!(property in b)) - changeList.push([ ChangeType.DELETE, property, undefined ]); - }); - - Object.entries(b).forEach(([ property, value ]) => - { - if (!(property in a)) - changeList.push([ ChangeType.INSERT, property, value ]); - - else if (a[property] instanceof Object && value instanceof Object - ) - { - const d = getChanges(a[property], value); - - if (d.length !== 0) - changeList.push([ ChangeType.PENDING, property, d ]); - } - - else if (a[property] !== value) - changeList.push([ ChangeType.UPDATE, property, value ]); - }); - return changeList; }; \ No newline at end of file