From 974258fe05796d296d5117a61c3bd0c5b5ea2483 Mon Sep 17 00:00:00 2001 From: Alex MacArthur Date: Sat, 19 Mar 2022 22:15:04 -0500 Subject: [PATCH] Rebuild with Symbol-based approach. --- index.html | 10 ++-- package-lock.json | 22 ++++---- package.json | 11 ++-- src/index.test.ts | 42 +++++++-------- src/index.ts | 65 ++++++++++++++++++++--- src/types.ts | 20 +++---- src/utils/deleteRef.test.ts | 22 ++++++++ src/utils/deleteRef.ts | 9 ++++ src/utils/fillStrings.test.ts | 75 --------------------------- src/utils/fillStrings.ts | 75 --------------------------- src/utils/getAllParts.test.ts | 33 ++++++++++++ src/utils/getAllParts.ts | 17 ++++++ src/utils/getDiff.ts | 23 -------- src/utils/hasBeenSlid.test.ts | 20 ------- src/utils/hasBeenSlid.ts | 38 -------------- src/utils/latestEmptyMatchingIndex.ts | 40 -------------- src/utils/toCharacters.test.ts | 20 +++++++ src/utils/toCharacters.ts | 6 +-- 18 files changed, 215 insertions(+), 333 deletions(-) create mode 100644 src/utils/deleteRef.test.ts create mode 100644 src/utils/deleteRef.ts delete mode 100644 src/utils/fillStrings.test.ts delete mode 100644 src/utils/fillStrings.ts create mode 100644 src/utils/getAllParts.test.ts create mode 100644 src/utils/getAllParts.ts delete mode 100644 src/utils/getDiff.ts delete mode 100644 src/utils/hasBeenSlid.test.ts delete mode 100644 src/utils/hasBeenSlid.ts delete mode 100644 src/utils/latestEmptyMatchingIndex.ts create mode 100644 src/utils/toCharacters.test.ts diff --git a/index.html b/index.html index e8250f2..88050f1 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,6 @@ - Vite App @@ -10,13 +9,10 @@ diff --git a/package-lock.json b/package-lock.json index 4f5d970..683a62f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,17 @@ { "name": "striff", - "version": "0.0.1", + "version": "0.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "striff", - "version": "0.0.1", + "version": "0.0.5", + "license": "MIT", "devDependencies": { "@types/jest": "^27.4.1", "jest": "^27.5.1", - "prettier": "^2.5.1", + "prettier": "^2.6.0", "ts-jest": "^27.1.3", "typescript": "^4.6.2", "vite": "^2.8.6" @@ -3696,15 +3697,18 @@ } }, "node_modules/prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.0.tgz", + "integrity": "sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A==", "dev": true, "bin": { "prettier": "bin-prettier.js" }, "engines": { "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, "node_modules/pretty-format": { @@ -7303,9 +7307,9 @@ "dev": true }, "prettier": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz", - "integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.0.tgz", + "integrity": "sha512-m2FgJibYrBGGgQXNzfd0PuDGShJgRavjUoRCw1mZERIWVSXF0iLzLm+aOqTAbLnC3n6JzUhAA8uZnFVghHJ86A==", "dev": true }, "pretty-format": { diff --git a/package.json b/package.json index ea499ca..b1c9842 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,17 @@ { "name": "striff", - "description": "Simple string diffing.", + "description": "Real simple string diffing.", "author": "Alex MacArthur (https://macarthur.me)", - "version": "0.0.5", + "version": "1.0.0", "main": "dist/index.umd.js", "module": "dist/index.es.js", "types": "dist/index.d.ts", "license": "MIT", + "keywords": [ + "string diffing", + "difference", + "differ" + ], "files": [ "src/", "dist/" @@ -20,7 +25,7 @@ "devDependencies": { "@types/jest": "^27.4.1", "jest": "^27.5.1", - "prettier": "^2.5.1", + "prettier": "^2.6.0", "ts-jest": "^27.1.3", "typescript": "^4.6.2", "vite": "^2.8.6" diff --git a/src/index.test.ts b/src/index.test.ts index a23a795..5a576dd 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -12,14 +12,14 @@ describe("characters are added", () => { const { added, removed } = striff("abc", "abcd"); expect(removed).toHaveLength(0); - expect(added).toEqual([{ character: "d", index: 3 }]); + expect(added).toEqual([{ value: "d", index: 3 }]); }); it("Correctly diffs when characters are added to middle.", () => { const { added, removed } = striff("hi pal", "hi, pal"); expect(removed).toHaveLength(0); - expect(added).toEqual([{ character: ",", index: 2}]); + expect(added).toEqual([{ value: ",", index: 2}]); }); }); @@ -31,11 +31,11 @@ describe("characters are removed", () => { expect(removed).toHaveLength(2); expect(removed).toEqual([ { - character: "b", + value: "b", index: 1, }, { - character: "c", + value: "c", index: 2, }, ]); @@ -48,7 +48,7 @@ describe("characters are removed", () => { expect(removed).toHaveLength(1); expect(removed).toEqual([ { - character: "a", + value: "a", index: 0, }, ]); @@ -63,13 +63,13 @@ describe("characters have changed", () => { expect(removed).toHaveLength(1); expect(removed).toEqual([ { - character: "b", + value: "b", index: 1, }, ]); expect(added).toEqual([ { - character: "z", + value: "z", index: 1, }, ]); @@ -82,29 +82,29 @@ describe("characters have changed", () => { expect(removed).toHaveLength(3); expect(removed).toEqual([ { - character: "a", + value: "a", index: 0, }, { - character: "b", + value: "b", index: 1, }, { - character: "c", + value: "c", index: 2, }, ]); expect(added).toEqual([ { - character: "x", + value: "x", index: 0, }, { - character: "y", + value: "y", index: 1, }, { - character: "z", + value: "z", index: 2, }, ]); @@ -119,37 +119,37 @@ describe("characters are changed and added", () => { expect(removed).toHaveLength(3); expect(removed).toEqual([ { - character: "a", + value: "a", index: 0, }, { - character: "b", + value: "b", index: 1, }, { - character: "c", + value: "c", index: 2, }, ]); expect(added).toEqual([ { - character: "x", + value: "x", index: 0, }, { - character: "y", + value: "y", index: 1, }, { - character: "z", + value: "z", index: 2, }, { - character: "1", + value: "1", index: 3, }, { - character: "2", + value: "2", index: 4, }, ]); diff --git a/src/index.ts b/src/index.ts index f71d3a2..a341368 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,65 @@ -import { DiffResult } from "./types"; -import { fillStrings } from "./utils/fillStrings"; -import getDiff from "./utils/getDiff"; +import { DiffResult, Character } from "./types"; +import deleteRef from "./utils/deleteRef"; +import getAllParts from "./utils/getAllParts"; +import toCharacters from "./utils/toCharacters"; -const striff = (str1: string, str2: string): DiffResult => { - let [strArr1, strArr2] = fillStrings(str1, str2); +const getDiff = (partsArr: Character[][], arr1: Character[], arr2: Character[]) => { + let str2 = arr2.map((c: Character) => c.value).join(""); + let matched = new Map(); + let matchedIndicies: {[key: string]: number} = {}; + + partsArr.forEach((part: Character[]) => { + let partString = part.map((c) => c.value).join(""); + let pattern = new RegExp(partString); + + // If this little thing matches ANY part of the string, it's in. + let result = pattern.exec(str2); + + // We found a match, so let's spread it into our 'matched' store. + let pastMatch = matchedIndicies[result?.index || ""]; + + // Give matched strings of longer length over all others. + if (result && (!pastMatch || partString.length > pastMatch)) { + matchedIndicies[result.index] = partString.length; + + // Since this is matched, I can safely update the second + // string with symbol references. + let partIndex = 0; + for (let i = result.index; i < part.length + result.index; i++) { + arr2[i].ref = part[partIndex].ref; + partIndex++; + } + + // Throw each of the matched characters into storage. + part.forEach((char) => matched.set(char.ref, char.value)); + } + }); + + let hasNoMatchingRef = (char: Character) => !matched.get(char.ref); return { - added: getDiff(strArr2, strArr1), - removed: getDiff(strArr1, strArr2), + // First string characters NOT in the "matched" set. + removed: arr1.filter(hasNoMatchingRef).map(deleteRef), + + // Second string characters NOT in the "matched" set. + added: arr2.filter(hasNoMatchingRef).map(deleteRef), }; }; +const striff = (str1: string, str2: string): DiffResult => { + let strArr1 = toCharacters(str1, true); + let strArr2 = toCharacters(str2).map((char, index) => { + let str1Char = strArr1[index]; + + if (str1Char?.value === char.value) { + char.ref = str1Char.ref; + } + + return char; + }); + + let parts = getAllParts(strArr1); + return getDiff(parts, strArr1, strArr2); +}; + export default striff; diff --git a/src/types.ts b/src/types.ts index 0f91142..5d9be68 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,16 +1,12 @@ -export interface Diff { - character: string; - index: number; +export interface Character { + index: number, + value: string, + ref: Symbol | null } -export interface DiffResult { - added: Diff[]; - removed: Diff[]; -} +export type PrunedCharacter = Pick; -export interface Character { - value: string | null, - accountedFor: boolean +export interface DiffResult { + added: PrunedCharacter[]; + removed: PrunedCharacter[]; } - -export type FilledString = (string | null)[]; diff --git a/src/utils/deleteRef.test.ts b/src/utils/deleteRef.test.ts new file mode 100644 index 0000000..796b761 --- /dev/null +++ b/src/utils/deleteRef.test.ts @@ -0,0 +1,22 @@ +import deleteRef from "./deleteRef"; +import toCharacters from "./toCharacters"; + +it("deletes refs from characters", () => { + let string = toCharacters("abc"); + let result = string.map(deleteRef); + + expect(result).toEqual([ + { + index: 0, + value: "a", + }, + { + index: 1, + value: "b", + }, + { + index: 2, + value: "c", + }, + ]); +}); diff --git a/src/utils/deleteRef.ts b/src/utils/deleteRef.ts new file mode 100644 index 0000000..56b9c93 --- /dev/null +++ b/src/utils/deleteRef.ts @@ -0,0 +1,9 @@ +import { Character, PrunedCharacter } from "../types"; + +let deleteRef = (char: Partial) => { + delete char.ref; + + return char as PrunedCharacter; +}; + +export default deleteRef; diff --git a/src/utils/fillStrings.test.ts b/src/utils/fillStrings.test.ts deleted file mode 100644 index 75eb2a5..0000000 --- a/src/utils/fillStrings.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { fillStrings } from "./fillStrings"; - -describe("strings with non-duplicate characters", () => { - it("Fills strings adding characters to end.", () => { - const [arr1, arr2] = fillStrings("abc", "abcd"); - - expect(arr1).toEqual(["a", "b", "c", null]); - expect(arr2).toEqual(["a", "b", "c", "d"]); - }); - - it("Fills strings adding characters to beginning.", () => { - const [arr1, arr2] = fillStrings("abc", "xxabc"); - - expect(arr1).toEqual([null, null, "a", "b", "c"]); - expect(arr2).toEqual(["x", "x", "a", "b", "c"]); - }); - - it("Fills strings removing characters from beginning.", () => { - const [arr1, arr2] = fillStrings("abc", "bc"); - - expect(arr1).toEqual(["a", "b", "c"]); - expect(arr2).toEqual([null, "b", "c"]); - }); - - it("Fills strings removing characters from end.", () => { - const [arr1, arr2] = fillStrings("abc", "a"); - - expect(arr1).toEqual(["a", "b", "c"]); - expect(arr2).toEqual(["a", null, null]); - }); -}); - -describe("strings with duplicate characters", () => { - it("Fills strings adding characters to end.", () => { - const [arr1, arr2] = fillStrings("abc", "abccc"); - - expect(arr1).toEqual(["a", "b", "c", null, null]); - expect(arr2).toEqual(["a", "b", "c", "c", "c"]); - }); - - it("Fills strings adding characters to beginning.", () => { - const [arr1, arr2] = fillStrings("abc", "xxxabc"); - - expect(arr1).toEqual([null, null, null, "a", "b", "c"]); - expect(arr2).toEqual(["x", "x", "x", "a", "b", "c"]); - }); - - it("Fills strings removing characters from beginning.", () => { - const [arr1, arr2] = fillStrings("abbbc", "bc"); - - expect(arr1).toEqual(["a", "b", "b", "b", "c"]); - expect(arr2).toEqual([null, null, null, "b", "c"]); - }); - - it("Fills strings removing characters from end.", () => { - const [arr1, arr2] = fillStrings("abbbc", "abb"); - - expect(arr1).toEqual(["a", "b", "b", "b", "c"]); - expect(arr2).toEqual(["a", "b", "b", null, null]); - }); - - it("Fills strings removing MORE characters from end.", () => { - const [arr1, arr2] = fillStrings("abbbc", "ab"); - - expect(arr1).toEqual(["a", "b", "b", "b", "c"]); - expect(arr2).toEqual(["a", "b", null, null, null]); - }); - - it("Does not fill strings that are totally different.", () => { - const [arr1, arr2] = fillStrings("abc", "xyz12"); - - expect(arr1).toEqual(["a", "b", "c", null, null]); - expect(arr2).toEqual(["x", "y", "z", "1", "2"]); - }); -}); diff --git a/src/utils/fillStrings.ts b/src/utils/fillStrings.ts deleted file mode 100644 index aa2ceff..0000000 --- a/src/utils/fillStrings.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { FilledString } from "../types"; -import latestEmptyMatchingIndex from "./latestEmptyMatchingIndex"; -import hasBeenSlid from "./hasBeenSlid"; -import toCharacters from "./toCharacters"; - -const fill = (str1: string[], str2: string[]): FilledString[] => { - let strArr1 = toCharacters(str1); - let strArr2 = toCharacters(str2); - - for (let index = 0; index < strArr1.length; index++) { - let character = strArr1[index]; - - if (character.value === null) { - continue; - } - - // Find a matching character in the other string that we haven't accounted for already. - let secondStringCharacter = strArr2.find( - (c) => c.value === character.value && !c.accountedFor - ); - let doesNotExist = !secondStringCharacter && strArr2[index] !== null; - - if (secondStringCharacter) { - secondStringCharacter.accountedFor = true; - } - - let isChangedCharacter = - character?.value && - strArr2[index]?.value && - character?.value !== strArr2[index]?.value; - - if (isChangedCharacter && !hasBeenSlid([strArr1, strArr2], index)) { - continue; - } - - let latestIndexMatch = latestEmptyMatchingIndex({ - initialIndex: index, - arr1: strArr1, - arr2: strArr2, - character: strArr2[index], - }); - - let difference = latestIndexMatch - index; - - if (difference > 0) { - let insertIndex = Math.round(index / strArr2.length) ? index + 1 : index; - let fillerItems = new Array(difference).fill({ - value: null, - accountedFor: true, - }); - strArr2.splice(insertIndex, 0, ...fillerItems); - index = latestIndexMatch; - continue; - } - - if (doesNotExist) { - strArr2.splice(index, 0, { value: null, accountedFor: true }); - continue; - } - } - - return [strArr1.map((c) => c.value), strArr2.map((c) => c.value)]; -}; - -const split = (str: string): string[] => str.split(""); - -export const fillStrings = (str1: string, str2: string): FilledString[] => { - let strArr1 = split(str1); - let strArr2 = split(str2); - - [strArr1 as FilledString, strArr2 as FilledString] = fill(strArr1, strArr2); - [strArr2 as FilledString, strArr1 as FilledString] = fill(strArr2, strArr1); - - return [strArr1, strArr2]; -}; diff --git a/src/utils/getAllParts.test.ts b/src/utils/getAllParts.test.ts new file mode 100644 index 0000000..889cbee --- /dev/null +++ b/src/utils/getAllParts.test.ts @@ -0,0 +1,33 @@ +import getAllParts from "./getAllParts"; +import toCharacters from "./toCharacters"; + +it("crawls string to break it into parts", () => { + let string = toCharacters("abc"); + let result = getAllParts(string); + + expect(result).toEqual([ + [{ value: "a", index: 0, ref: null }], + [ + { value: "a", index: 0, ref: null }, + { value: "b", index: 1, ref: null }, + ], + [ + { value: "a", index: 0, ref: null }, + { value: "b", index: 1, ref: null }, + { value: "c", index: 2, ref: null }, + ], + [{ value: "b", index: 1, ref: null }], + [ + { value: "b", index: 1, ref: null }, + { value: "c", index: 2, ref: null }, + ], + [{ value: "c", index: 2, ref: null }], + ]); +}); + +it("gets parts of small string", () => { + let string = toCharacters("a"); + let result = getAllParts(string); + + expect(result).toEqual([[{ value: "a", index: 0, ref: null }]]); +}); diff --git a/src/utils/getAllParts.ts b/src/utils/getAllParts.ts new file mode 100644 index 0000000..754d2e0 --- /dev/null +++ b/src/utils/getAllParts.ts @@ -0,0 +1,17 @@ +import { Character } from "../types"; + +const getAllParts = (strArray: Character[]): Character[][] => { + let allParts = []; + + for (let i = 0; i < strArray.length; i++) { + let parts = strArray + .map((_char, index) => strArray.slice(i, index + 1)) + .filter((part) => part.length); + + allParts.push(parts); + } + + return allParts.flat(); +}; + +export default getAllParts; diff --git a/src/utils/getDiff.ts b/src/utils/getDiff.ts deleted file mode 100644 index 5b3e5ba..0000000 --- a/src/utils/getDiff.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Diff, FilledString } from "../types"; - -const getDiff = (str1: FilledString, str2: FilledString) => { - let diff: Diff[] = []; - let lastFoundIndex = -1; - - str1.forEach((character: string | null, index: number) => { - let secondStringIndex = str2.findIndex((c) => c === character); - - if (secondStringIndex > lastFoundIndex) { - lastFoundIndex = index; - return; - } - - if (character) { - diff.push({ character, index }); - } - }); - - return diff; -}; - -export default getDiff; diff --git a/src/utils/hasBeenSlid.test.ts b/src/utils/hasBeenSlid.test.ts deleted file mode 100644 index 182eb6a..0000000 --- a/src/utils/hasBeenSlid.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import hasBeenSlid from "./hasBeenSlid"; -import toCharacters from "./toCharacters"; - -it("returns true when string has been shifted", () => { - let str1 = ["a", "b", "c", "d"]; - let str2 = ["b", "c", "d"]; - - let result = hasBeenSlid([toCharacters(str1), toCharacters(str2)], 2); - - expect(result).toBe(true); -}); - -it("returns false when index is out of range for a string", () => { - let str1 = ["a", "b", "c", "d"]; - let str2 = ["b", "c", "d"]; - - let result = hasBeenSlid([toCharacters(str1), toCharacters(str2)], 3); - - expect(result).toBe(false); -}); diff --git a/src/utils/hasBeenSlid.ts b/src/utils/hasBeenSlid.ts deleted file mode 100644 index a8658a2..0000000 --- a/src/utils/hasBeenSlid.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Character } from "../types"; - -let joinRange = ( - arr: Character[], - rangeStart: number, - rangeEnd: number -): string => { - return arr - .slice(rangeStart, rangeEnd) - .map((c) => c.value) - .join(""); -}; - -let isPartialSubstring = ( - divisor: Character[], - dividend: Character[], - index: number -) => { - let subString = joinRange(divisor, index, index + 2); - let regex = new RegExp(subString); - - if (subString.length < 2) { - return false; - } - - return regex.test(joinRange(dividend, 0, dividend.length)); -}; - -let hasBeenSlid = (arrs: Character[][], index: number) => { - let [clone1, clone2] = arrs.map((a) => [...a]); - - return ( - isPartialSubstring(clone1, clone2, index) || - isPartialSubstring(clone2, clone1, index) - ); -}; - -export default hasBeenSlid; diff --git a/src/utils/latestEmptyMatchingIndex.ts b/src/utils/latestEmptyMatchingIndex.ts deleted file mode 100644 index 29b9d35..0000000 --- a/src/utils/latestEmptyMatchingIndex.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Character } from "../types"; - -type latestEmptyMatchingIndexArgs = { - initialIndex: number; - arr1: Character[]; - arr2: Character[]; - character: Character; -}; - -const latestEmptyMatchingIndex = ({ - initialIndex, - arr1, - arr2, - character, -}: latestEmptyMatchingIndexArgs): number => { - if (arr1[initialIndex]?.value !== character?.value) { - return -1; - } - - let nextIndex = initialIndex + 1; - let nextCharacterIsSame = arr1[nextIndex]?.value === character.value; - let nextSearchedCharacterIsDifferent = - arr2[nextIndex]?.value !== character.value; - - // Only continue searching if the next "source" character is the same, - // AND the next character in the array we're searching is still different. - // If it's the same, there's no point in continuing to search. - if (nextCharacterIsSame && nextSearchedCharacterIsDifferent) { - return latestEmptyMatchingIndex({ - initialIndex: nextIndex, - arr1, - arr2, - character, - }); - } - - return initialIndex; -}; - -export default latestEmptyMatchingIndex; diff --git a/src/utils/toCharacters.test.ts b/src/utils/toCharacters.test.ts new file mode 100644 index 0000000..3c9d880 --- /dev/null +++ b/src/utils/toCharacters.test.ts @@ -0,0 +1,20 @@ +import toCharacters from "./toCharacters"; + +it("converts a string into characters", () => { + let result = toCharacters("hi"); + + expect(result).toEqual([ + { value: "h", index: 0, ref: null }, + { value: "i", index: 1, ref: null }, + ]); +}); + +it("sets ref when parameter is passed", () => { + let result = toCharacters("bye", true); + + expect(result).toEqual([ + { value: "b", index: 0, ref: expect.any(Symbol) }, + { value: "y", index: 1, ref: expect.any(Symbol) }, + { value: "e", index: 2, ref: expect.any(Symbol) }, + ]); +}); diff --git a/src/utils/toCharacters.ts b/src/utils/toCharacters.ts index f05d5f1..3c9d78a 100644 --- a/src/utils/toCharacters.ts +++ b/src/utils/toCharacters.ts @@ -1,8 +1,8 @@ import { Character } from "../types"; -const toCharacters = (arr: string[]): Character[] => { - return arr.map((c) => { - return { value: c, accountedFor: false }; +const toCharacters = (str: string, setRef = false): Character[] => { + return str.split("").map((char: string, index: number) => { + return { value: char, index, ref: setRef ? Symbol(char) : null }; }); };