basic-rope 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +19 -0
- data/README.textile +69 -0
- data/Rakefile +2 -0
- data/basic-rope.gemspec +22 -0
- data/benchmark.rb +53 -0
- data/lib/rope.rb +16 -0
- data/lib/rope/basic_node.rb +36 -0
- data/lib/rope/basic_rope.rb +109 -0
- data/lib/rope/interior_node.rb +170 -0
- data/lib/rope/leaf_node.rb +48 -0
- data/lib/rope/node.rb +27 -0
- data/lib/rope/string_methods.rb +6 -0
- data/lib/rope/version.rb +3 -0
- data/spec/concatenation_spec.rb +35 -0
- data/spec/dup_spec.rb +14 -0
- data/spec/initialization_spec.rb +15 -0
- data/spec/replace_spec.rb +62 -0
- data/spec/slice_spec.rb +83 -0
- metadata +102 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6643ff0ca0cec28bee1440332fc9d71c3ce7f67f
|
4
|
+
data.tar.gz: c7563f1d28ee7587b850ed0cc7d4d454b2e29288
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 27581ca183715f80cc38c1b358ff9356f478d589c40bbf0ff7e0fb4c91002058423e32b9089bd304d6df6aa215f52e22fa5d2b88992e7d496d56afac0ba23579
|
7
|
+
data.tar.gz: 6e7031ddabc83fc06940f147e155ab8a14dacac15f6f0b67d2b53e58bba206ef2f5aaf21b4d16f0e438ecf185d70b74eb0fab5acdfe7598a50a6c800ff6ebe7d
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (c) 2010 Andy Lindeman
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
5
|
+
in the Software without restriction, including without limitation the rights
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
THE SOFTWARE.
|
data/README.textile
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
p{color:red}. *rope* is still under early development. Check it out if you wish to follow my progress or help, but do not use it in your applications (yet!).
|
2
|
+
|
3
|
+
*rope* is a pure Ruby implementation of the "Rope data structure":http://en.wikipedia.org/wiki/Rope_%28computer_science%29.
|
4
|
+
|
5
|
+
For many applications, the *Rope* class can be a drop-in replacement for *String* that is optimized for certain use cases.
|
6
|
+
|
7
|
+
Using a *Rope* instance over a *String* may be desirable in applications that manipulate large amounts of text.
|
8
|
+
|
9
|
+
*rope* currently offers:
|
10
|
+
* Fast string concatenation and substring operations involving large strings
|
11
|
+
* Immutability, which is desirable for functional programming techniques or multithreaded applications
|
12
|
+
|
13
|
+
Planned features for *rope*:
|
14
|
+
* Ability to view a function producing characters as a Rope (including I/O operations). For instance, a piece of a Rope may be a 100MB file, but is only read when that section of the string is examined. Concatenating to the end of that Rope does not involve reading the entire file.
|
15
|
+
* Implement a *Rope* counterpart to every immutable method available on the *String* class.
|
16
|
+
|
17
|
+
Disadvantages of *rope*:
|
18
|
+
* Single character replacements are expensive
|
19
|
+
* Iterating character-by-character is slightly more expensive than in a String (TODO: how much? .. haven't implemented iterators yet)
|
20
|
+
|
21
|
+
h1. Installation
|
22
|
+
|
23
|
+
*rope* is hosted on "rubygems":http://www.rubygems.org/
|
24
|
+
|
25
|
+
<pre>
|
26
|
+
gem install rope
|
27
|
+
</pre>
|
28
|
+
|
29
|
+
... or in your Gemfile
|
30
|
+
|
31
|
+
<pre>
|
32
|
+
gem 'rope'
|
33
|
+
</pre>
|
34
|
+
|
35
|
+
*rope* is tested against MRI 1.8.7 and 1.9.2.
|
36
|
+
|
37
|
+
h1. Usage
|
38
|
+
|
39
|
+
h2. Creating a Rope
|
40
|
+
|
41
|
+
<pre>
|
42
|
+
rope = "123456789".to_rope # Rope::Rope.new("123456789") also works
|
43
|
+
|
44
|
+
puts rope # "123456789"
|
45
|
+
</pre>
|
46
|
+
|
47
|
+
h2. Concatenation
|
48
|
+
|
49
|
+
A *Rope* instance can be concatenated with another *Rope* or *String* instance.
|
50
|
+
|
51
|
+
<pre>
|
52
|
+
rope = "12345"
|
53
|
+
string = "6789"
|
54
|
+
|
55
|
+
rope += string
|
56
|
+
puts rope # "123456789"
|
57
|
+
</pre>
|
58
|
+
|
59
|
+
h2. Slices/Substrings
|
60
|
+
|
61
|
+
A *Rope* instance offers efficient substring operations. The *slice* and *[]* methods are synonymous with their "String counterparts (Ruby API documentation)":http://ruby-doc.org/core-1.9/classes/String.html#M000293.
|
62
|
+
|
63
|
+
<pre>
|
64
|
+
rope = "123456789".to_rope
|
65
|
+
|
66
|
+
puts rope.slice(3, 4) # 4567
|
67
|
+
puts rope.slice(-6, 4) # 4567
|
68
|
+
# TODO: More examples when they are implemented
|
69
|
+
</pre>
|
data/Rakefile
ADDED
data/basic-rope.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "rope/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "basic-rope"
|
7
|
+
s.version = Rope::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Andy Lindeman", "Logan Bowers"]
|
10
|
+
s.email = ["alindeman@gmail.com", "logan@datacurrent.com"]
|
11
|
+
s.homepage = "http://rubygems.org/gems/basic-rope"
|
12
|
+
s.summary = %q{Pure Ruby implementation of a Rope data structure}
|
13
|
+
s.description = %q{A Rope is a convenient data structure for manipulating large amounts of text. This implementation is inspired by http://www.rubyquiz.com/quiz137.html and https://rubygems.org/gems/cord. Basic-rope is a fork of Andy Lindeman's original rope gem to generalize the rope data structure to work with any data type that has a length and supports slice(). }
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_development_dependency "rspec"
|
21
|
+
s.add_development_dependency "rdoc"
|
22
|
+
end
|
data/benchmark.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Benchmarking code from
|
2
|
+
# http://www.rubyquiz.com/quiz137.html
|
3
|
+
|
4
|
+
# Compare:
|
5
|
+
# ruby benchmark.rb
|
6
|
+
# ruby -r lib/rope.rb -I lib benchmark.rb Rope::Rope
|
7
|
+
|
8
|
+
require 'benchmark'
|
9
|
+
|
10
|
+
#This code make a String/Rope of CHUNCKS chunks of text
|
11
|
+
#each chunck is SIZE bytes long. Each chunck starts with
|
12
|
+
#an 8 byte number. Initially the chuncks are shuffled the
|
13
|
+
#qsort method sorts them into ascending order.
|
14
|
+
|
15
|
+
puts 'preparing data...'
|
16
|
+
TextClass = eval(ARGV.shift || "String")
|
17
|
+
|
18
|
+
def qsort(text)
|
19
|
+
return TextClass.new if text.length == 0
|
20
|
+
pivot = text.slice(0,8).to_s.to_i
|
21
|
+
less = TextClass.new
|
22
|
+
more = TextClass.new
|
23
|
+
offset = 8+SIZE
|
24
|
+
while (offset < text.length)
|
25
|
+
i = text.slice(offset,8).to_s.to_i
|
26
|
+
(i < pivot ? less : more) << text.slice(offset,8+SIZE)
|
27
|
+
offset = offset + 8+SIZE
|
28
|
+
end
|
29
|
+
print "*"
|
30
|
+
return qsort(less) << text.slice(0,8+SIZE) << qsort(more)
|
31
|
+
end
|
32
|
+
|
33
|
+
SIZE = 512 * 1024
|
34
|
+
CHUNCKS = 128
|
35
|
+
CHARS = %w[R O P E]
|
36
|
+
data = TextClass.new
|
37
|
+
bulk_string =
|
38
|
+
TextClass.new(Array.new(SIZE) { CHARS[rand(4)] }.join)
|
39
|
+
puts 'Building Text...'
|
40
|
+
build = Benchmark.measure do
|
41
|
+
(0..CHUNCKS).sort_by { rand }.each do |n|
|
42
|
+
data<< sprintf("%08i",n) << bulk_string
|
43
|
+
end
|
44
|
+
data.normalize if data.respond_to? :normalize
|
45
|
+
end
|
46
|
+
GC.start
|
47
|
+
sort = Benchmark.measure do
|
48
|
+
puts "Sorting Text..."
|
49
|
+
qsort(data)
|
50
|
+
puts"\nEND"
|
51
|
+
end
|
52
|
+
|
53
|
+
puts "Build: #{build}Sort: #{sort}"
|
data/lib/rope.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
require 'rope/basic_rope'
|
4
|
+
|
5
|
+
require 'rope/string_methods'
|
6
|
+
|
7
|
+
module Rope
|
8
|
+
Rope = BasicRope.rope_for_type(String) do
|
9
|
+
#
|
10
|
+
# Special case for string Ropes since to_s is universal
|
11
|
+
#
|
12
|
+
def to_s
|
13
|
+
to_primitive
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Rope
|
2
|
+
class BasicNode
|
3
|
+
# Length of the underlying data in the tree and its descendants
|
4
|
+
attr_reader :length
|
5
|
+
|
6
|
+
# Depth of the tree
|
7
|
+
attr_reader :depth
|
8
|
+
|
9
|
+
# Concatenates this tree with another tree (non-destructive to either
|
10
|
+
# tree)
|
11
|
+
def +(other)
|
12
|
+
InteriorNode.new(self, other)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Gets the string representation of the underlying data in the tree
|
16
|
+
def to_primitive
|
17
|
+
data
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the Node that contains this index or nil if the index is out of bounds
|
21
|
+
def segment(index)
|
22
|
+
raise NotImplementedError
|
23
|
+
end
|
24
|
+
|
25
|
+
# Rebalances this tree
|
26
|
+
def rebalance!
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
#Swaps out the data in this node to be rhs. Must be same length
|
31
|
+
def replace!(index, length, rhs)
|
32
|
+
raise NotImplementedError
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'rope/leaf_node'
|
2
|
+
require 'rope/interior_node'
|
3
|
+
|
4
|
+
module Rope
|
5
|
+
# BasicRope is a data-type-agnostic Rope data structure. The data type is
|
6
|
+
# typically a string but can be anything that implements the following
|
7
|
+
# methods:
|
8
|
+
#
|
9
|
+
# * length - integer length of a unit of the data type
|
10
|
+
# * slice - break a unit of the data type into two pieces on an index boundary
|
11
|
+
# * + - join two pieces of the data type together
|
12
|
+
# * [] - alias for slice
|
13
|
+
#
|
14
|
+
class BasicRope
|
15
|
+
extend Forwardable
|
16
|
+
|
17
|
+
def self.rope_for_type(type, &block)
|
18
|
+
Class.new(self) do
|
19
|
+
define_method :primitive_type do
|
20
|
+
type
|
21
|
+
end
|
22
|
+
|
23
|
+
class_eval &block if block_given?
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Initializes a new rope
|
28
|
+
def initialize(arg=nil)
|
29
|
+
case arg
|
30
|
+
when BasicNode
|
31
|
+
@root = arg
|
32
|
+
when NilClass
|
33
|
+
@root = LeafNode.new(primitive_type.new)
|
34
|
+
when primitive_type
|
35
|
+
@root = LeafNode.new(arg)
|
36
|
+
when self.class, InteriorNode
|
37
|
+
@root = LeafNode.new(arg.to_primitive)
|
38
|
+
else
|
39
|
+
raise ArgumentError, "#{arg} is not a #{primitive_type}"
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def_delegators :@root, :to_primitive, :length, :rebalance!, :segment
|
44
|
+
|
45
|
+
# Concatenates this rope with another rope or string
|
46
|
+
def +(other)
|
47
|
+
self.class.new(concatenate(other))
|
48
|
+
end
|
49
|
+
|
50
|
+
# Tests whether this rope is equal to another rope
|
51
|
+
def ==(other)
|
52
|
+
to_primitive == (BasicRope === other ? other.to_primitive : other)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Creates a copy of this rope
|
56
|
+
def dup
|
57
|
+
root.freeze #Prevents errors when
|
58
|
+
self.class.new(root)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Gets a slice of this rope
|
62
|
+
def slice(*args)
|
63
|
+
slice = root.slice(*args)
|
64
|
+
|
65
|
+
case slice
|
66
|
+
when Fixnum # slice(Fixnum) returns a plain Fixnum
|
67
|
+
slice
|
68
|
+
when BasicNode, primitive_type # create a new Rope with the returned tree as the root
|
69
|
+
self.class.new(slice)
|
70
|
+
else
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
alias :[] :slice
|
75
|
+
|
76
|
+
def []=(index, length=1, rhs)
|
77
|
+
@root = @root.replace!(index, length, rhs)
|
78
|
+
self
|
79
|
+
end
|
80
|
+
|
81
|
+
|
82
|
+
def <<(rhs)
|
83
|
+
self[length] = rhs
|
84
|
+
end
|
85
|
+
protected
|
86
|
+
|
87
|
+
# Root node (could either be a LeafNode or some child of LeafNode)
|
88
|
+
attr_reader :root
|
89
|
+
|
90
|
+
private
|
91
|
+
|
92
|
+
# Generate a concatenation node to combine this rope and another rope
|
93
|
+
# or string
|
94
|
+
def concatenate(other)
|
95
|
+
# TODO: Automatically balance the tree if needed
|
96
|
+
case other
|
97
|
+
when primitive_type
|
98
|
+
InteriorNode.new(root, LeafNode.new(other))
|
99
|
+
when BasicRope
|
100
|
+
InteriorNode.new(root, other.root)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def primitive_type
|
105
|
+
raise NotImplementedError, "This method must return the data type stored in the rope"
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'rope/basic_node'
|
2
|
+
|
3
|
+
module Rope
|
4
|
+
# Specifies an interior node. Its underlying data is retrieved by combining
|
5
|
+
# its children recursively.
|
6
|
+
class InteriorNode < BasicNode
|
7
|
+
# Left and right nodes
|
8
|
+
attr_reader :left, :right
|
9
|
+
|
10
|
+
# Initializes a new concatenation node.
|
11
|
+
def initialize(left, right)
|
12
|
+
@left = left
|
13
|
+
@right = right
|
14
|
+
|
15
|
+
@length = left.length + right.length
|
16
|
+
@depth = [left.depth, right.depth].max + 1
|
17
|
+
end
|
18
|
+
|
19
|
+
# Gets the underlying data in the tree
|
20
|
+
def data
|
21
|
+
left.data + right.data
|
22
|
+
end
|
23
|
+
|
24
|
+
# Gets a slice of the underlying data in the tree
|
25
|
+
def slice(arg0, *args)
|
26
|
+
if args.length == 0
|
27
|
+
if arg0.is_a?(Fixnum)
|
28
|
+
slice(arg0, 1)
|
29
|
+
elsif arg0.is_a?(Range)
|
30
|
+
from, to = arg0.minmax
|
31
|
+
|
32
|
+
# Special case when the range doesn't actually describe a valid range
|
33
|
+
return nil if from.nil? || to.nil?
|
34
|
+
|
35
|
+
# Normalize so both from and to are positive indices
|
36
|
+
if from < 0
|
37
|
+
from += @length
|
38
|
+
end
|
39
|
+
if to < 0
|
40
|
+
to += @length
|
41
|
+
end
|
42
|
+
|
43
|
+
if from <= to
|
44
|
+
subtree(from, (to - from) + 1)
|
45
|
+
else
|
46
|
+
# Range first is greater than range last
|
47
|
+
# Return empty string to match what String does
|
48
|
+
raise "TODO"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# TODO: arg0.is_a?(Range)
|
53
|
+
# TODO: arg0.is_a?(Regexp)
|
54
|
+
# TODO: arg0.is_a?(String)
|
55
|
+
else
|
56
|
+
arg1 = args[0] # may be slightly confusing; refer to method definition
|
57
|
+
if arg0.is_a?(Fixnum) && arg1.is_a?(Fixnum) # Fixnum, Fixnum
|
58
|
+
if arg1 >= 0
|
59
|
+
subtree(arg0, arg1)
|
60
|
+
else
|
61
|
+
# Negative length, return nil to match what String does
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# TODO: arg0.is_a?(Regexp) && arg1.is_a?(Fixnum)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
#
|
71
|
+
# Freeze is redefined to freeze the subtree; used when dup'ing a Rope to protect against aliasing
|
72
|
+
#
|
73
|
+
def freeze
|
74
|
+
unless frozen?
|
75
|
+
super
|
76
|
+
@left.freeze unless @left.frozen?
|
77
|
+
@right.freeze unless @right.frozen?
|
78
|
+
end
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
# Rebalances this tree
|
83
|
+
def rebalance!
|
84
|
+
# TODO
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns a node that represents a slice of this tree
|
88
|
+
def subtree(from, length)
|
89
|
+
# Translate to positive index if given a negative one
|
90
|
+
if from < 0
|
91
|
+
from += @length
|
92
|
+
end
|
93
|
+
|
94
|
+
# If more than @length characters are requested, truncate
|
95
|
+
length = [(@length - from), length].min
|
96
|
+
|
97
|
+
# Entire length requested
|
98
|
+
return self if length == @length
|
99
|
+
|
100
|
+
# Check if the requested subtree is entirely in the right subtree
|
101
|
+
rfrom = from - @left.length
|
102
|
+
return @right.subtree(rfrom, length) if rfrom >= 0
|
103
|
+
|
104
|
+
llen = @left.length - from
|
105
|
+
rlen = length - llen
|
106
|
+
if rlen > 0
|
107
|
+
# Requested subtree overlaps both the left and the right subtree
|
108
|
+
@left.subtree(from, llen) + @right.subtree(0, rlen)
|
109
|
+
else
|
110
|
+
# Requested subtree is entirely in the left subtree
|
111
|
+
@left.subtree(from, length)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
#
|
116
|
+
# Overwrites data at index with substr
|
117
|
+
#
|
118
|
+
# Returns self, however leaf nodes may return a new interior node if the replace! causes a leaf to be split
|
119
|
+
#
|
120
|
+
def replace!(index, length, substr)
|
121
|
+
# We may be frozen, so this all gets reified into a new node or overwrites on ourselves
|
122
|
+
new_left = @left
|
123
|
+
new_right = @right
|
124
|
+
|
125
|
+
# Translate to positive index if given a negative one
|
126
|
+
if index < 0
|
127
|
+
index += @length
|
128
|
+
end
|
129
|
+
|
130
|
+
rindex = index - @left.length
|
131
|
+
if(index == 0 && length == @left.length)
|
132
|
+
#substr exactly replaces left sub-tree
|
133
|
+
new_left = LeafNode.new(substr)
|
134
|
+
elsif(index == @left.length && length == @right.length)
|
135
|
+
#substr exactly replaces right sub-tree
|
136
|
+
new_right = LeafNode.new(substr)
|
137
|
+
elsif rindex < 0
|
138
|
+
if(index + length <= @left.length)
|
139
|
+
#Replacement segment is a subsection of the left tree
|
140
|
+
|
141
|
+
#Requested index is in the left subtree, and a split may occur
|
142
|
+
new_left = @left.replace!(index, length, substr)
|
143
|
+
else
|
144
|
+
#Replacement segement is a subsection of left tree along with a subsection of the right tree
|
145
|
+
left_count = @left.length - index
|
146
|
+
|
147
|
+
new_left = InteriorNode.new(
|
148
|
+
@left.subtree(0, index),
|
149
|
+
LeafNode.new(substr)
|
150
|
+
)
|
151
|
+
new_right = @right.subtree(rindex + length, @right.length - (rindex + length))
|
152
|
+
end
|
153
|
+
else
|
154
|
+
# Requested index is in the right subtree, and a split may occur
|
155
|
+
new_right = @right.replace!(rindex, length, substr)
|
156
|
+
end
|
157
|
+
|
158
|
+
if(frozen?)
|
159
|
+
InteriorNode.new(new_left, new_right)
|
160
|
+
else
|
161
|
+
#Length could have changed if the substr replaced a section of a different size or there was an append
|
162
|
+
@left = new_left
|
163
|
+
@right = new_right
|
164
|
+
@length = @left.length + @right.length
|
165
|
+
@depth = [left.depth, right.depth].max + 1
|
166
|
+
self
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'rope/basic_node'
|
2
|
+
|
3
|
+
module Rope
|
4
|
+
# Specifies a leaf node that contains a basic string
|
5
|
+
class LeafNode < BasicNode
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
# The underlying data in the tree
|
9
|
+
attr_reader :data
|
10
|
+
|
11
|
+
def_delegator :@data, :slice
|
12
|
+
|
13
|
+
# Initializes a node that contains a basic string
|
14
|
+
def initialize(data)
|
15
|
+
@data = data.freeze #Freezes the data to protect against aliasing errors
|
16
|
+
@length = data.length
|
17
|
+
@depth = 0
|
18
|
+
end
|
19
|
+
|
20
|
+
def subtree(from, length)
|
21
|
+
if length == @data.length
|
22
|
+
self
|
23
|
+
else
|
24
|
+
self.class.new(@data.slice(from, length))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def replace!(index, length, substr)
|
29
|
+
left = if(index == 0)
|
30
|
+
LeafNode.new(substr)
|
31
|
+
else
|
32
|
+
InteriorNode.new(
|
33
|
+
LeafNode.new(@data.slice(0,index)),
|
34
|
+
LeafNode.new(substr)
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
if((index + length) < @data.length)
|
39
|
+
InteriorNode.new(
|
40
|
+
left,
|
41
|
+
LeafNode.new(@data.slice(index + length, @data.length - (index + length)))
|
42
|
+
)
|
43
|
+
else
|
44
|
+
left
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/lib/rope/node.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Rope
|
2
|
+
class Node
|
3
|
+
# Length of the underlying data in the tree and its descendants
|
4
|
+
attr_reader :length
|
5
|
+
|
6
|
+
# Depth of the tree
|
7
|
+
attr_reader :depth
|
8
|
+
|
9
|
+
# The underlying data in the tree
|
10
|
+
attr_reader :data
|
11
|
+
|
12
|
+
# Concatenates this tree with another tree (non-destructive to either
|
13
|
+
# tree)
|
14
|
+
def +(other)
|
15
|
+
ConcatenationNode.new(self, other)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Gets the string representation of the underlying data in the tree
|
19
|
+
def to_s
|
20
|
+
data.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
# Rebalances this tree
|
24
|
+
def rebalance!
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/rope/version.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
|
2
|
+
require 'rope'
|
3
|
+
|
4
|
+
describe "rope" do
|
5
|
+
describe "#+" do
|
6
|
+
it "should allow concatenation of two Rope instances" do
|
7
|
+
rope1 = "123".to_rope
|
8
|
+
rope2 = "456".to_rope
|
9
|
+
|
10
|
+
rope3 = rope1 + rope2
|
11
|
+
rope3.to_s.should == "123456"
|
12
|
+
|
13
|
+
# rope1 and rope2 should not have been affected
|
14
|
+
rope1.to_s.should == "123"
|
15
|
+
rope2.to_s.should == "456"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should allow concatenation of a Rope and a String" do
|
19
|
+
rope = "123".to_rope
|
20
|
+
string = "456"
|
21
|
+
|
22
|
+
rope_concat = rope + string
|
23
|
+
rope_concat.to_s.should == "123456"
|
24
|
+
|
25
|
+
# rope and string should not have been affected
|
26
|
+
rope.to_s.should == "123"
|
27
|
+
string.should == "456"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should allow concatenation of many Rope instances" do
|
31
|
+
rope_concat = ["123", "456", "789", "012"].inject(Rope::Rope.new) { |combined, str| combined += str.to_rope }
|
32
|
+
rope_concat.to_s.should == "123456789012"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/spec/dup_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rope'
|
2
|
+
|
3
|
+
describe "rope" do
|
4
|
+
describe "#initialize" do
|
5
|
+
it "can be constructed using the Rope constructor" do
|
6
|
+
rope = Rope::Rope.new("testing123")
|
7
|
+
rope.to_s.should == "testing123"
|
8
|
+
end
|
9
|
+
|
10
|
+
it "can be constructed using the to_rope method on a string" do
|
11
|
+
rope = "testing123".to_rope
|
12
|
+
rope.to_s.should == "testing123"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'rope'
|
2
|
+
|
3
|
+
describe Rope::Rope do
|
4
|
+
describe "[<Fixnum>]=" do
|
5
|
+
(1..4).each do |segments|
|
6
|
+
rope_len = segments.times.collect { |i| i + 1 }.reduce(0, :+)
|
7
|
+
|
8
|
+
context "a #{segments} segment rope (length #{rope_len})" do
|
9
|
+
#Makes a rope of successive pieces, each one longer than the previous
|
10
|
+
subject {
|
11
|
+
segments.times.collect { |i| i + 1 }.reduce("".freeze.to_rope) do |rope, i|
|
12
|
+
rope += (rope.length..(rope.length + i)).collect { |x| (x.to_i % 10).to_s.freeze }.join
|
13
|
+
end
|
14
|
+
}
|
15
|
+
|
16
|
+
(0..2).each do |replacement_len|
|
17
|
+
context "a #{replacement_len} length replacement str" do
|
18
|
+
let(:replacement) { ('a'..'z').to_a[0,replacement_len].join }
|
19
|
+
|
20
|
+
(0..rope_len).each do |offset|
|
21
|
+
(0..2).each do |replace_len|
|
22
|
+
it "replaces a substring of length #{replace_len} at offset #{offset} with a string" do
|
23
|
+
as_string = subject.to_s.dup
|
24
|
+
|
25
|
+
as_string[offset, replace_len] = replacement
|
26
|
+
subject[offset, replace_len] = replacement
|
27
|
+
|
28
|
+
expect(subject.to_s).to eq(as_string)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
it "doesn't alias" do
|
38
|
+
r1 = "foo".to_rope + "bar" + "baz"
|
39
|
+
r2 = r1.dup
|
40
|
+
|
41
|
+
r2[0,3] = "baz"
|
42
|
+
|
43
|
+
expect(r2).not_to eq(r1)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
subject { "foo".to_rope }
|
48
|
+
|
49
|
+
describe "[self.length]=" do
|
50
|
+
it "appends to the end of the rope" do
|
51
|
+
subject[subject.length] = "bar"
|
52
|
+
should eq "foobar"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "<<" do
|
57
|
+
it "appends to the end of the rope" do
|
58
|
+
subject << "bar"
|
59
|
+
should eq "foobar"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
data/spec/slice_spec.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
|
2
|
+
require 'rope'
|
3
|
+
|
4
|
+
describe Rope::Rope do
|
5
|
+
describe "#slice" do
|
6
|
+
context "Fixnum, Fixnum" do # slice(Fixnum, Fixnum)
|
7
|
+
it "should return a slice for a Rope instance created with a String" do
|
8
|
+
rope = "12345".to_rope
|
9
|
+
rope_slice = rope.slice(0, 2)
|
10
|
+
|
11
|
+
rope_slice.to_s.should == "12"
|
12
|
+
rope_slice.class.should == Rope::Rope
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should return a slice for a Rope instance created by concatenating other Rope instances" do
|
16
|
+
rope = ["123", "456", "789", "012"].inject(Rope::Rope.new) { |combined, str| combined += str.to_rope }
|
17
|
+
rope_slice = rope.slice(2, 6)
|
18
|
+
|
19
|
+
rope_slice.to_s.should == "345678"
|
20
|
+
rope_slice.class.should == Rope::Rope
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should return a slice when given a negative index" do
|
24
|
+
rope = ["123", "456", "789", "012"].inject(Rope::Rope.new) { |combined, str| combined += str.to_rope }
|
25
|
+
rope_slice = rope.slice(-8, 6)
|
26
|
+
|
27
|
+
rope_slice.to_s.should == "567890"
|
28
|
+
rope_slice.class.should == Rope::Rope
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should return nil when given a negative length" do
|
32
|
+
"1234567".to_rope.slice(2, -1).should be_nil
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should return an empty string when given a 0 length" do
|
36
|
+
"1234567".to_rope.slice(2, 0).to_s.should be_empty
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "Fixnum" do # slice(Fixnum)
|
41
|
+
it "should return the character value of the character at the given position" do
|
42
|
+
rope = ["123", "456", "789", "012"].inject(Rope::Rope.new) { |combined, str| combined += str.to_rope }
|
43
|
+
|
44
|
+
rope.slice(0).should == ?1
|
45
|
+
rope.slice(3).should == ?4
|
46
|
+
rope.slice(7).should == ?8
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should return a single character string" do
|
50
|
+
"12345".slice(0).should == "1"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "Range" do # slice(Range)
|
55
|
+
it "should return a slice for a Rope when given a Range" do
|
56
|
+
rope = ["123", "456", "789", "012"].inject(Rope::Rope.new) { |combined, str| combined += str.to_rope }
|
57
|
+
|
58
|
+
rope.slice(0..2).to_s.should == "123"
|
59
|
+
rope.slice(0...2).to_s.should == "12"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should return a slice for a Rope when given a Range that exercises edge cases" do
|
63
|
+
rope = ["123", "456", "789", "012"].inject(Rope::Rope.new) { |combined, str| combined += str.to_rope }
|
64
|
+
|
65
|
+
rope.slice(5..5).to_s.should == "6"
|
66
|
+
rope.slice(5...5).to_s.should == ""
|
67
|
+
rope.slice(0...0).to_s.should == ""
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should return a slice for a Rope when given a Range that contains negative indices" do
|
71
|
+
rope = ["123", "456", "789", "012"].inject(Rope::Rope.new) { |combined, str| combined += str.to_rope }
|
72
|
+
|
73
|
+
rope.slice(-4..-2).to_s.should == "901"
|
74
|
+
rope.slice(-4...-2).to_s.should == "90"
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should return an empty Rope when given a Range where the first index is greater than the last index" do
|
78
|
+
"1234567".slice(4..2).to_s.should be_empty
|
79
|
+
"1234567".slice(-2..-4).to_s.should be_empty
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
metadata
ADDED
@@ -0,0 +1,102 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: basic-rope
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Andy Lindeman
|
8
|
+
- Logan Bowers
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-11-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: rspec
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '0'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rdoc
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ">="
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
description: 'A Rope is a convenient data structure for manipulating large amounts
|
43
|
+
of text. This implementation is inspired by http://www.rubyquiz.com/quiz137.html
|
44
|
+
and https://rubygems.org/gems/cord. Basic-rope is a fork of Andy Lindeman''s original
|
45
|
+
rope gem to generalize the rope data structure to work with any data type that has
|
46
|
+
a length and supports slice(). '
|
47
|
+
email:
|
48
|
+
- alindeman@gmail.com
|
49
|
+
- logan@datacurrent.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- ".gitignore"
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.textile
|
58
|
+
- Rakefile
|
59
|
+
- basic-rope.gemspec
|
60
|
+
- benchmark.rb
|
61
|
+
- lib/rope.rb
|
62
|
+
- lib/rope/basic_node.rb
|
63
|
+
- lib/rope/basic_rope.rb
|
64
|
+
- lib/rope/interior_node.rb
|
65
|
+
- lib/rope/leaf_node.rb
|
66
|
+
- lib/rope/node.rb
|
67
|
+
- lib/rope/string_methods.rb
|
68
|
+
- lib/rope/version.rb
|
69
|
+
- spec/concatenation_spec.rb
|
70
|
+
- spec/dup_spec.rb
|
71
|
+
- spec/initialization_spec.rb
|
72
|
+
- spec/replace_spec.rb
|
73
|
+
- spec/slice_spec.rb
|
74
|
+
homepage: http://rubygems.org/gems/basic-rope
|
75
|
+
licenses: []
|
76
|
+
metadata: {}
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
require_paths:
|
80
|
+
- lib
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
requirements: []
|
92
|
+
rubyforge_project:
|
93
|
+
rubygems_version: 2.4.2
|
94
|
+
signing_key:
|
95
|
+
specification_version: 4
|
96
|
+
summary: Pure Ruby implementation of a Rope data structure
|
97
|
+
test_files:
|
98
|
+
- spec/concatenation_spec.rb
|
99
|
+
- spec/dup_spec.rb
|
100
|
+
- spec/initialization_spec.rb
|
101
|
+
- spec/replace_spec.rb
|
102
|
+
- spec/slice_spec.rb
|