ot 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []