56 Merge composite string insertion operations
3-partitions of q-contiguous operations
Definition: Let operation O represent insertions on a single given text field. Let the insertion intervals be recorded in an ordered list, expressed in post-insertion coordinates (i.e. q coordinates). We say that O is q-contiguous if there are no gaps in q-space between adjacent intervals. In other words, execution of O is equivalent to the insertion of a single string into the text field.
O = [...)[.........)[.)[..............)[....)
Note that if O contains no intervals, or only a single interval then we assume O is q-contiguous.
Definition: If O is q-contiguous, let O.q denote the q position of its left most character (if any) and O.size denote the total size of its intervals (i.e. the total number of characters inserted).
Definition: Given lists L1, L2, let L1 + L2 denote the concatenation of L1 and L2 (i.e. L2 appended to the end of L1). Note that concatenation is associative but not commutative.
Claim: L1 and L2 are q-contiguous and L1.q + L1.size = L2.q ⇔ (L1+L2) is q-contiguous.
Definition [x1,…,xn] denotes the list containing elements x1,x2,….,xn in that order.
Definition: Let O be nonempty and q-contiguous. A 3-partition of O is some partition of O into three parts as follows:
O = A+[b]+C
where b is an interval in O satisfying ¬ (∃ Ox ∈ Ψ(O) such that Ox → b).
It follows that
- A and C are q-contiguous (and possibly empty)
- A.q = O.q
- b.q = A.q + A.size
- C.q = b.q + b.size.
The condition ¬ (∃ Ox ∈ Ψ(O) such that Ox → b) shows that a valid factorisation exists that extracts L-factor [b] from O (because it doesn’t break causality). Therefore we can write the factorisation into contextually serialised left and right parts as follows:
O = [b’] ⊕>> (A+C)
The Lfactor algorithm involves decrementing the q-position of [b] as it ETs backward past A, resulting in b’.q = O.q. The RFactor algorithm doesn’t alter q positions (ie A.q and C.q are unchanged by factorisation), so we know a gap in q coordinates of b.size has appeared between A and C. Therefore the R-factor (A+C) is not q-contiguous if both A and C are non-empty.
Definition: A 3-partition is non-trivial if either A or C in the above definition is non empty.
The merge of concurrent q-contiguous operations at different positions
Let shift(O,n) represent the result of shifting all insertion intervals in O to the right by n characters.
- O1 is q-contiguous
- O2 = [b2] is a single interval
- O1 || O2
- O1 <> O2
- O1.q < O2.q
O1 ⊕<> O2 = O1 + shift(O2, O1.size)
(where + denotes list concatenation).
(n = 1) : This follows readily
(n → n+1) : Let there be a 3-partition O1=A1+[b1]+C1. As an L-factor b1 satisfies b1.q = O1.q < O2.q = b2.q, so b2 will be shifted to the right by b1.size as it ITs past b1. Since A1.size < O1.size, by the induction premise b1 will be shifted to the right by A1.size as it ITs past A1. This results in b2 shifted by b1.size + A1.size. Therefore at this point the shifted b2.q satisfies C1.q = O1.q + A1.size + b1.size < b2.q. Since C1.size < O1.size, by the induction premise b2 will be shifted to the right as it IT's past C1. This results in b2.q being shifted by a further C1.size. So overall b2 is shifted by O1.size = A1.size + b1.size + C1.size.
- O1 is q-contiguous
- O2 is q-contiguous
- O1 || O2
- O1 <> O2
- O1.q < O2.q
O1 ⊕<> O2 = O1 + shift(O2, O1.size)
Let there be a 3-partition O2=A2+[b2]+C2. As an L-factor b2 satisfies b2.q = O2.q > O1.q, so it follows that the first b2 will be shifted to the right by O1.size as it ITs past O1.
This leaves the R-factor (A2+C2) remaining in O2. If the next interval in temporal order is in A2 then as an LFactor it has position O2.q. Otherwise if it comes from C2 then as an LFactor it will have position O2.q + b2.size. It follows that it will be shifted to the right by O1.size as it ITs past all of O1.
Over time more gaps will appear in the remaining R-factor of O2, so extracted intervals (as LFactors) tend to have q positions shifted to the right relative to the original O2.q. Therefore they are always shifted by O1.size as they IT past O1.
Note that at no time was it necessary to compare siteids.
The merge of concurrent q-contiguous operations at the same position
We are interested in defining the merge of O1,O2 where O1,O2 are each q-contiguous and satisfy O1 || O2, O1 <> O2 and O1.q = O2.q.
Evidently the merge Osum = O1 ⊕<> O2 will also be q-contiguous and satisfy Osum.q = O1.q = O2.q, Osum.size = O1.size + O2.size.
Since O1 is q-contiguous, the q-position of each interval is determined by O1.q and the sizes of all the intervals that come before it. Similarly for O2 and Osum. This suggests that the recorded q-positions are redundant.
The goal is to define a merge algorithm that completely ignores q-position values, and instead uses them only implicitly by virtue of the order in which the insertion intervals are stored.
Merging an interval into a context equivalent, concurrent, coincident, contiguous operation
Let O1 be q-contiguous and have a 3-partition O1 = A1+[b1]+C1 and let O2 = [b2] where O1 <> O2 and O1|| O2. Let O1,O2 be coincident - meaning that O1.q = O2.q. To simplify the following exposition assume WLOG O1.q = O2.q = 0. We are interested in the merge of O2 into O1 using an algorithm that ignores q-position values.
Let O1 be factorised as follows
O1 = [b1] ⊕>> (A1+C1)
Then we know as an LFactor, b1 satisfies b1.q = 0.
The dual IT between O1,O2 first involves comparison of siteids of b1,b2 because they are coincident at q = 0.
Firstly assume b1.s < b2.s so b2 will appear on the right of b1 in the effects document. Under dual IT of b1,b2 we see that after transformation b1.q = 0 whereas b2.q = b1.size. b2 then dualITs against (A1+C1) which is not q-contiguous. Since b2.q = b1.size > 0 we see that the first interval of A1, having q = 0 is necessarily to the left of b2. This pushes b2 further to the right. In fact when b2 is compared to any subsequent intervals of A1, b2 will always be pushed further to the right. So eventually b2 IT’s past all of A1 resulting in b2.q = b1.size + A1.size. Now b2 will dualIT with C1 which also begins at q = A1.size + b1.size, so b2 and C1 are coincident. Therefore following our idea of ignoring q-positions in the merge algorithm, we see that b2 needs to be merged into C1 in a recursive fashion. The conclusion is that O1 ⊕<> O2 = A1 + [b1] + (C1 ⊕<> [b2]).
[todo: this is sloppy – we need to more formally bring in the idea that we have developed an algorithm that ignores q positions]
Otherwise assume b2.s < b1.s so b2 will appear on the left of b1 in the effects document. When b2 IT’s past b1 we end up with b2.q = 0. Therefore b2 will dualIT with (A1+C1) at the same q position. In fact we can do better than that because we know that b2 will appear to the left of b1 so it is sufficient to dual IT b2 with A1 only.
This suggests the following result:
Given 3-partition of O1 = A1+[b1]+C1 and O2 = [b2] where O1 <> O2 and O1 || O2 then
O1 ⊕<> O2 = (b1.s < b2.s) ? (A1+[b1]+ (C1 ⊕<> [b2])) : ((A1 ⊕<> [b2]) +[b1]+C1)
As the algorithm proceeds there is a range of intervals that narrows over time until we have found the proper insertion position.
The approach is similar to binary search in the way at each step we have a range of potential insertion positions, and using a comparison to some point within the range we narrow the search down to either the left or the right "half". However, binary search normally allows us to compare to a point midway in the range, but we have no such luck. Instead we must make comparisons to intervals in an order compatible with the partial ordering defined by the precedes relation. Therefore there is the potential to only eat away at the range one interval at a time.
Presumably on average this algorithm is O(n log n), and in the worst case is O(n2) where n is the number of intervals in O1. Maybe it doesn’t matter anyway because it’s not common to have many concurrent insertions in the same field at exactly the same position. [Actually what it we merge the work of two independent groups of users that have typed in thousands of characters into an initially empty document?]
Merge algorithm
Consider that (i1,i2) represents an open range of intervals in O1. In other words the range is exclusive of positions i1,i2 and i1 and i2 are respectively one before and one after sentinels. If the range is empty (i.e. next(i1) = i2) then we stop and insert at the position between i1 and i2. Otherwise we scan the intervals in (i1,i2) for the interval with the minimum temporal index u. This yields an interval j satisfying i1 < j < i2. Depending on the siteid comparison we either assign i1=j or else i2 = j. Note therefore that the range must get smaller at each iteration which guarantees that the loop will eventually terminate.
// Merge coincident interval x into contiguous range of intervals
// in (i1,i2)
void MergeCoincidentIntervalIntoContiguousRange(i1,i2,x)
{
while(next(i1) != i2)
{
scan j in (i1,i2) for smallest u
if (x.s < j.s) i2 = j; else i1 = j;
}
insert(i1,x); // insert x immediately after i1.
}
Example:
Let (s,u) denote an insertion interval with siteid s and temporal index u.
In the following we depict the insertion intervals ordered left to right by q position. Let s1 < s2 < s3 < s4 and u1 < u2 < u3
O1 intervals: (s1,u2) (s2,u3) (s4,u1)
O2 intervals: (s3,u1)
Consider that we pick the first (and only) interval from O2 which is (s3,u1). Our job is to find its insertion position amongst the intervals of O1. Initially there are 3 intervals and hence 4 possible insertion positions.
First we compare (s3,u1) with (s4,u1). Comparing siteids we deduce that (s3,u1) is to the left of (s4,u1). This narrows the potential insertion positions according to the intervals to the left of (s4,u1). I.e. (s1,u2) and (s2,u3). So now there are only 3 potential insertion positions.
Next we compare (s3,u1) with (s1,u2) and find that (s3,u1) must be to the right of (s1,u2). This leave us with only (s2,u3) in the range, and we deduce that (s3,u1) must be inserted between (s2,u3) and (s4,u1).
Assigning the temporal index as we merge operations
As we merge operations into a history buffer there should be no need to change the temporal index values of the existing intervals. In fact the basic idea is for the HB to record the next u, and as intervals are added to the HB we assign u values to these new intervals as required. This can be regarded as analogous to thinking of the HB in its conventional form of a growing linear list of operations ordered by u.
Merging of two 3-partitions
Let O1, O2 be q-contiguous and satisfy O1 <> O2 and O1 || O2. Let O1.q = O2.q = 0.
Our approach to merge O1,O2 is to merge one interval at a time from O2 into O1, until all intervals from O2 have been merged into O1. We process the intervals of O2 in temporal order. We expect that each time we merge an interval from O2 into O1, O1 will remain q-contiguous.
Let there be a 3-partition of O2 = A2+[b2 ]+C2. If we L-factorise b2 we get:
O2 = [b2] + (A2+C2)
In this form b2.q = A2.q = 0. A gap in q coordinates of b2.size has appeared between A2 and C2.
The merge of [b2] into O1 is straightforward (see section ?) and yields O1' containing b2 somewhere. However for the purposes of merging in the remaining intervals from O2, it is actually important to L-factorise [b2] from O1', which means a gap may well appear in the remaining RFactor of O1'.
After removal of b2 from O2 we are left with the following R-factor:
O2 := (A2 + C2)
Now we want to take the next 'b2' interval (in temporal order) from O2 as an LFactor. There are two cases. If it lies in A2 then in similar fashion the interval b2 to be extracted will satisfy b2.q = 0. However, if b2 is inside C2 then as an LFactor it will have b2.q > 0. In either case this LFactor of O2 is suitable for merging into the RFactor of O1' that doesn't contain the intervals that have already been added from O2.
Claim: When a b2 is L-factored out of O2, as it ETs backward through earlier intervals of O2, the gaps from missing intervals on its left are in effect summed in order to determine its resultant q position as an LFactor of O2. Then when it is merged with the relevant RFactor of O1, the gaps caused by the very same O2 intervals that have previously been added cause a neat book keeping effect such that the merge will in fact be applied to a q-contiguous section bounded below and above by the relevant b2 's appearing now on O1 according to their original q-position order in O2.
Upshot: We can implement an algorithm that is based around the idea that an interval is merged into a coincident, concurrent, contiguous range of intervals. To merge O2 into O1 we simply iterate through intervals of O2 in temporal order, and merge them into a range within O1 that is bounded by the intervals that have already been inserted so as to ensure we maintain the existing q-position order in O2.
Note that we should merge O2 into O1 if O2 has fewer intervals. Otherwise it is better to instead merge intervals from O1 into O2.
Merge algorithm
The interesting aspect here is how to efficiently keep track of the locations of the intervals from O2 that have been inserted so far into O1, so that when we insert the next interval from O2 we can quickly find the q-contiguous range of intervals from O1 into which it should be merged.
Assuming double linked lists in order of q-position, for a given interval from O2, we can quickly find the previous and next intervals in O2. However they may not have been inserted into O1 yet. So that doesn't really help us.
Consider that we separately record an array of pointers to the intervals from O2 that have already been inserted into O1. Then when we want to insert the next interval from O2 we can perform a binary search to find the insertion position with respect to the intervals from O2 that have already been inserted into O1. More to the point this gives us the intervals below and above. Now since these intervals have already been inserted into O1 they form part of the double linked list ordered by q-position. The upshot is that we have found a pair of iterators that bound the range in which the next insertion must be made.
In the following let X and Y be arrays of pointers to intervals.
// Merge O2 = (j1,j2) into O1 = (i1,i2) where O1,O2 are q-contiguous,
// concurrent and coincident.
void MergeCoincidentRanges(
Interval* i1,Interval* i2,
Interval* j1,Interval* j2)
{
Copy pointers to intervals in (j1,j2) to an array X;
Sort pointers to intervals in X by their u values;
Y = [];
for each j in X (in order of increasing u)
{
// Y is ordered by q-position. Insert j into Y using binary
// search according to q-position
// Returns pointers to intervals y1,y2 that straddle interval j.
// May return y1=NULL or y2=NULL (or both)
(y1,y2) = Y.insert(j); // insert j in Y
if (!y1) y1 = i1;
if (!y2) y2 = i2;
// Merge j into open range (y1,y2)
MergeCoincidentIntervalIntoContiguousRange(y1,y2,j);
}
}
Recursive formulation of the algorithm
Lemma: Let O1 = A1+C1, O2 = A2+C2, where A1,A2,C1, C2 are each q-contiguous. Let A1,A2 be q-coincident. Let there be a gap of x>0 characters between the end of A1 and the start of C1. Let there be the same sized gap x between A2 and C2. Then
(A1+C1) ⊕ (A2+C2) = (A1 ⊕ A2) + (C1 ⊕ C2)
Let there be a 3-partition of O2 as A2+[b2 ]+C2. As an L-Factor we know that b2.q = 0. Therefore b2 is coincident with O1 and by section ? we have an algorithm to find the insertion position of b2 within O1. Let is write this as
O1 = A1 + [b2] + C1.
i.e. the interval b2 we inserted into O1 can be written as a 3-partition of O1. This leaves us with O2 = A2+C2.
Claim: O1 ⊕ O2 = (A1 ⊕ A2) + [b2] + (C1 ⊕ C2)