diff --git a/src/components/collaborative-editing/diff/createOperationsByStringsDiff.ts b/src/components/collaborative-editing/diff/createOperationsByStringsDiff.ts new file mode 100644 index 00000000..ed3f5f4f --- /dev/null +++ b/src/components/collaborative-editing/diff/createOperationsByStringsDiff.ts @@ -0,0 +1,100 @@ +import { InsertOperation, OperationType, RemoveOperation } from './types'; + +/** + * Function finds difference between two different strings + * + * @param str1 - original string + * @param str2 - new string that comes from the first + * + * @returns {object[]} array of operations + */ +export function createOperationsByStringsDiff(str1: string, str2: string): (InsertOperation | RemoveOperation)[] { + if (str1 === str2) { + return []; + } + + const length1 = str1.length; + const length2 = str2.length; + + /** + * Left border of changes + */ + let left = 0; + + while (str1[left] === str2[left]) { + left++; + } + + /** + * Right border of changes + */ + let right = 0; + + while (str1[length1 - right - 1] === str2[length2 - right - 1]) { + right++; + } + + /** + * Length of difference in the first string + */ + const diffLength1 = length1 - left - right; + + /** + * Length of difference in the second string + */ + const diffLength2 = length2 - left - right; + + /** + * There are differences in two strings + * User replaced values by new + */ + if (diffLength1 > 0 && diffLength2 > 0) { + const before = str1.slice(left, length1 - right); + const after = str2.slice(left, length2 - right); + + return [ + { + type: OperationType.Remove, + index: left, + data: before, + }, + { + type: OperationType.Insert, + index: left, + data: after, + }, + ]; + } + + /** + * There is a difference only in the second string + * Used inserted new values + */ + if (diffLength2 > 0) { + const data = str2.slice(left, length2 - right + (left + right - length1)); + + return [ + { + type: OperationType.Insert, + index: left, + data, + }, + ]; + } + + /** + * There is a difference only in the first string + * Used removed values from it + */ + if (diffLength1 > 0) { + const data = str1.slice(left, length1 - right + (left + right - length2)); + + return [ + { + type: OperationType.Remove, + index: left, + data, + }, + ]; + } +} diff --git a/src/components/collaborative-editing/diff/types/Operation.ts b/src/components/collaborative-editing/diff/types/Operation.ts new file mode 100644 index 00000000..d83a9912 --- /dev/null +++ b/src/components/collaborative-editing/diff/types/Operation.ts @@ -0,0 +1,18 @@ +import { OperationType } from './OperationType'; + +export interface Operation { + /** + * Type of the operation + */ + type: OperationType; + + /** + * Start index of changes + */ + index: number; + + /** + * Changed data + */ + data: DATA_TYPE; +} diff --git a/src/components/collaborative-editing/diff/types/OperationType.ts b/src/components/collaborative-editing/diff/types/OperationType.ts new file mode 100644 index 00000000..2e4fd48b --- /dev/null +++ b/src/components/collaborative-editing/diff/types/OperationType.ts @@ -0,0 +1,14 @@ +/** + * Possible operation types + */ +export enum OperationType { + /** + * User inserts symbols + */ + Insert, + + /** + * User removes symbols + */ + Remove, +} diff --git a/src/components/collaborative-editing/diff/types/index.ts b/src/components/collaborative-editing/diff/types/index.ts new file mode 100644 index 00000000..833ab29a --- /dev/null +++ b/src/components/collaborative-editing/diff/types/index.ts @@ -0,0 +1,3 @@ +export * from './OperationType'; +export * from './operations/InsertOperation'; +export * from './operations/RemoveOperation'; diff --git a/src/components/collaborative-editing/diff/types/operations/InsertOperation.ts b/src/components/collaborative-editing/diff/types/operations/InsertOperation.ts new file mode 100644 index 00000000..61fabde2 --- /dev/null +++ b/src/components/collaborative-editing/diff/types/operations/InsertOperation.ts @@ -0,0 +1,9 @@ +import { Operation } from '../Operation'; +import { OperationType } from '../OperationType'; + +/** + * Operation of inserting some data inside + */ +export interface InsertOperation extends Operation { + type: OperationType.Insert; +} diff --git a/src/components/collaborative-editing/diff/types/operations/RemoveOperation.ts b/src/components/collaborative-editing/diff/types/operations/RemoveOperation.ts new file mode 100644 index 00000000..4c64e002 --- /dev/null +++ b/src/components/collaborative-editing/diff/types/operations/RemoveOperation.ts @@ -0,0 +1,9 @@ +import { Operation } from '../Operation'; +import { OperationType } from '../OperationType'; + +/** + * Operation of removing some data + */ +export interface RemoveOperation extends Operation { + type: OperationType.Remove; +} diff --git a/test/cypress/tests/collaborative-editing/diff/createOperationsByStringsDiff.spec.ts b/test/cypress/tests/collaborative-editing/diff/createOperationsByStringsDiff.spec.ts new file mode 100644 index 00000000..43d6a893 --- /dev/null +++ b/test/cypress/tests/collaborative-editing/diff/createOperationsByStringsDiff.spec.ts @@ -0,0 +1,243 @@ +import { createOperationsByStringsDiff } from '../../../../../src/components/collaborative-editing/diff/createOperationsByStringsDiff'; +import { OperationType } from '../../../../../src/components/collaborative-editing/diff/types'; + +describe('createOperationsByStringsDiff function', () => { + describe('User inserts symbols', () => { + it('should create Insert operation when a user inserts symbols at the start of the string', () => { + const before = 'string'; + const after = 'Changed string'; + const expected = [ + { + type: OperationType.Insert, + index: 0, + data: 'Changed ', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Insert operation when a user inserts symbols inside the string', () => { + const before = 'string'; + const after = 'stri123ng'; + const expected = [ + { + type: OperationType.Insert, + index: 4, + data: '123', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Insert operation when a user inserts symbols at the end of the string', () => { + const before = 'string'; + const after = 'string is changed'; + const expected = [ + { + type: OperationType.Insert, + index: 6, + data: ' is changed', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Insert operation when a user inserts symbols inside the empty string', () => { + const before = ''; + const after = 'String'; + const expected = [ + { + type: OperationType.Insert, + index: 0, + data: 'String', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + }); + + describe('User removes symbols', () => { + it('should create Remove operation when a user removes symbols at the start of the string', () => { + const before = 'string'; + const after = 'ing'; + const expected = [ + { + type: OperationType.Remove, + index: 0, + data: 'str', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Remove operation when a user removes symbols from the center of the string', () => { + const before = 'String is changed'; + const after = 'String changed'; + const expected = [ + { + type: OperationType.Remove, + index: 7, + data: 'is ', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Remove operation when a user removes symbols at the end of the string', () => { + const before = 'String is changed'; + const after = 'String is'; + const expected = [ + { + type: OperationType.Remove, + index: 9, + data: ' changed', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Remove operation when a user removes full string', () => { + const before = 'String'; + const after = ''; + const expected = [ + { + type: OperationType.Remove, + index: 0, + data: 'String', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + }); + + describe('User replaces symbols', () => { + it('should create Remove and Insert operations when a user replaces symbols at the start of the string', () => { + const before = 'string'; + const after = 'aaing'; + const expected = [ + { + type: OperationType.Remove, + index: 0, + data: 'str', + }, + { + type: OperationType.Insert, + index: 0, + data: 'aa', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Remove and Insert operations when a user replaces symbols from the center of the string', () => { + const before = 'abcaafd'; + const after = 'abceecfd'; + const expected = [ + { + type: OperationType.Remove, + index: 3, + data: 'aa', + }, + { + type: OperationType.Insert, + index: 3, + data: 'eec', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Remove and Insert operations when a user replaces symbols at the end of the string', () => { + const before = 'abcdef'; + const after = 'abcaa'; + const expected = [ + { + type: OperationType.Remove, + index: 3, + data: 'def', + }, + { + type: OperationType.Insert, + index: 3, + data: 'aa', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Remove and Insert operations when a user replaces full string', () => { + const before = 'abcd'; + const after = 'efg'; + const expected = [ + { + type: OperationType.Remove, + index: 0, + data: 'abcd', + }, + { + type: OperationType.Insert, + index: 0, + data: 'efg', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + + it('should create Remove and Insert operations when there are two changes at the start and the end of the string', () => { + const before = 'String is changed'; + const after = 'Number is changed many times'; + const expected = [ + { + type: OperationType.Remove, + index: 0, + data: 'String is changed', + }, + { + type: OperationType.Insert, + index: 0, + data: 'Number is changed many times', + }, + ]; + + const result = createOperationsByStringsDiff(before, after); + + expect(result).deep.equal(expected); + }); + }); +});