Created
July 18, 2011 19:40
-
-
Save josephg/1090444 to your computer and use it in GitHub Desktop.
TP2 Operational transform code for text, in C
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// text-tp2.h | |
#import <Foundation/Foundation.h> | |
//typedef struct { | |
// NSString *str; | |
// int num_tombs; | |
// int tombs[]; | |
//} Doc; | |
typedef enum { | |
NONE = 0, | |
SKIP = 1, | |
INSERT_TOMBSTONES = 2, | |
INSERT_CHARACTERS = 3, | |
DELETE = 4 | |
} ComponentType; | |
typedef struct { | |
ComponentType type; | |
union { | |
NSUInteger num; | |
NSString *str; | |
}; | |
} OpComponent; | |
typedef struct { | |
int num_components; | |
int capacity; | |
OpComponent components[]; | |
} Op; | |
typedef Op Doc; | |
Op *tp2_text_create_op(); | |
void tp2_text_free_op(Op *op); | |
Op *tp2_text_op_from_components(int num, OpComponent components[]); | |
void tp2_text_print_op(Op *op); | |
Op *tp2_text_doc_from_JSON(NSArray *arr); | |
Op *tp2_text_op_from_JSON(NSArray *arr); | |
BOOL tp2_text_compare(Op *op1, Op *op2); | |
Op *tp2_text_apply(Doc *doc, Op *op); | |
Op *tp2_text_normalize(Op *op); | |
Op *tp2_text_transform(Op *op, Op *otherOp, BOOL isLefthand); | |
Op *tp2_text_prune(Op *op, Op *otherOp); | |
Op *tp2_text_compose(Op *op1, Op *op2); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// text-tp2.m | |
// TP2 text operational transform code for C | |
// This is a C rewrite of this: | |
// https://github.com/josephg/TP2/blob/master/src/text.coffee | |
#import <stdio.h> | |
#import "text-tp2.h" | |
// **** UTILITY CODE | |
static Op *increase_op_capacity_to(Op *op, int min_size) { | |
if (op->capacity < min_size) { | |
do { | |
op->capacity = op->capacity ? (op->capacity << 1) : 8; | |
} while (op->capacity < min_size); | |
op = realloc(op, sizeof(Op) + op->capacity * sizeof(OpComponent)); | |
} | |
return op; | |
} | |
static Op *increase_op_capacity(Op *op) { | |
return increase_op_capacity_to(op, op->capacity + 1); | |
} | |
Op *tp2_text_create_op() { | |
Op *op = malloc(sizeof(Op)); | |
*op = (Op){}; | |
return op; | |
} | |
static void component_free(OpComponent c) { | |
if (c.type == INSERT_CHARACTERS) { | |
[c.str release]; | |
} | |
} | |
void tp2_text_free_op(Op *op) { | |
for (int i = 0; i < op->num_components; i++) { | |
component_free(op->components[i]); | |
} | |
op->num_components = 0; | |
free(op); | |
} | |
static void print_op_component(OpComponent component) { | |
switch (component.type) { | |
case SKIP: | |
printf("Skip : %lu", component.num); | |
break; | |
case INSERT_TOMBSTONES: | |
printf("Insert tombs : %lu", component.num); | |
break; | |
case INSERT_CHARACTERS: | |
printf("Insert chars : %lu ('%s')", | |
[component.str length], [component.str cStringUsingEncoding:NSUTF8StringEncoding]); | |
break; | |
case DELETE: | |
printf("Delete : %lu", component.num); | |
break; | |
default: | |
break; | |
} | |
printf("\n"); | |
fflush(stdout); | |
} | |
void tp2_text_print_op(Op *op) { | |
for (int i = 0; i < op->num_components; i++) { | |
printf("%d.\t", i); | |
print_op_component(op->components[i]); | |
} | |
printf("\n"); | |
} | |
Op *tp2_text_doc_from_JSON(NSArray *arr) { | |
OpComponent components[[arr count]]; | |
for (int i = 0; i < [arr count]; i++) { | |
NSObject *obj = [arr objectAtIndex:i]; | |
if ([obj isKindOfClass:[NSString class]]) { | |
components[i] = (OpComponent){INSERT_CHARACTERS, .str=(NSString *)obj}; | |
} else { | |
int num = [(NSNumber *)obj intValue]; | |
components[i] = (OpComponent){INSERT_TOMBSTONES, .num=num}; | |
} | |
} | |
return tp2_text_op_from_components((int)[arr count], components); | |
} | |
Op *tp2_text_op_from_JSON(NSArray *arr) { | |
OpComponent components[[arr count]]; | |
for (int i = 0; i < [arr count]; i++) { | |
NSObject *obj = [arr objectAtIndex:i]; | |
if ([obj isKindOfClass:[NSNumber class]]) { | |
components[i] = (OpComponent){SKIP, .num=[(NSNumber *)obj intValue]}; | |
} else { | |
NSDictionary *dict = (NSDictionary *)obj; | |
NSNumber *d = [dict objectForKey:@"d"]; | |
NSObject *ins = [dict objectForKey:@"i"]; | |
if (d != nil) { | |
components[i] = (OpComponent){DELETE, .num=[d intValue]}; | |
} else { | |
if ([ins isKindOfClass:[NSNumber class]]) { | |
components[i] = (OpComponent){INSERT_TOMBSTONES, .num=[(NSNumber *)ins intValue]}; | |
} else { | |
components[i] = (OpComponent){INSERT_CHARACTERS, .str=(NSString *)ins}; | |
} | |
} | |
} | |
} | |
return tp2_text_op_from_components((int)[arr count], components); | |
} | |
BOOL tp2_text_compare(Op *op1, Op *op2) { | |
if (op1->num_components != op2->num_components) { | |
return NO; | |
} else { | |
for (int i = 0; i < op1->num_components; i++) { | |
OpComponent c1 = op1->components[i]; | |
OpComponent c2 = op2->components[i]; | |
if (c1.type != c2.type) { | |
return NO; | |
} | |
if (c1.type == INSERT_CHARACTERS) { | |
if (![c1.str isEqualToString:c2.str]) { | |
return NO; | |
} | |
} else { | |
if (c1.num != c2.num) { | |
return NO; | |
} | |
} | |
} | |
return YES; | |
} | |
} | |
static Op *append_raw(Op *op, OpComponent c) { | |
if (c.type == INSERT_CHARACTERS) { | |
[c.str retain]; | |
} | |
if (op->capacity == op->num_components) { | |
op = increase_op_capacity(op); | |
} | |
op->components[op->num_components++] = c; | |
return op; | |
} | |
static NSUInteger componentLength(OpComponent c) { | |
return c.type == INSERT_CHARACTERS ? [c.str length] : c.num; | |
} | |
// Append an op component to the end of the specified op. | |
static Op *append(Op *op, OpComponent c) { | |
//p "append #{i op} + #{i component}" | |
if (componentLength(c) == 0) { | |
return op; | |
} else if (op->num_components == 0) { | |
return append_raw(op, c); | |
} else { | |
OpComponent last = op->components[op->num_components - 1]; | |
if (c.type == last.type) { | |
if (c.type == INSERT_CHARACTERS) { | |
[op->components[op->num_components - 1].str autorelease]; | |
op->components[op->num_components - 1].str = | |
[[op->components[op->num_components - 1].str stringByAppendingString:c.str] retain]; | |
} else { | |
op->components[op->num_components - 1].num += c.num; | |
} | |
return op; | |
} else { | |
return append_raw(op, c); | |
} | |
} | |
//p "-> #{i op}" | |
//checkOp op | |
} | |
Op *tp2_text_op_from_components(int num, OpComponent components[]) { | |
Op *op = tp2_text_create_op(); | |
for (int i = 0; i < num; i++) { | |
op = append(op, components[i]); | |
} | |
return op; | |
} | |
// Returns a string with the error text, or null if the op is fine. | |
static void checkOp(Op *op) { | |
int lastType = -1; | |
for (int i = 0; i < op->num_components; i++) { | |
OpComponent c = op->components[i]; | |
if (c.type == lastType) { | |
[NSException raise:@"Invalid op" format:@"An op component is the same type as the previous op component"]; | |
} | |
switch(c.type) { | |
case SKIP: | |
case INSERT_TOMBSTONES: | |
case DELETE: | |
if (c.num <= 0) { | |
[NSException raise:@"Invalid op" format:@"Op component must have a positive length"]; | |
} | |
break; | |
case INSERT_CHARACTERS: | |
if ([c.str length] == 0) { | |
[NSException raise:@"Invalid op" format:@"Inserts must have a positive length"]; | |
} | |
break; | |
default: | |
[NSException raise:@"Invalid op" format:@"Unrecognised op component type %d", c.type]; | |
} | |
lastType = c.type; | |
} | |
} | |
typedef struct { | |
NSUInteger index; | |
NSUInteger offset; | |
} OpPosition; | |
// Take the next part (up to length maxlength) from the op. If maxlength is <0, there is no max. | |
// If insertsIndivisible is true, inserts (& insert tombstones) won't be separated. | |
// | |
// Returns a {type:NONE} component when the op is fully consumed. | |
static OpComponent take(Op *op, OpPosition *position, NSInteger maxlength, BOOL insertsIndivisable) { | |
// p "take #{maxlength} index: #{index} off: #{offset}" | |
if (position->index == op->num_components) { | |
return (OpComponent){}; | |
} | |
OpComponent e = op->components[position->index]; | |
NSUInteger length = componentLength(e); | |
if (maxlength < 0 | |
|| (insertsIndivisable && (e.type == INSERT_CHARACTERS || e.type == INSERT_TOMBSTONES))) { | |
maxlength = length - position->offset; | |
} else { | |
maxlength = MIN(maxlength, length - position->offset); | |
} | |
if (e.type == INSERT_CHARACTERS) { | |
e.str = [[e.str substringWithRange:NSMakeRange(position->offset, maxlength)] retain]; | |
} else { | |
e.num = maxlength; | |
} | |
position->offset += maxlength; | |
if (position->offset >= length) { | |
position->offset = 0; | |
position->index++; | |
} | |
return e; | |
} | |
// Apply the op to the document. The document is not modified in the process. | |
Op *tp2_text_apply(Doc *doc, Op *op) { | |
// p "Applying #{i op} to #{i doc}" | |
checkOp(op); | |
Doc *newDoc = tp2_text_create_op(); | |
OpPosition position = (OpPosition){}; | |
for (int i = 0; i < op->num_components; i++) { | |
OpComponent c = op->components[i]; | |
switch (c.type) { | |
case SKIP: { | |
NSUInteger len = c.num; | |
while (len > 0) { | |
OpComponent part = take(doc, &position, len, false); | |
newDoc = append(newDoc, part); | |
len -= componentLength(part); | |
} | |
break; | |
} | |
case INSERT_TOMBSTONES: | |
case INSERT_CHARACTERS: | |
newDoc = append(newDoc, c); | |
break; | |
case DELETE: { | |
NSUInteger len = c.num; | |
while (len > 0) { | |
OpComponent part = take(doc, &position, len, false); | |
//component_free(part); | |
len -= componentLength(part); | |
} | |
newDoc = append(newDoc, (OpComponent){INSERT_TOMBSTONES, .num=c.num}); | |
break; | |
} | |
default: | |
// Should never get here - checkOp(op) above will make sure the components are sweet. | |
break; | |
} | |
} | |
return newDoc; | |
} | |
// Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate | |
// adjacent inserts and deletes. | |
Op *tp2_text_normalize(Op *op) { | |
Op *newOp = tp2_text_create_op(); | |
for (int i = 0; i < op->num_components; i++) { | |
OpComponent c = op->components[i]; | |
newOp = append(newOp, c); | |
} | |
return newOp; | |
} | |
#define IS_INSERT(t) ((t) == INSERT_TOMBSTONES || (t) == INSERT_CHARACTERS) | |
// This is a helper method to transform and prune. goForwards is true for transform, false for prune. | |
static Op *transformer(Op *op, Op *otherOp, BOOL goForwards, BOOL isLefthand) { | |
// p "TRANSFORMER op #{i op} by #{i otherOp} (delta: #{idDelta}) forwards: #{goForwards}" | |
checkOp(op); | |
checkOp(otherOp); | |
Op *newOp = tp2_text_create_op(); | |
OpPosition position = (OpPosition){}; | |
for (int i = 0; i < otherOp->num_components; i++) { | |
OpComponent c = otherOp->components[i]; | |
NSUInteger length = componentLength(c); | |
if (IS_INSERT(c.type)) { | |
// Inserting. | |
if (goForwards) { | |
// Transforming | |
if (isLefthand == YES) { | |
while (position.index < op->num_components | |
&& IS_INSERT(op->components[position.index].type)) { | |
newOp = append(newOp, take(op, &position, -1, NO)); | |
} | |
} | |
newOp = append(newOp, (OpComponent){SKIP, .num=length}); | |
} else { | |
// Pruning. | |
while (length > 0) { | |
OpComponent chunk = take(op, &position, length, YES); | |
if (chunk.type == NONE) { | |
[NSException raise:@"Op wrong length" format:@"The transformed op is too short"]; | |
} else if (chunk.type == DELETE) { | |
[NSException raise:@"Op invalid" format:@"Transformed op deletes locally inserted characters - it cannot be purged of the insert."]; | |
} | |
if (chunk.type == SKIP) { | |
length -= chunk.num; | |
} else { | |
newOp = append(newOp, chunk); | |
} | |
} | |
} | |
} else { | |
// Skip or a delete. | |
while (length > 0) { | |
OpComponent chunk = take(op, &position, length, YES); | |
if (chunk.type == NONE) { | |
[NSException raise:@"Op wrong length" format:@"The op is too short"]; | |
} | |
newOp = append(newOp, chunk); | |
if (!IS_INSERT(chunk.type)) { | |
length -= componentLength(chunk); | |
} | |
} | |
} | |
} | |
while (position.index < op->num_components) { | |
OpComponent chunk = take(op, &position, -1, NO); | |
if (!IS_INSERT(chunk.type)) { | |
[NSException raise:@"Op wrong length" format:@"The op has extra fragments at the end"]; | |
} | |
newOp = append(newOp, chunk); | |
} | |
return newOp; | |
} | |
// transform op1 by op2. Return transformed version of op1. | |
// op1 and op2 are unchanged by transform. | |
// left handedness = usually client, right = server. | |
Op *tp2_text_transform(Op *op, Op *otherOp, BOOL isLefthand) { | |
return transformer(op, otherOp, YES, isLefthand); | |
} | |
// Prune is the inverse of transform. | |
Op *tp2_text_prune(Op *op, Op *otherOp) { | |
return transformer(op, otherOp, NO, YES /* handedness is irrelevant for prune */); | |
} | |
// Compose 2 ops into 1 op. | |
Op *tp2_text_compose(Op *op1, Op *op2) { | |
//p "COMPOSE #{i op1} + #{i op2}" | |
checkOp(op1); | |
checkOp(op2); | |
Op *result = tp2_text_create_op(); | |
OpPosition position = (OpPosition){}; | |
for (int i = 0; i < op2->num_components; i++) { | |
OpComponent c = op2->components[i]; | |
// p "component in op2 #{i component}" | |
switch (c.type) { | |
case SKIP: { | |
// Just copy from op1. | |
NSUInteger length = c.num; | |
while (length > 0) { | |
OpComponent chunk = take(op1, &position, length, NO); | |
if (chunk.type == NONE) { | |
[NSException raise:@"Op wrong length" format:@"The op is too short"]; | |
} | |
result = append(result, chunk); | |
length -= componentLength(chunk); | |
} | |
break; | |
} | |
case INSERT_CHARACTERS: | |
case INSERT_TOMBSTONES: | |
result = append(result, c); | |
break; | |
case DELETE: { | |
NSUInteger length = c.num; | |
while (length > 0) { | |
OpComponent chunk = take(op1, &position, length, NO); | |
if (chunk.type == NONE) { | |
[NSException raise:@"Op wrong length" format:@"The op is too short"]; | |
} | |
NSUInteger clength = componentLength(chunk); | |
if (IS_INSERT(chunk.type)) { | |
result = append(result, (OpComponent){INSERT_TOMBSTONES, .num=clength}); | |
} else { | |
result = append(result, (OpComponent){DELETE, .num=clength}); | |
} | |
length -= clength; | |
} | |
break; | |
} | |
default: | |
break; | |
} | |
} | |
while (position.index < op1->num_components) { | |
OpComponent chunk = take(op1, &position, -1, NO); | |
if (!IS_INSERT(chunk.type)) { | |
[NSException raise:@"Op wrong length" format:@"The op has extra fragments at the end"]; | |
} | |
result = append(result, chunk); | |
} | |
// p "= #{i result}" | |
return result; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment