ot 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a2aa04ca759227fedfd134d4fe0fc7be9d81d2c6
4
+ data.tar.gz: d4ae2da9934dcc3969bca96f65c917b3edcb0ff3
5
+ SHA512:
6
+ metadata.gz: d81ac40bb41788e563cd2591e1d70efe13c1b329cb43294104b955a4093e5c783a3b83c3852e20b6a8a144ccdb8648de48fbae13c2500708505049f9aa5e20a0
7
+ data.tar.gz: 100feef5b48e4428da6e0c6e4c2c711402c6d12deadeca6f21066cac8bad5b6c4111feb7164aa798e638ebaf8eac86116f1a72fcbd4273ecd36aee964cd2a8aa
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
@@ -0,0 +1,11 @@
1
+ LineLength:
2
+ Enabled: false
3
+
4
+ RedundantReturn:
5
+ Enabled: false
6
+
7
+ SingleLineBlockParams:
8
+ Enabled: false
9
+
10
+ SingleSpaceBeforeFirstArg:
11
+ Enabled: false
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 2.1.7
5
+ - 2.2.3
6
+
7
+ before_install: gem install bundler -v 1.10.5
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ot.gemspec
4
+ gemspec
@@ -0,0 +1,23 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Hayden Ball, http://haydenball.me.uk
4
+
5
+ Original (ot.js) Copyright (c) 2012-2014 Tim Baumann, http://timbaumann.info
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
@@ -0,0 +1,113 @@
1
+ # ot.rb
2
+
3
+ [![Build Status](https://travis-ci.org/ball-hayden/ot.rb.svg)](https://travis-ci.org/ball-hayden/ot.rb)
4
+
5
+ This is a Ruby port of the <https://github.com/Operational-Transformation/ot.js>
6
+ Operational Transformation library.
7
+
8
+ At this time, only `TextOperation` has been ported.
9
+
10
+ ## Installation
11
+
12
+ Add this line to your application's Gemfile:
13
+
14
+ ```ruby
15
+ gem 'ot'
16
+ ```
17
+
18
+ And then execute:
19
+
20
+ $ bundle
21
+
22
+ Or install it yourself as:
23
+
24
+ $ gem install ot
25
+
26
+ ## Usage
27
+
28
+ ### TextOperation
29
+
30
+ `retain(n)`
31
+
32
+ Skip over a given number of charaters
33
+
34
+ `insert(string)`
35
+
36
+ Insert a string at the current position
37
+
38
+ `delete(n)`
39
+
40
+ Delete a string at the current position
41
+
42
+ `noop?`
43
+
44
+ Tests whether this operation has no effect.
45
+
46
+ `to_a`
47
+
48
+ Converts operation into an array value.
49
+ Note that this replaces the `toJSON` method in ot.js
50
+
51
+ `self.from_a(ops)`
52
+
53
+ Converts an array into an operation and validates it.
54
+ Note that this replaces the `fromJSON` method in ot.js
55
+
56
+ `apply(str)`
57
+
58
+ Apply an operation to a string, returning a new string. Throws an error if
59
+ there's a mismatch between the input string and the operation.
60
+
61
+ `invert(str)`
62
+
63
+ Computes the inverse of an operation. The inverse of an operation is the
64
+ operation that reverts the effects of the operation, e.g. when you have an
65
+ operation 'insert("hello "); skip(6);' then the inverse is 'delete("hello ");
66
+ skip(6);'. The inverse should be used for implementing undo.
67
+
68
+ `compose(operation2)`
69
+
70
+ Compose merges two consecutive operations into one operation, that
71
+ preserves the changes of both. Or, in other words, for each input string S
72
+ and a pair of consecutive operations A and B,
73
+ apply(apply(S, A), B) = apply(S, compose(A, B)) must hold.
74
+
75
+ `compose_with?(other)`
76
+
77
+ When you use ctrl-z to undo your latest changes, you expect the program not
78
+ to undo every single keystroke but to undo your last sentence you wrote at
79
+ a stretch or the deletion you did by holding the backspace key down. This
80
+ This can be implemented by composing operations on the undo stack. This
81
+ method can help decide whether two operations should be composed. It
82
+ returns true if the operations are consecutive insert operations or both
83
+ operations delete text at the same position. You may want to include other
84
+ factors like the time since the last change in your decision.
85
+
86
+ `compose_with_inverted?(other)`
87
+
88
+ Decides whether two operations should be composed with each other
89
+ if they were inverted, that is
90
+ `shouldBeComposedWith(a, b) = shouldBeComposedWithInverted(b^{-1}, a^{-1})`.
91
+
92
+ `transform(operation1, operation2)`
93
+
94
+ Transform takes two operations A and B that happened concurrently and
95
+ produces two operations A' and B' (in an array) such that
96
+ `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the
97
+ heart of OT.
98
+
99
+ ## Development
100
+
101
+ After checking out the repo, run `bundle install` to install dependencies. Then,
102
+ run `rake rspec` to run the tests. You can also run `bin/console` for an
103
+ interactive prompt that will allow you to experiment.
104
+
105
+ ## Contributing
106
+
107
+ Bug reports and pull requests are welcome on GitHub at
108
+ <https://github.com/ball-hayden/ot.rb>.
109
+
110
+ ## License
111
+
112
+ The gem is available as open source under the terms of the
113
+ [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ot"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
@@ -0,0 +1,5 @@
1
+ require 'ot/version'
2
+ require 'ot/text_operation'
3
+
4
+ module OT
5
+ end
@@ -0,0 +1,566 @@
1
+ module OT
2
+ class TextOperation
3
+ attr_reader :ops, :base_length, :target_length
4
+
5
+ def initialize
6
+ # When an operation is applied to an input string, you can think of this as
7
+ # if an imaginary cursor runs over the entire string and skips over some
8
+ # parts, deletes some parts and inserts characters at some positions. These
9
+ # actions (skip/delete/insert) are stored as an array in the "ops" property.
10
+ @ops = []
11
+ # An operation's base_length is the length of every string the operation
12
+ # can be applied to.
13
+ @base_length = 0
14
+ # The target_length is the length of every string that results from applying
15
+ # the operation on a valid input string.
16
+ @target_length = 0
17
+ end
18
+
19
+ def ==(other)
20
+ return false unless base_length == other.base_length
21
+ return false unless target_length == other.target_length
22
+ return false unless ops.length == other.ops.length
23
+
24
+ ops.length.times do |i|
25
+ return false unless ops[i] == other.ops[i]
26
+ end
27
+
28
+ return true
29
+ end
30
+
31
+ # Operation are essentially lists of ops. There are three types of ops:
32
+ #
33
+ # * Retain ops: Advance the cursor position by a given number of characters.
34
+ # Represented by positive ints.
35
+ # * Insert ops: Insert a given string at the current cursor position.
36
+ # Represented by strings.
37
+ # * Delete ops: Delete the next n characters. Represented by negative ints.
38
+
39
+ def self.retain_op?(op)
40
+ op.is_a?(Integer) && op > 0
41
+ end
42
+
43
+ def retain_op?(op)
44
+ TextOperation.retain_op?(op)
45
+ end
46
+
47
+ def self.insert_op?(op)
48
+ op.is_a? String
49
+ end
50
+
51
+ def insert_op?(op)
52
+ TextOperation.insert_op?(op)
53
+ end
54
+
55
+ def self.delete_op?(op)
56
+ op.is_a?(Integer) && op < 0
57
+ end
58
+
59
+ def delete_op?(op)
60
+ TextOperation.delete_op?(op)
61
+ end
62
+
63
+ # After an operation is constructed, the user of the library can specify the
64
+ # actions of an operation (skip/insert/delete) with these three builder
65
+ # methods. They all return the operation for convenient chaining.
66
+
67
+ # Skip over a given number of characters.
68
+ def retain(n)
69
+ fail 'retain expects an integer' unless n.is_a? Integer
70
+
71
+ return self if n == 0
72
+
73
+ @base_length += n
74
+ @target_length += n
75
+
76
+ if retain_op?(@ops.last)
77
+ # The last op is a retain op => we can merge them into one op.
78
+ @ops[-1] += n
79
+ else
80
+ # Create a new op.
81
+ @ops.push(n)
82
+ end
83
+
84
+ return self
85
+ end
86
+
87
+ # Insert a string at the current position.
88
+ def insert(str)
89
+ fail 'insert expects a string' unless str.is_a? String
90
+
91
+ return self if str == ''
92
+
93
+ @target_length += str.length
94
+
95
+ if insert_op?(ops.last)
96
+ # Merge insert op.
97
+ @ops[-1] += str
98
+ elsif delete_op?(ops.last)
99
+ # It doesn't matter when an operation is applied whether the operation
100
+ # is delete(3), insert("something") or insert("something"), delete(3).
101
+ # Here we enforce that in this case, the insert op always comes first.
102
+ # This makes all operations that have the same effect when applied to
103
+ # a document of the right length equal in respect to the `equals` method.
104
+ if insert_op?(ops[-2])
105
+ @ops[-2] += str
106
+ else
107
+ @ops.insert(-2, str)
108
+ end
109
+ else
110
+ @ops.push(str)
111
+ end
112
+
113
+ return self
114
+ end
115
+
116
+ # Delete a string at the current position.
117
+ def delete(n)
118
+ fail 'delete expects an integer or a string' unless n.is_a?(Integer) || n.is_a?(String)
119
+
120
+ n = n.length if n.is_a? String
121
+
122
+ return self if n == 0
123
+
124
+ n = -n if n > 0
125
+
126
+ @base_length -= n
127
+
128
+ if delete_op?(@ops.last)
129
+ @ops[-1] += n
130
+ else
131
+ @ops.push(n)
132
+ end
133
+
134
+ return self
135
+ end
136
+
137
+ # Tests whether this operation has no effect.
138
+ def noop?
139
+ return @ops.length == 0 || (@ops.length == 1 && retain_op?(@ops[0]))
140
+ end
141
+
142
+ # Pretty printing.
143
+ def to_s
144
+ # map: build a new array by applying a function to every element in an old
145
+ # array.
146
+ @ops.map do |op|
147
+ if retain_op?(op)
148
+ "retain #{op}"
149
+ elsif insert_op?(op)
150
+ "insert '#{op}'"
151
+ else
152
+ "delete #{-op}"
153
+ end
154
+ end.join(', ')
155
+ end
156
+
157
+ # Converts operation into an array value.
158
+ # Note that this replaces the toJSON method in ot.js
159
+ def to_a
160
+ return @ops
161
+ end
162
+
163
+ # Converts an array into an operation and validates it.
164
+ # Note that this replaces the fromJSON method in ot.js
165
+ def self.from_a(ops)
166
+ operation = TextOperation.new
167
+
168
+ ops.each do |op|
169
+ if retain_op?(op)
170
+ operation.retain(op)
171
+ elsif insert_op?(op)
172
+ operation.insert(op)
173
+ elsif delete_op?(op)
174
+ operation.delete(op)
175
+ else
176
+ fail 'unknown operation: ' + op.to_s
177
+ end
178
+ end
179
+
180
+ return operation
181
+ end
182
+
183
+ # Apply an operation to a string, returning a new string. Throws an error if
184
+ # there's a mismatch between the input string and the operation.
185
+ def apply(str)
186
+ if str.length != base_length
187
+ fail "The operation's base length must be equal to the string's length."
188
+ end
189
+
190
+ new_str = ''
191
+ str_index = 0
192
+
193
+ @ops.each do |op|
194
+ if retain_op?(op)
195
+ if (str_index + op) > str.length
196
+ fail "Operation can't retain more characters than are left in the string."
197
+ end
198
+
199
+ # Copy skipped part of the old string.
200
+ new_str += str.slice(str_index, op)
201
+ str_index += op
202
+ elsif insert_op?(op)
203
+ # Insert string.
204
+ new_str += op
205
+ else
206
+ # delete op
207
+ str_index -= op
208
+ end
209
+ end
210
+
211
+ if (str_index != str.length)
212
+ fail "The operation didn't operate on the whole string."
213
+ end
214
+
215
+ return new_str
216
+ end
217
+
218
+ # Computes the inverse of an operation. The inverse of an operation is the
219
+ # operation that reverts the effects of the operation, e.g. when you have an
220
+ # operation 'insert("hello "); skip(6);' then the inverse is 'delete("hello ");
221
+ # skip(6);'. The inverse should be used for implementing undo.
222
+ def invert(str)
223
+ str_index = 0
224
+ inverse = TextOperation.new
225
+
226
+ @ops.each do |op|
227
+ if retain_op?(op)
228
+ inverse.retain(op)
229
+ str_index += op
230
+ elsif insert_op?(op)
231
+ inverse.delete(op.length)
232
+ else # delete op
233
+ inverse.insert(str.slice(str_index, -op))
234
+ str_index -= op
235
+ end
236
+ end
237
+
238
+ return inverse
239
+ end
240
+
241
+ # Compose merges two consecutive operations into one operation, that
242
+ # preserves the changes of both. Or, in other words, for each input string S
243
+ # and a pair of consecutive operations A and B,
244
+ # apply(apply(S, A), B) = apply(S, compose(A, B)) must hold.
245
+ def compose(operation2)
246
+ operation1 = self
247
+ if operation1.target_length != operation2.base_length
248
+ fail 'The base length of the second operation has to be the target length of the first operation'
249
+ end
250
+
251
+ operation = TextOperation.new; # the combined operation
252
+
253
+ # for fast access
254
+ ops1 = operation1.ops
255
+ ops2 = operation2.ops
256
+
257
+ # current index into ops1 respectively ops2
258
+ i1 = 0
259
+ i2 = 0
260
+
261
+ # current ops
262
+ op1 = ops1[i1]
263
+ op2 = ops2[i2]
264
+
265
+ loop do
266
+ # Dispatch on the type of op1 and op2
267
+ if op1.nil? && op2.nil?
268
+ # end condition: both ops1 and ops2 have been processed
269
+ break
270
+ end
271
+
272
+ if delete_op?(op1)
273
+ operation.delete(op1)
274
+
275
+ op1 = ops1[i1 += 1]
276
+ next
277
+ end
278
+
279
+ if insert_op?(op2)
280
+ operation.insert(op2)
281
+
282
+ op2 = ops2[i2 += 1]
283
+ next
284
+ end
285
+
286
+ if op1.nil?
287
+ fail 'Cannot compose operations: first operation is too short.'
288
+ end
289
+ if op2.nil?
290
+ fail 'Cannot compose operations: first operation is too long.'
291
+ end
292
+
293
+ if retain_op?(op1) && retain_op?(op2)
294
+ if op1 > op2
295
+ operation.retain(op2)
296
+ op1 -= op2
297
+
298
+ op2 = ops2[i2 += 1]
299
+ elsif (op1 == op2)
300
+ operation.retain(op1)
301
+
302
+ op1 = ops1[i1 += 1]
303
+ op2 = ops2[i2 += 1]
304
+ else
305
+ operation.retain(op1)
306
+ op2 -= op1
307
+
308
+ op1 = ops1[i1 += 1]
309
+ end
310
+ elsif insert_op?(op1) && delete_op?(op2)
311
+ if op1.length > -op2
312
+ op1 = op1.slice(-op2, op1.length)
313
+ op2 = ops2[i2 += 1]
314
+ elsif (op1.length == -op2)
315
+ op1 = ops1[i1 += 1]
316
+ op2 = ops2[i2 += 1]
317
+ else
318
+ op2 += op1.length
319
+ op1 = ops1[i1 += 1]
320
+ end
321
+ elsif insert_op?(op1) && retain_op?(op2)
322
+ if op1.length > op2
323
+ operation.insert(op1.slice(0, op2))
324
+ op1 = op1.slice(op2, op1.length - op2)
325
+ op2 = ops2[i2 += 1]
326
+ elsif (op1.length == op2)
327
+ operation.insert(op1)
328
+ op1 = ops1[i1 += 1]
329
+ op2 = ops2[i2 += 1]
330
+ else
331
+ operation.insert(op1)
332
+ op2 -= op1.length
333
+ op1 = ops1[i1 += 1]
334
+ end
335
+ elsif retain_op?(op1) && delete_op?(op2)
336
+ if op1 > -op2
337
+ operation.delete(op2)
338
+ op1 += op2
339
+ op2 = ops2[i2 += 1]
340
+ elsif (op1 == -op2)
341
+ operation.delete(op2)
342
+ op1 = ops1[i1 += 1]
343
+ op2 = ops2[i2 += 1]
344
+ else
345
+ operation.delete(op1)
346
+ op2 += op1
347
+ op1 = ops1[i1 += 1]
348
+ end
349
+ else
350
+ fail "This shouldn't happen: op1: " +
351
+ JSON.stringify(op1) + ', op2: ' +
352
+ JSON.stringify(op2)
353
+ end
354
+ end
355
+
356
+ return operation
357
+ end
358
+
359
+ def self.get_simple_op(operation)
360
+ ops = operation.ops
361
+
362
+ case (ops.length)
363
+ when 1
364
+ return ops[0]
365
+ when 2
366
+ return retain_op?(ops[0]) ? ops[1] : (retain_op?(ops[1]) ? ops[0] : nil)
367
+ when 3
368
+ return ops[1] if retain_op?(ops[0]) && retain_op?(ops[2])
369
+ end
370
+
371
+ return
372
+ end
373
+
374
+ def self.get_start_index(operation)
375
+ return operation.ops[0] if retain_op?(operation.ops[0])
376
+ return 0
377
+ end
378
+
379
+ # When you use ctrl-z to undo your latest changes, you expect the program not
380
+ # to undo every single keystroke but to undo your last sentence you wrote at
381
+ # a stretch or the deletion you did by holding the backspace key down. This
382
+ # This can be implemented by composing operations on the undo stack. This
383
+ # method can help decide whether two operations should be composed. It
384
+ # returns true if the operations are consecutive insert operations or both
385
+ # operations delete text at the same position. You may want to include other
386
+ # factors like the time since the last change in your decision.
387
+ def compose_with?(other)
388
+ return true if noop? || other.noop?
389
+
390
+ start_a = TextOperation.get_start_index(self)
391
+ start_b = TextOperation.get_start_index(other)
392
+
393
+ simple_a = TextOperation.get_simple_op(self)
394
+ simple_b = TextOperation.get_simple_op(other)
395
+
396
+ return false unless simple_a && simple_b
397
+
398
+ if insert_op?(simple_a) && insert_op?(simple_b)
399
+ return start_a + simple_a.length == start_b
400
+ end
401
+
402
+ if delete_op?(simple_a) && delete_op?(simple_b)
403
+ # there are two possibilities to delete: with backspace and with the
404
+ # delete key.
405
+ return (start_b - simple_b == start_a) || start_a == start_b
406
+ end
407
+
408
+ return false
409
+ end
410
+
411
+ # Decides whether two operations should be composed with each other
412
+ # if they were inverted, that is
413
+ # `shouldBeComposedWith(a, b) = shouldBeComposedWithInverted(b^{-1}, a^{-1})`.
414
+ def compose_with_inverted?(other)
415
+ return true if noop? || other.noop?
416
+
417
+ start_a = TextOperation.get_start_index(self)
418
+ start_b = TextOperation.get_start_index(other)
419
+
420
+ simple_a = TextOperation.get_simple_op(self)
421
+ simple_b = TextOperation.get_simple_op(other)
422
+
423
+ return false unless simple_a && simple_b
424
+
425
+ if insert_op?(simple_a) && insert_op?(simple_b)
426
+ return start_a + simple_a.length == start_b || start_a == start_b
427
+ end
428
+
429
+ if delete_op?(simple_a) && delete_op?(simple_b)
430
+ return start_b - simple_b == start_a
431
+ end
432
+
433
+ return false
434
+ end
435
+
436
+ # Transform takes two operations A and B that happened concurrently and
437
+ # produces two operations A' and B' (in an array) such that
438
+ # `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the
439
+ # heart of OT.
440
+ def self.transform(operation1, operation2)
441
+ if (operation1.base_length != operation2.base_length)
442
+ fail 'Both operations have to have the same base length'
443
+ end
444
+
445
+ operation1prime = TextOperation.new
446
+ operation2prime = TextOperation.new
447
+
448
+ ops1 = operation1.ops
449
+ ops2 = operation2.ops
450
+
451
+ i1 = 0
452
+ i2 = 0
453
+
454
+ op1 = ops1[i1]
455
+ op2 = ops2[i2]
456
+
457
+ loop do
458
+ # At every iteration of the loop, the imaginary cursor that both
459
+ # operation1 and operation2 have that operates on the input string must
460
+ # have the same position in the input string.
461
+
462
+ if op1.nil? && op2.nil?
463
+ # end condition: both ops1 and ops2 have been processed
464
+ break
465
+ end
466
+
467
+ # next two cases: one or both ops are insert ops
468
+ # => insert the string in the corresponding prime operation, skip it in
469
+ # the other one. If both op1 and op2 are insert ops, prefer op1.
470
+ if insert_op?(op1)
471
+ operation1prime.insert(op1)
472
+ operation2prime.retain(op1.length)
473
+ op1 = ops1[i1 += 1]
474
+ next
475
+ end
476
+
477
+ if insert_op?(op2)
478
+ operation1prime.retain(op2.length)
479
+ operation2prime.insert(op2)
480
+ op2 = ops2[i2 += 1]
481
+ next
482
+ end
483
+
484
+ if op1.nil?
485
+ fail 'Cannot transform operations: first operation is too short.'
486
+ end
487
+ if op2.nil?
488
+ fail 'Cannot transform operations: first operation is too long.'
489
+ end
490
+
491
+ minl = nil
492
+
493
+ if retain_op?(op1) && retain_op?(op2)
494
+ # Simple case: retain/retain
495
+ if op1 > op2
496
+ minl = op2
497
+ op1 -= op2
498
+ op2 = ops2[i2 += 1]
499
+ elsif (op1 == op2)
500
+ minl = op2
501
+ op1 = ops1[i1 += 1]
502
+ op2 = ops2[i2 += 1]
503
+ else
504
+ minl = op1
505
+ op2 -= op1
506
+ op1 = ops1[i1 += 1]
507
+ end
508
+
509
+ operation1prime.retain(minl)
510
+ operation2prime.retain(minl)
511
+ elsif delete_op?(op1) && delete_op?(op2)
512
+ # Both operations delete the same string at the same position. We don't
513
+ # need to produce any operations, we just skip over the delete ops and
514
+ # handle the case that one operation deletes more than the other.
515
+ if -op1 > -op2
516
+ op1 -= op2
517
+ op2 = ops2[i2 += 1]
518
+ elsif (op1 == op2)
519
+ op1 = ops1[i1 += 1]
520
+ op2 = ops2[i2 += 1]
521
+ else
522
+ op2 -= op1
523
+ op1 = ops1[i1 += 1]
524
+ end
525
+ # next two cases: delete/retain and retain/delete
526
+ elsif delete_op?(op1) && retain_op?(op2)
527
+ if -op1 > op2
528
+ minl = op2
529
+ op1 += op2
530
+ op2 = ops2[i2 += 1]
531
+ elsif (-op1 == op2)
532
+ minl = op2
533
+ op1 = ops1[i1 += 1]
534
+ op2 = ops2[i2 += 1]
535
+ else
536
+ minl = -op1
537
+ op2 += op1
538
+ op1 = ops1[i1 += 1]
539
+ end
540
+
541
+ operation1prime.delete(minl)
542
+ elsif retain_op?(op1) && delete_op?(op2)
543
+ if op1 > -op2
544
+ minl = -op2
545
+ op1 += op2
546
+ op2 = ops2[i2 += 1]
547
+ elsif (op1 == -op2)
548
+ minl = op1
549
+ op1 = ops1[i1 += 1]
550
+ op2 = ops2[i2 += 1]
551
+ else
552
+ minl = op1
553
+ op2 += op1
554
+ op1 = ops1[i1 += 1]
555
+ end
556
+
557
+ operation2prime.delete(minl)
558
+ else
559
+ throw new Error("The two operations aren't compatible")
560
+ end
561
+ end
562
+
563
+ return [operation1prime, operation2prime]
564
+ end
565
+ end
566
+ end
@@ -0,0 +1,3 @@
1
+ module OT
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'ot/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'ot'
8
+ spec.version = OT::VERSION
9
+ spec.authors = ['Hayden Ball']
10
+ spec.email = ['hayden@haydenball.me.uk']
11
+
12
+ spec.summary = 'A Ruby port of the ot.js Operational Transformation library'
13
+ spec.homepage = 'https://github.com/ball-hayden/ot.rb'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = 'exe'
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ['lib']
20
+
21
+ spec.add_development_dependency 'bundler', '~> 1.10'
22
+ spec.add_development_dependency 'byebug'
23
+ spec.add_development_dependency 'rake', '~> 10.0'
24
+ spec.add_development_dependency 'rspec'
25
+ spec.add_development_dependency 'rubocop'
26
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hayden Ball
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2015-10-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: byebug
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ description:
84
+ email:
85
+ - hayden@haydenball.me.uk
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - ".rubocop.yml"
93
+ - ".travis.yml"
94
+ - Gemfile
95
+ - LICENSE.txt
96
+ - README.md
97
+ - Rakefile
98
+ - bin/console
99
+ - lib/ot.rb
100
+ - lib/ot/text_operation.rb
101
+ - lib/ot/version.rb
102
+ - ot.gemspec
103
+ homepage: https://github.com/ball-hayden/ot.rb
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '0'
121
+ requirements: []
122
+ rubyforge_project:
123
+ rubygems_version: 2.4.8
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: A Ruby port of the ot.js Operational Transformation library
127
+ test_files: []