basic-rope 0.0.2
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.
- 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
|