Skip to content

Commit 4ec0aae

Browse files
committed
feat(json-ot): 🎸 add string type transform() function
1 parent 104e3ad commit 4ec0aae

File tree

2 files changed

+156
-1
lines changed

2 files changed

+156
-1
lines changed

src/json-ot/types/ot-string/StringType.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,19 @@ const chunk = (component: StringTypeOpComponent, offset: number, maxLength: numb
170170
}
171171
};
172172

173+
/**
174+
* Combine two operations into one, such that the changes produced by the
175+
* by the single operation are the same as if the two operations were applied
176+
* in sequence.
177+
*
178+
* ```
179+
* apply(str, combine(op1, op2)) === apply(apply(str, op1), op2)
180+
* ```
181+
*
182+
* @param op1 First operation.
183+
* @param op2 Second operation.
184+
* @returns A combined operation.
185+
*/
173186
export const compose = (op1: StringTypeOp, op2: StringTypeOp): StringTypeOp => {
174187
const op3: StringTypeOp = [];
175188
const len1 = op1.length;
@@ -237,3 +250,87 @@ export const compose = (op1: StringTypeOp, op2: StringTypeOp): StringTypeOp => {
237250
trim(op3);
238251
return op3;
239252
};
253+
254+
/**
255+
* Transforms an operation such that the transformed operations can be
256+
* applied to a string in reverse order.
257+
*
258+
* ```
259+
* apply(apply(doc, op1), transform(op2, op1)) === apply(apply(doc, op2), transform(op1, op2))
260+
* ```
261+
*
262+
* @param op1 The operation to transform.
263+
* @param op2 The operation to transform against.
264+
* @returns A new operation with user intentions preserved.
265+
*/
266+
export const transform = (op1: StringTypeOp, op2: StringTypeOp, leftInsertFirst: boolean): StringTypeOp => {
267+
const op3: StringTypeOp = [];
268+
const len1 = op1.length;
269+
const len2 = op2.length;
270+
let i1 = 0;
271+
let i2 = 0;
272+
let off1 = 0;
273+
for (; i2 < len2; i2++) {
274+
const comp2 = op2[i2];
275+
let doDelete = false;
276+
switch (typeof comp2) {
277+
case 'number': {
278+
if (comp2 > 0) {
279+
let length2 = comp2;
280+
while (length2 > 0) {
281+
const comp1 = op1[i1];
282+
const comp = i1 >= len1 ? length2 : chunk(comp1, off1, length2);
283+
const compLength = componentLength(comp);
284+
const length1 = componentLength(comp1 || comp);
285+
append(op3, comp);
286+
off1 += compLength;
287+
if (off1 >= length1) {
288+
i1++;
289+
off1 = 0;
290+
}
291+
if (typeof comp !== 'string') length2 -= compLength;
292+
}
293+
} else doDelete = true;
294+
break;
295+
}
296+
case 'string': {
297+
if (leftInsertFirst) {
298+
if (typeof op1[i1] === 'string') {
299+
const comp = chunk(op1[i1++], off1, Infinity);
300+
off1 = 0;
301+
append(op3, comp)
302+
}
303+
}
304+
append(op3, comp2.length);
305+
break;
306+
}
307+
case 'object': {
308+
doDelete = true;
309+
break;
310+
}
311+
}
312+
if (doDelete) {
313+
const isReversible = comp2 instanceof Array;
314+
const length2 = isReversible ? comp2[0].length : -comp2;
315+
let off2 = 0;
316+
while (off2 < length2) {
317+
const remaining = length2 - off2;
318+
const comp1 = op1[i1];
319+
const comp = i1 >= len1 ? remaining : chunk(comp1, off1, remaining);
320+
const compLength = componentLength(comp);
321+
const length1 = componentLength(comp1 || comp);
322+
if (typeof comp === 'string') append(op3, comp);
323+
else off2 += compLength;
324+
off1 += compLength;
325+
if (off1 >= length1) {
326+
i1++;
327+
off1 = 0;
328+
}
329+
}
330+
}
331+
}
332+
if (i1 < len1 && off1) append(op3, chunk(op1[i1++], off1, Infinity));
333+
for (; i1 < len1; i1++) append(op3, op1[i1]);
334+
trim(op3);
335+
return op3;
336+
};

src/json-ot/types/ot-string/__tests__/StringType.spec.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {validate, append, normalize, apply, compose} from '../StringType';
1+
import {validate, append, normalize, apply, compose, transform} from '../StringType';
22
import {StringTypeOp} from '../types';
33

44
describe('validate()', () => {
@@ -131,3 +131,61 @@ describe('compose()', () => {
131131
}
132132
});
133133
});
134+
135+
describe('transform()', () => {
136+
test('can transform two inserts', () => {
137+
const op1: StringTypeOp = [1, 'a'];
138+
const op2: StringTypeOp = [3, 'b'];
139+
const op3 = transform(op1, op2, true);
140+
const op4 = transform(op2, op1, false);
141+
expect(op3).toStrictEqual([1, 'a']);
142+
expect(op4).toStrictEqual([4, 'b']);
143+
});
144+
145+
test('insert at the same place', () => {
146+
const op1: StringTypeOp = [3, 'a'];
147+
const op2: StringTypeOp = [3, 'b'];
148+
const op3 = transform(op1, op2, true);
149+
const op4 = transform(op2, op1, false);
150+
expect(op3).toStrictEqual([3, 'a']);
151+
expect(op4).toStrictEqual([4, 'b']);
152+
});
153+
154+
test('can transform two deletes', () => {
155+
const op1: StringTypeOp = [1, -1];
156+
const op2: StringTypeOp = [3, -1];
157+
const op3 = transform(op1, op2, true);
158+
const op4 = transform(op2, op1, false);
159+
expect(op3).toStrictEqual([1, -1]);
160+
expect(op4).toStrictEqual([2, -1]);
161+
});
162+
163+
// type TestCase = [name: string, str: string, op1: StringTypeOp, op2: StringTypeOp, expected: string, only?: boolean];
164+
165+
// const testCases: TestCase[] = [
166+
// ['insert-insert', 'abc', [1, 'a'], [1, 'b'], 'ababc'],
167+
// ['insert-delete', 'abc', [1, 'a'], [1, -1], 'abc'],
168+
// ['insert-delete-2', 'abc', [1, 'a'], [2, -1], 'aac'],
169+
// ['insert in previous insert', 'aabb', [2, '1111'], [4, '22'], 'aa112211bb'],
170+
// ['fuzzer bug #1', 'd6', ['}B'], [['}'], ';0q', 2, ['6']], ';0qBd'],
171+
// ['fuzzer bug #2', 'Ai', [['A'], '#', -1], [-1], ''],
172+
// ['fuzzer bug #3', 'M}', ['!y1'], ["'/*s", 2, ',/@', -2, ['}']], "'/*s!y,/@"],
173+
// ['fuzzer bug #4', '8sL', [-2, 'w', ['L']], [['w']], ''],
174+
// ['fuzzer bug #5', '%V=', [2, ';'], ['3O"', 1, 'J', -2], '3O"%J='],
175+
// ];
176+
177+
// describe('can compose', () => {
178+
// for (const [name, str, op1, op2, expected, only] of testCases) {
179+
// (only ? test.only : test)(`${name}`, () => {
180+
// const res1 = apply(apply(str, op1), op2);
181+
// // console.log('res1', res1);
182+
// const op3 = compose(op1, op2);
183+
// // console.log('op3', op3);
184+
// const res2 = apply(str, op3);
185+
// // console.log('res2', res2);
186+
// expect(res2).toStrictEqual(res1);
187+
// expect(res2).toStrictEqual(expected);
188+
// });
189+
// }
190+
// });
191+
});

0 commit comments

Comments
 (0)