order_tree 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.DS_Store +0 -0
- data/.gitignore +5 -0
- data/.yardoc/checksums +5 -0
- data/.yardoc/objects/root.dat +0 -0
- data/.yardoc/proxy_types +0 -0
- data/Gemfile +4 -0
- data/README.md +26 -0
- data/Rakefile +39 -0
- data/lib/order_tree.rb +6 -0
- data/lib/order_tree/order_tree.rb +319 -0
- data/lib/order_tree/order_tree_node.rb +66 -0
- data/lib/order_tree/unique_proxy.rb +96 -0
- data/lib/order_tree/version.rb +3 -0
- data/order_tree.gemspec +26 -0
- data/spec/order_tree_spec.rb +338 -0
- data/spec/spec_helper.rb +35 -0
- metadata +110 -0
data/.DS_Store
ADDED
Binary file
|
data/.yardoc/checksums
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
lib/order_tree.rb 1587f76284fef215167a4b0970a1411503f6543f
|
2
|
+
lib/order_tree/version.rb 12d621b5c0101d185a48bac5d5ad897d7b83053b
|
3
|
+
lib/order_tree/unique_proxy.rb 632e468875c365d94b069f5c7e4d3d10030ccb55
|
4
|
+
lib/order_tree/order_tree.rb 3dc2a2a576334be3fe9cd19fe999b310fbfd7ea6
|
5
|
+
lib/order_tree/order_store.rb cfef2da2707e61ed0637a48e9a0c1a55d9578b1c
|
Binary file
|
data/.yardoc/proxy_types
ADDED
Binary file
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#Ordered Tree
|
2
|
+
|
3
|
+
This is a nested hash / unbalance tree structure which iterates over all of
|
4
|
+
its available values in insertion order.
|
5
|
+
|
6
|
+
##Like a Normal Hash?
|
7
|
+
|
8
|
+
Exactly like a normal hash. Althought with a standard Ruby Hash it's impossible to
|
9
|
+
know whether or not `hash["a"]["b"]` was inserted before or after `hash["b"]["c"]`
|
10
|
+
because each individual Hash maintains it's own insertion order.
|
11
|
+
|
12
|
+
Now you can know. If you need to. The main thing you gain with this over an
|
13
|
+
Array is the ability to prune or transplant multiple values at the same
|
14
|
+
time by cutting on one of the branches.
|
15
|
+
|
16
|
+
##Caveat
|
17
|
+
|
18
|
+
Each value is actually stored in a proxy object which maintains a unique id.
|
19
|
+
This is necessary so that if you insert three `4`s into the tree you can tell
|
20
|
+
which one came first. This wouldn't actually be necessary in a C implementation,
|
21
|
+
or in one based on WeakRefs, but it works okay here.
|
22
|
+
|
23
|
+
You can generally treat the OrderTree exactly like a nested hash, but be aware
|
24
|
+
that the first and last methods (as well as the #each iterator) actually return
|
25
|
+
an OrderTreeNode and not the actual object stored at that location. (You can
|
26
|
+
get at it by using #orig on the returned value.
|
data/Rakefile
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rspec/core'
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
8
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
9
|
+
spec.rspec_opts = "-d"
|
10
|
+
end
|
11
|
+
|
12
|
+
RSpec::Core::RakeTask.new(:spec_with_report) do |spec|
|
13
|
+
spec.fail_on_error = false
|
14
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
15
|
+
spec.rspec_opts = "--format html --out report/test_report.html"
|
16
|
+
end
|
17
|
+
|
18
|
+
task :report do
|
19
|
+
Dir.mkdir "report" unless File.exists? "report"
|
20
|
+
Dir.mkdir "report/profile" unless File.exists? "report/profile"
|
21
|
+
File.open "report/index.html","w" do |f|
|
22
|
+
f.write <<-HTML
|
23
|
+
<html>
|
24
|
+
<body>
|
25
|
+
<h1> Status Report </h1>
|
26
|
+
<a href="coverage/index.html"> Coverage </a>
|
27
|
+
<a href="profile/profile.html"> Speed Profile </a>
|
28
|
+
<a href="test_report.html"> Test Report </a>
|
29
|
+
</body>
|
30
|
+
</html>
|
31
|
+
HTML
|
32
|
+
end
|
33
|
+
ENV["REPORT"] = "1"
|
34
|
+
Rake::Task[:spec_with_report].invoke
|
35
|
+
ENV["REPORT"] = ""
|
36
|
+
end
|
37
|
+
|
38
|
+
task :default => :spec
|
39
|
+
|
data/lib/order_tree.rb
ADDED
@@ -0,0 +1,319 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module OrderTree
|
4
|
+
|
5
|
+
# A unbalance tree / nested hash type structure that implements #each and returns
|
6
|
+
# the values in the order in which they were inserted, regardless of depth
|
7
|
+
# It can mostly be treated as a nested hash - but #each will return a #path
|
8
|
+
# to the values it iterates
|
9
|
+
class OrderTree
|
10
|
+
include Enumerable
|
11
|
+
include ProxyOperator
|
12
|
+
|
13
|
+
class PathNotFound < StandardError; end
|
14
|
+
|
15
|
+
attr_accessor :first, :root
|
16
|
+
attr_reader :last
|
17
|
+
|
18
|
+
# Create a new OrderTree
|
19
|
+
# @param [Hash] constructor - a hash literal for the initial values
|
20
|
+
# @param [OrderTree] OrderTree - the root tree object.
|
21
|
+
# @note The order of insertion might not be what you would expect for multi-
|
22
|
+
# level hash literals. The most deeply nested values will be inserted FIRST.
|
23
|
+
def initialize(constructor = {}, root = nil)
|
24
|
+
@_delegate_hash = {}
|
25
|
+
self.root = root || self
|
26
|
+
constructor.each_with_object(self) do |(k,v),memo|
|
27
|
+
memo[k] = v
|
28
|
+
end
|
29
|
+
self.default = OrderTreeNode.new(nil,self) if self.root
|
30
|
+
_delegate_hash.default = self.default
|
31
|
+
end
|
32
|
+
|
33
|
+
def last= obj
|
34
|
+
if @last.nil?
|
35
|
+
@first = obj
|
36
|
+
@last = obj
|
37
|
+
else
|
38
|
+
@last = obj
|
39
|
+
end
|
40
|
+
end
|
41
|
+
protected :last=
|
42
|
+
|
43
|
+
# Set the default value for the tree
|
44
|
+
# This place the default object behind a UniqueProxy. The default
|
45
|
+
# is not remembered within the order
|
46
|
+
# @param [Object] obj
|
47
|
+
def default= obj
|
48
|
+
unless proxy? obj
|
49
|
+
obj = OrderTreeNode.new(obj,self)
|
50
|
+
end
|
51
|
+
@default = obj
|
52
|
+
_delegate_hash.default = @default
|
53
|
+
_delegate_hash.each_value do |v|
|
54
|
+
if v.is_a? OrderTree
|
55
|
+
v.default= obj
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_reader :default
|
61
|
+
|
62
|
+
# Yields each path, value pair of the OrderTree in the order in which it was
|
63
|
+
# inserted
|
64
|
+
# @return [Enumerator]
|
65
|
+
# @yield [path, value] yields the path (as an array) to a value
|
66
|
+
# @yieldparam [Array] path the path as an array
|
67
|
+
# @yieldparam [Object] value the original object stored in the OrderTree
|
68
|
+
def each_pair
|
69
|
+
return enum_for(:each_pair) unless block_given?
|
70
|
+
self.each do |c|
|
71
|
+
yield c.path, c.orig
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Array] the results of calling {#each}.to_a
|
76
|
+
def order
|
77
|
+
each_pair.to_a
|
78
|
+
end
|
79
|
+
|
80
|
+
# @return [Enumerator] collection of OrderTreeNode objects in insertion order
|
81
|
+
def each
|
82
|
+
return enum_for(:each) unless block_given?
|
83
|
+
c = root.first
|
84
|
+
while c
|
85
|
+
yield c
|
86
|
+
c = c.next
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# @return [Enumerator] collection of paths in insertion order
|
91
|
+
def each_path
|
92
|
+
return enum_for(:each_path) unless block_given?
|
93
|
+
self.each do |v|
|
94
|
+
yield v.path
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# @return [Enumerator] collection of leaf values in insertion order
|
99
|
+
def each_leaf
|
100
|
+
return enum_for(:each_leaf) unless block_given?
|
101
|
+
self.each do |v|
|
102
|
+
unless v.is_a? OrderTree
|
103
|
+
yield v.orig
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# @return [Enumerator] collection of values in insertion order, including subnodes
|
109
|
+
def each_value
|
110
|
+
return enum_for(:each_value) unless block_given?
|
111
|
+
self.each do |v|
|
112
|
+
yield v.orig
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# Finds the first occurence of an object in the tree
|
117
|
+
# @param [Object] val the value to search for this is a O(n) operation
|
118
|
+
# @param [Block] block if both a block and val are passed, the
|
119
|
+
# block will be evalueated first, then the value
|
120
|
+
# @return [Array, false, true] path to value, false if not found, or true if value == self
|
121
|
+
# @yield [value] use the block to perform more complicated tests
|
122
|
+
# @yieldreturn [Boolean]
|
123
|
+
# @raises [ArgumentError] if neither a val nor block is given.
|
124
|
+
# @note You cannot search for a nil value by passing nil as the value. You must
|
125
|
+
# pass a block that compares for nil
|
126
|
+
# @note This methods does NOT guarantee that you will recieve the result
|
127
|
+
# in inserted order
|
128
|
+
def path val = nil, &block
|
129
|
+
begin
|
130
|
+
path! val, &block
|
131
|
+
rescue PathNotFound
|
132
|
+
nil
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Finds the first occurence of a specific insertion in the tree
|
137
|
+
# @param [OrderTreeNode] val the proxy object to find
|
138
|
+
def strict_path val = nil
|
139
|
+
begin
|
140
|
+
strict_path! val
|
141
|
+
rescue PathNotFound
|
142
|
+
nil
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# Raises an exception if it can't determine the path
|
147
|
+
# @see {#path}
|
148
|
+
def path! val = nil, &block
|
149
|
+
raise ArgumentError, "requires search value or block" if val.nil? and block.nil?
|
150
|
+
__path(val, false, [], &block)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Raises an exception if it can't find the object
|
154
|
+
# @see {#strict_path}
|
155
|
+
def strict_path! val
|
156
|
+
unless ProxyOperator.proxy? val
|
157
|
+
raise ArgumentError, "OrderTreeNode expected for strict_path"
|
158
|
+
end
|
159
|
+
__path(val, true)
|
160
|
+
end
|
161
|
+
|
162
|
+
# @private
|
163
|
+
def __path val = nil, strict = false, key_path = [], &block
|
164
|
+
op = strict ? :equal? : :==
|
165
|
+
return true if (yield(val) unless block.nil?) or self.__send__ op, val
|
166
|
+
_delegate_hash.each do |k,v|
|
167
|
+
if (yield v unless block.nil?) or v.__send__ op, val
|
168
|
+
key_path << k
|
169
|
+
break
|
170
|
+
elsif v.respond_to? :__path, true
|
171
|
+
begin
|
172
|
+
v.__path(val, strict, key_path, &block)
|
173
|
+
key_path.unshift(k)
|
174
|
+
break
|
175
|
+
rescue PathNotFound
|
176
|
+
# try the next one
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
180
|
+
raise PathNotFound, "Couldn't find path to #{val} in #{self.to_s}" if key_path.empty?
|
181
|
+
key_path
|
182
|
+
end
|
183
|
+
private :__path
|
184
|
+
|
185
|
+
# @private
|
186
|
+
# @api Delegate
|
187
|
+
def _delegate_hash
|
188
|
+
@_delegate_hash
|
189
|
+
end
|
190
|
+
private :_delegate_hash
|
191
|
+
|
192
|
+
def to_s
|
193
|
+
"#<#{self.class}:#{'0x%x' % self.__id__ << 1}>"
|
194
|
+
end
|
195
|
+
|
196
|
+
def inspect
|
197
|
+
_delegate_hash.inspect
|
198
|
+
end
|
199
|
+
|
200
|
+
# @param [OrderTree] other
|
201
|
+
# @return [true] if self and other do not share all leaves or were inserted in a different order
|
202
|
+
def != other
|
203
|
+
!(self == other)
|
204
|
+
end
|
205
|
+
|
206
|
+
# @param [OrderTree] other
|
207
|
+
# @return [true] if self and other share all of their leaves and were inserted in the same order
|
208
|
+
def == other
|
209
|
+
return false if other.class != self.class
|
210
|
+
ov,sv = other.each_value.to_a, self.each_value.to_a
|
211
|
+
t_arr = ov.size > sv.size ? (ov.zip(sv)) : (sv.zip(ov))
|
212
|
+
t_arr.each do |sv, ov|
|
213
|
+
return false if sv.nil? ^ ov.nil?
|
214
|
+
if [ov,sv].map { |v| v.respond_to? :_delegate_hash, true}.all?
|
215
|
+
return false unless ov.send(:_delegate_hash) == sv.send(:_delegate_hash)
|
216
|
+
elsif ov != sv
|
217
|
+
return false
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
# @param [OrderTree] other
|
223
|
+
# @return [true] if self and other contains the same key/path pairs as leaf nodes, regardless of their
|
224
|
+
# order of insertion
|
225
|
+
def contents_equal? other
|
226
|
+
return false if other.class != self.class
|
227
|
+
ov,sv = other.each_leaf.to_a.sort, self.each_leaf.to_a.sort
|
228
|
+
t_arr = ov.size > sv.size ? (ov.zip(sv)) : (sv.zip(ov))
|
229
|
+
t_arr.each do |sv,ov|
|
230
|
+
return false unless sv == ov
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
# Returns the UniqueProxy at path.
|
235
|
+
# @param [Array] path the path to return
|
236
|
+
# @return [OrdeTreeNode] either OrderTreeNode default or the OrderTreeNode at path
|
237
|
+
def at *paths
|
238
|
+
t = self
|
239
|
+
paths.each do |p|
|
240
|
+
if t.respond_to? :at # if it's a branch nodejkeep looking
|
241
|
+
t = (t.__send__ :_delegate_hash)[p]
|
242
|
+
else
|
243
|
+
#other wise resturn the default
|
244
|
+
return self.root.default
|
245
|
+
end
|
246
|
+
end
|
247
|
+
t
|
248
|
+
end
|
249
|
+
|
250
|
+
# Return the object stored at path
|
251
|
+
# @param [Array] path you may specify the path as either an array, or
|
252
|
+
# by using the nested hash syntax
|
253
|
+
# @example
|
254
|
+
# obj = OrderTree.new( :first => { :second => { :third => 3 }})
|
255
|
+
# obj[:first, :second, :third] #=> 3
|
256
|
+
# obj[:first][:second][:third] #=> 3
|
257
|
+
def [] *paths
|
258
|
+
t = self.at *paths
|
259
|
+
t.orig
|
260
|
+
end
|
261
|
+
|
262
|
+
|
263
|
+
# @private
|
264
|
+
def _find_delegate_hash *paths
|
265
|
+
under = self
|
266
|
+
paths.each do |k|
|
267
|
+
under = under.instance_eval do
|
268
|
+
unless self.respond_to? :_delegate_hash, true
|
269
|
+
raise NoMethodError, "Can't reifiy tree leaf on access to #{paths}"
|
270
|
+
end
|
271
|
+
h = self.send :_delegate_hash
|
272
|
+
if h.has_key? k and k != paths.last
|
273
|
+
h[k]
|
274
|
+
else
|
275
|
+
break h
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
under
|
280
|
+
end
|
281
|
+
private :_find_delegate_hash
|
282
|
+
|
283
|
+
# Prune branch from the tree
|
284
|
+
# @param [Array] path
|
285
|
+
def delete *paths
|
286
|
+
under = _find_delegate_hash *paths
|
287
|
+
if under.has_key? paths.last
|
288
|
+
under[paths.last].remove
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Stores the value at path
|
293
|
+
# @param [Array] path
|
294
|
+
# @param [Object] value
|
295
|
+
def []= *paths, value
|
296
|
+
under = _find_delegate_hash *paths
|
297
|
+
|
298
|
+
if value.kind_of? Hash or value.kind_of? OrderTree
|
299
|
+
value = OrderTree.new(value, @root)
|
300
|
+
value.default= self.root.default
|
301
|
+
end
|
302
|
+
|
303
|
+
if under.has_key? paths.last # i am overwriting a path
|
304
|
+
under[paths.last].remove
|
305
|
+
end
|
306
|
+
|
307
|
+
under[paths.last] = OrderTreeNode.new(value, self)
|
308
|
+
under[paths.last].prev = root.last if root.last
|
309
|
+
root.last.next = under[paths.last] if root.last
|
310
|
+
root.last = under[paths.last]
|
311
|
+
|
312
|
+
#@order.push under[paths.last]
|
313
|
+
|
314
|
+
#puts "insertion of '#{value}' in #{self.to_s} -> #{@order.to_s} (id #{under[paths.last].unique_id})"
|
315
|
+
value
|
316
|
+
end
|
317
|
+
alias :store :[]=
|
318
|
+
end
|
319
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module OrderTree
|
2
|
+
class OrderTreeNode < UniqueProxy
|
3
|
+
attr_accessor :next, :prev
|
4
|
+
attr_accessor :tree
|
5
|
+
attr_reader :path
|
6
|
+
|
7
|
+
def initialize obj, tree
|
8
|
+
super(obj)
|
9
|
+
@tree = tree
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove
|
13
|
+
prev_node = self.prev
|
14
|
+
next_node = self.next
|
15
|
+
self.next.prev = prev_node if self.next
|
16
|
+
self.prev.next = next_node if self.prev
|
17
|
+
|
18
|
+
if self.tree.root.first.equal? self
|
19
|
+
self.tree.root.first = self.next
|
20
|
+
end
|
21
|
+
|
22
|
+
# try this so that the node can remove
|
23
|
+
# itself fromt he tree
|
24
|
+
my_path = self.path
|
25
|
+
self.tree.instance_eval do
|
26
|
+
_delegate_hash.delete my_path.last
|
27
|
+
end
|
28
|
+
@path = nil
|
29
|
+
@tree = nil
|
30
|
+
@next = nil
|
31
|
+
@prev = nil
|
32
|
+
self
|
33
|
+
end
|
34
|
+
|
35
|
+
def before other
|
36
|
+
(self <=> other) == -1 ? true : false
|
37
|
+
end
|
38
|
+
|
39
|
+
def after other
|
40
|
+
(self <=> other) == 1 ? true : false
|
41
|
+
end
|
42
|
+
|
43
|
+
def <=> other
|
44
|
+
if self.equal? other
|
45
|
+
return 0
|
46
|
+
else
|
47
|
+
p, n = self.prev, self.next
|
48
|
+
while p or n
|
49
|
+
return 1 if p.equal? other
|
50
|
+
return -1 if n.equal? other
|
51
|
+
p = p.prev if p
|
52
|
+
n = n.next if n
|
53
|
+
end
|
54
|
+
end
|
55
|
+
raise ::ArgumentError, "Cannot compare #{self} and #{other} because they are not in the same tree"
|
56
|
+
end
|
57
|
+
|
58
|
+
def path
|
59
|
+
@path || self.path!
|
60
|
+
end
|
61
|
+
|
62
|
+
def path!
|
63
|
+
@path = self.tree.root.strict_path! self
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'securerandom'
|
2
|
+
|
3
|
+
module OrderTree
|
4
|
+
|
5
|
+
# Mixin module that provides a proxy convience functions
|
6
|
+
module ProxyOperator
|
7
|
+
#@param [Object] obj object to test
|
8
|
+
#@return [Boolean] is object in fact a proxy?
|
9
|
+
def proxy? obj
|
10
|
+
!!(obj.instance_eval { @is_proxy })
|
11
|
+
rescue false
|
12
|
+
end
|
13
|
+
module_function :proxy?
|
14
|
+
|
15
|
+
#@param [Object] obj object to proxy
|
16
|
+
#@return [UniqueProxy] create a unique proxy over obj
|
17
|
+
def proxy obj
|
18
|
+
UniqueProxy.new obj
|
19
|
+
rescue false
|
20
|
+
end
|
21
|
+
module_function :proxy
|
22
|
+
end
|
23
|
+
|
24
|
+
# Simple Proxy for distinguishing between the insertions of two identical
|
25
|
+
# objects in an order tree. Assign a unique ID to any object passed through
|
26
|
+
# the proxy, so you can always find the same object, even if you move it
|
27
|
+
# around in the tree. It also enables you to tell the differen between two
|
28
|
+
# different insertions of the same singleton object.
|
29
|
+
class UniqueProxy < BasicObject
|
30
|
+
class << self
|
31
|
+
attr_accessor :verbose_inspect
|
32
|
+
end
|
33
|
+
|
34
|
+
# @param [Object] obj - the proxy target
|
35
|
+
def initialize obj
|
36
|
+
@is_proxy = true
|
37
|
+
@obj = obj
|
38
|
+
@uuid ||= ::SecureRandom.uuid
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [String] the unique ID of the proxy
|
42
|
+
def unique_id
|
43
|
+
@uuid
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [String] a string describing the proxy if UniqueProxy.verbose_inspect is not false
|
47
|
+
# otherwise calls #inspect on the proxied object
|
48
|
+
def inspect
|
49
|
+
if UniqueProxy.verbose_inspect
|
50
|
+
"#<#{UniqueProxy}::#{@uuid} => #{@obj.inspect}>"
|
51
|
+
else
|
52
|
+
@obj.inspect
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String] a eval-able string to create a new proxy over this proxied object
|
57
|
+
def to_s
|
58
|
+
if UniqueProxy.verbose_inspect
|
59
|
+
"proxy(#{@obj.to_s})"
|
60
|
+
else
|
61
|
+
@obj.to_s
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Is true only if the other object has the same unique_id as self
|
66
|
+
def equal? other
|
67
|
+
(@uuid == other.unique_id) rescue false
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Object] the unproxied target
|
71
|
+
def orig
|
72
|
+
@obj
|
73
|
+
end
|
74
|
+
|
75
|
+
# Dispatches methods calls to proxy target
|
76
|
+
def method_missing(method, *args, &block)
|
77
|
+
@obj.__send__ method, *args, &block
|
78
|
+
end
|
79
|
+
|
80
|
+
# @private
|
81
|
+
def !
|
82
|
+
!@obj
|
83
|
+
end
|
84
|
+
|
85
|
+
# @private
|
86
|
+
def == arg
|
87
|
+
@obj == arg
|
88
|
+
end
|
89
|
+
|
90
|
+
# @private
|
91
|
+
def != arg
|
92
|
+
@obj != arg
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
data/order_tree.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "order_tree/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "order_tree"
|
7
|
+
s.version = OrderTree::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Stephen Prater"]
|
10
|
+
s.email = ["stephenp@agrussell.com"]
|
11
|
+
s.homepage = "http://github.com/stephenprater/order_tree"
|
12
|
+
s.summary = %q{An unbalanced tree / nested hash which remember insertion order}
|
13
|
+
s.description = %q{Use OrderTree when you need both insertion order access and nested hash path style access}
|
14
|
+
|
15
|
+
s.rubyforge_project = "order_tree"
|
16
|
+
|
17
|
+
s.files = `git ls-files`.split("\n")
|
18
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
19
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
20
|
+
s.require_paths = ["lib"]
|
21
|
+
|
22
|
+
s.add_development_dependency('rspec')
|
23
|
+
s.add_development_dependency('simplecov')
|
24
|
+
s.add_development_dependency('ruby-prof')
|
25
|
+
s.add_development_dependency('ruby-debug19')
|
26
|
+
end
|
@@ -0,0 +1,338 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
require 'pp'
|
4
|
+
|
5
|
+
require 'order_tree'
|
6
|
+
|
7
|
+
describe OrderTree::UniqueProxy do
|
8
|
+
before :all do
|
9
|
+
Object.send :include, OrderTree::ProxyOperator
|
10
|
+
end
|
11
|
+
|
12
|
+
it "can tell apart things that are the same" do
|
13
|
+
(4 == 4).should eq true
|
14
|
+
(4.equal? 4).should eq true #because fixnums are really the same object
|
15
|
+
a = OrderTree::UniqueProxy.new(4)
|
16
|
+
b = OrderTree::UniqueProxy.new(4)
|
17
|
+
c = OrderTree::UniqueProxy.new(5)
|
18
|
+
(a == b).should eq true
|
19
|
+
(!c).should eq false
|
20
|
+
(c != a).should eq true
|
21
|
+
(a.equal? b).should eq false
|
22
|
+
(a.orig.equal? b.orig).should eq true
|
23
|
+
end
|
24
|
+
|
25
|
+
it "can retrieve the unique id" do
|
26
|
+
a = OrderTree::UniqueProxy.new(4)
|
27
|
+
b = OrderTree::UniqueProxy.new(4)
|
28
|
+
a.unique_id.should_not eq b.unique_id
|
29
|
+
end
|
30
|
+
|
31
|
+
it "can identify a proxy" do
|
32
|
+
a = OrderTree::UniqueProxy.new(5)
|
33
|
+
proxy(5).should eq proxy(5)
|
34
|
+
(proxy(5).equal? proxy(5)).should be_false
|
35
|
+
(proxy? a).should be_true
|
36
|
+
(proxy? 5).should be_false
|
37
|
+
end
|
38
|
+
|
39
|
+
it "can help you inspect and to_s proxies" do
|
40
|
+
proxy(5).to_s.should eq "5"
|
41
|
+
proxy(5).inspect.should eq "5"
|
42
|
+
OrderTree::UniqueProxy.verbose_inspect = true
|
43
|
+
p = proxy(5)
|
44
|
+
p.inspect.to_s.should match(/#<UniqueProxy:(.*?)\s=>\s5>/)
|
45
|
+
p.to_s.should == "proxy(5)"
|
46
|
+
OrderTree::UniqueProxy.verbose_inspect = false
|
47
|
+
p2 = eval(p.to_s)
|
48
|
+
p2.should eq p
|
49
|
+
(p2.equal? p).should be_false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe OrderTree::OrderTree do
|
54
|
+
before :all do
|
55
|
+
@testhash = {
|
56
|
+
:from => {
|
57
|
+
:a => {
|
58
|
+
:b => 4,
|
59
|
+
:c => 4,
|
60
|
+
}
|
61
|
+
},
|
62
|
+
:to => {
|
63
|
+
:d => 4,
|
64
|
+
:e => 4,
|
65
|
+
:to_to => {
|
66
|
+
:f => 5,
|
67
|
+
:g => 6,
|
68
|
+
:h => 7,
|
69
|
+
}
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
@testhash_insertion = {
|
74
|
+
:to => {
|
75
|
+
:d => 4,
|
76
|
+
:e => 4,
|
77
|
+
:to_to => {
|
78
|
+
:f => 5,
|
79
|
+
:g => 6,
|
80
|
+
:h => 7,
|
81
|
+
}
|
82
|
+
},
|
83
|
+
:from => {
|
84
|
+
:a => {
|
85
|
+
:b => 4,
|
86
|
+
:c => 4,
|
87
|
+
}
|
88
|
+
}
|
89
|
+
}
|
90
|
+
|
91
|
+
@order = [[:from, :a, :b],
|
92
|
+
[:from, :a, :c],
|
93
|
+
[:from, :a],
|
94
|
+
[:from],
|
95
|
+
[:to, :d],
|
96
|
+
[:to, :e],
|
97
|
+
[:to, :to_to, :f],
|
98
|
+
[:to, :to_to, :g],
|
99
|
+
[:to, :to_to, :h],
|
100
|
+
[:to, :to_to],
|
101
|
+
[:to]]
|
102
|
+
end
|
103
|
+
|
104
|
+
it "initializes with a hash" do
|
105
|
+
ot = OrderTree::OrderTree.new(@testhash)
|
106
|
+
ot2 = OrderTree::OrderTree.new(@testhash_insertion)
|
107
|
+
end
|
108
|
+
|
109
|
+
it "can retrieve based on path or nest" do
|
110
|
+
ot = OrderTree::OrderTree.new(@testhash)
|
111
|
+
ot2 = OrderTree::OrderTree.new(@testhash_insertion)
|
112
|
+
[ot, ot2].map do |t|
|
113
|
+
t[:from][:a][:c].should eq 4
|
114
|
+
t[:from, :a, :c].should eq 4
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "can set based on path or nest" do
|
119
|
+
ot = OrderTree::OrderTree.new(@testhash)
|
120
|
+
ot2 = OrderTree::OrderTree.new(@testhash_insertion)
|
121
|
+
[ot, ot2].map do |t|
|
122
|
+
t[:from][:a][:d] = 4
|
123
|
+
t[:from, :a, :d].should eq 4
|
124
|
+
t[:from, :a, :e] = 6
|
125
|
+
t[:from][:a][:e].should eq 6
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
it "remember the order" do
|
130
|
+
ot = OrderTree::OrderTree.new(@testhash)
|
131
|
+
ot2 = OrderTree::OrderTree.new(@testhash_insertion)
|
132
|
+
ot.each_path.to_a.should eq @order
|
133
|
+
ot2.each_path.to_a.should_not eq @order
|
134
|
+
end
|
135
|
+
|
136
|
+
it "does not reify the hash on access" do
|
137
|
+
ot = OrderTree::OrderTree.new
|
138
|
+
lambda do
|
139
|
+
ot[:a, :b, :c] = 4
|
140
|
+
end.should raise_error NoMethodError
|
141
|
+
end
|
142
|
+
|
143
|
+
it "remembers the order after initialize" do
|
144
|
+
ot = OrderTree::OrderTree.new
|
145
|
+
order_paths = [[:a],
|
146
|
+
[:a, :a1],
|
147
|
+
[:a, :a2],
|
148
|
+
[:b],
|
149
|
+
[:b, :c],
|
150
|
+
[:b, :c, :d]]
|
151
|
+
order_paths.map do |v|
|
152
|
+
if [[:a], [:b], [:b, :c]].include? v
|
153
|
+
ot[*v] = {}
|
154
|
+
else
|
155
|
+
ot[*v] = 4
|
156
|
+
end
|
157
|
+
end
|
158
|
+
ot.each_path.to_a.should eq order_paths
|
159
|
+
end
|
160
|
+
|
161
|
+
it "can retrieve each pair" do
|
162
|
+
ot = OrderTree::OrderTree.new @testhash
|
163
|
+
|
164
|
+
ot.each_pair.with_index do |(p,v),i|
|
165
|
+
p.should eq @order[i]
|
166
|
+
ot[*p].should eq v
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
it "can overwrite nodes" do
|
171
|
+
ot = OrderTree::OrderTree.new @testhash
|
172
|
+
ot[:from, :a, :c] = 'overwritten'
|
173
|
+
|
174
|
+
new_pairs = ot.each_pair.to_a
|
175
|
+
p,v = new_pairs.last
|
176
|
+
p.should eq [:from, :a, :c]
|
177
|
+
v.should eq 'overwritten'
|
178
|
+
|
179
|
+
ot[:from, :a, :c] = 'overwritten again'
|
180
|
+
|
181
|
+
p.should eq [:from, :a, :c]
|
182
|
+
ot[:from, :a, :c].should eq 'overwritten again'
|
183
|
+
|
184
|
+
new_pairs = ot.each_pair.to_a
|
185
|
+
p,v = new_pairs.first
|
186
|
+
p.should eq [:from, :a, :b]
|
187
|
+
v.should eq 4
|
188
|
+
|
189
|
+
p,v = new_pairs[3]
|
190
|
+
p.should eq [:to, :d]
|
191
|
+
v.should eq 4
|
192
|
+
end
|
193
|
+
|
194
|
+
it "does == comparison" do
|
195
|
+
ot = OrderTree::OrderTree.new @testhash
|
196
|
+
ot2 = OrderTree::OrderTree.new @testhash
|
197
|
+
|
198
|
+
ot.first.should eq ot2.first #because underlying objects are compared
|
199
|
+
(ot == ot2).should be_true #each order and == on the object
|
200
|
+
ot.equal?(ot2).should be_false #we're comparing the proxies here
|
201
|
+
|
202
|
+
(ot.first.equal? ot2.first).should be_false
|
203
|
+
end
|
204
|
+
|
205
|
+
it "does != comparison" do
|
206
|
+
ot = OrderTree::OrderTree.new @testhash
|
207
|
+
ot2 = OrderTree::OrderTree.new @testhash_insertion
|
208
|
+
|
209
|
+
(ot != ot2).should be_true
|
210
|
+
end
|
211
|
+
|
212
|
+
it "does leaf/node equality with contents_equal?" do
|
213
|
+
ot = OrderTree::OrderTree.new @testhash
|
214
|
+
ot2 = OrderTree::OrderTree.new @testhash_insertion
|
215
|
+
(ot.contents_equal? ot2).should be_true
|
216
|
+
end
|
217
|
+
|
218
|
+
it "overwriting a key moves it to the end of the order" do
|
219
|
+
ot = OrderTree::OrderTree.new
|
220
|
+
ot[:a] = 4
|
221
|
+
ot[:b] = 4
|
222
|
+
ot.each_path.to_a.should eq [[:a], [:b]]
|
223
|
+
ot[:a] = 5
|
224
|
+
ot.each_path.to_a.should eq [[:b], [:a]]
|
225
|
+
end
|
226
|
+
|
227
|
+
it "overwriting a nested keys moves to the end of the order" do
|
228
|
+
ot = OrderTree::OrderTree.new( {:a => { :b => 4}, :c => 5})
|
229
|
+
ot.each_path.to_a.should eq [[:a, :b], [:a], [:c]]
|
230
|
+
ot[:a,:b] = 5
|
231
|
+
ot.each_path.to_a.should eq [[:a], [:c], [:a, :b]]
|
232
|
+
end
|
233
|
+
|
234
|
+
it "does not double proxy the default" do
|
235
|
+
ot = OrderTree::OrderTree.new @testhash
|
236
|
+
(proxy? ot.default).should be_true
|
237
|
+
(proxy? ot[:foobar]).should be_false
|
238
|
+
(proxy? ot[:to, :to_to, :no_key]).should be_false
|
239
|
+
end
|
240
|
+
|
241
|
+
it "returns a default when the key doesn't exist" do
|
242
|
+
ot = OrderTree::OrderTree.new @testhash
|
243
|
+
ot.default = "foo"
|
244
|
+
ot[:to, :foo].should eq "foo"
|
245
|
+
|
246
|
+
#copies it to nested levels
|
247
|
+
ot.default = "bar"
|
248
|
+
ot[:to, :foo].should eq "bar"
|
249
|
+
|
250
|
+
ot[:to, :to_to, :no_key].should eq "bar"
|
251
|
+
|
252
|
+
# does't matter how deep i look
|
253
|
+
ot[:foo, :bar, :foo, :monkey].should eq "bar"
|
254
|
+
end
|
255
|
+
|
256
|
+
it "can find the path for value" do
|
257
|
+
ot = OrderTree::OrderTree.new @testhash
|
258
|
+
ot.path(7).should eq [:to, :to_to, :h]
|
259
|
+
ot.path(8).should be_nil
|
260
|
+
|
261
|
+
lambda do
|
262
|
+
ot.path!(7).should eq [:to, :to_to, :h]
|
263
|
+
ot.path!(8)
|
264
|
+
end.should raise_error OrderTree::OrderTree::PathNotFound
|
265
|
+
end
|
266
|
+
|
267
|
+
it "can prune the tree" do
|
268
|
+
ot = OrderTree::OrderTree.new @testhash
|
269
|
+
ot.default = "bob"
|
270
|
+
ot.delete :from, :a, :b
|
271
|
+
ot[:from, :a, :b].should eq "bob"
|
272
|
+
|
273
|
+
to_to = ot.at :to, :to_to
|
274
|
+
to_to.remove
|
275
|
+
|
276
|
+
ot[:to, :to_to].should eq "bob"
|
277
|
+
end
|
278
|
+
|
279
|
+
it "can find the path for a node object" do
|
280
|
+
ot = OrderTree::OrderTree.new @testhash
|
281
|
+
lambda do
|
282
|
+
ot.strict_path(7)
|
283
|
+
end.should raise_error ArgumentError
|
284
|
+
|
285
|
+
seven_node = ot.at *ot.path(7)
|
286
|
+
ot.strict_path(seven_node).should eq [:to, :to_to, :h]
|
287
|
+
|
288
|
+
seven_node.remove
|
289
|
+
ot.strict_path(seven_node).should be_nil
|
290
|
+
#this is the internal call that it uses - it's just here for completeness
|
291
|
+
lambda do
|
292
|
+
ot.strict_path!(seven_node)
|
293
|
+
end.should raise_error OrderTree::OrderTree::PathNotFound
|
294
|
+
end
|
295
|
+
|
296
|
+
it "can run enumerable methods which depend on <=>" do
|
297
|
+
ot = OrderTree::OrderTree.new @testhash
|
298
|
+
ot.max.should eq ot.last
|
299
|
+
ot.min.should eq ot.first
|
300
|
+
ot.sort.should eq ot.each_value.to_a
|
301
|
+
|
302
|
+
# roundabout
|
303
|
+
ot.max.should eq ot[*ot.strict_path!(ot.last)]
|
304
|
+
ot.min.should eq ot[*ot.strict_path!(ot.first)]
|
305
|
+
end
|
306
|
+
|
307
|
+
it "can tell you about insertion order, natch" do
|
308
|
+
ot = OrderTree::OrderTree.new @testhash
|
309
|
+
ot2 = OrderTree::OrderTree.new @testhash_insertion
|
310
|
+
|
311
|
+
(ot.at(:from, :a, :c).before(ot.at(:from, :a, :b))).should be_false
|
312
|
+
(ot.at(:from, :a, :b).before(ot.at(:to, :d))).should be_true
|
313
|
+
|
314
|
+
(ot.at(:from, :a, :c).after(ot.at(:from, :a, :b))).should be_true
|
315
|
+
(ot.at(:to, :e).after(ot.at(:from, :a, :b))).should be_true
|
316
|
+
|
317
|
+
#this probably is only possible if you're doing this.
|
318
|
+
(ot.at(:from, :a, :b) <=> ot.at(:from, :a, :b)).should eq 0
|
319
|
+
end
|
320
|
+
|
321
|
+
it "can't compare nodes across trees" do
|
322
|
+
ot = OrderTree::OrderTree.new @testhash
|
323
|
+
ot2 = OrderTree::OrderTree.new @testhash_insertion
|
324
|
+
|
325
|
+
lambda do
|
326
|
+
ot.at(:from, :a, :c).before(ot2.at(:from, :a, :b))
|
327
|
+
end.should raise_error ArgumentError
|
328
|
+
end
|
329
|
+
|
330
|
+
it "can run regular enumerable methods" do
|
331
|
+
# i'm not going to try all of these, just the one i know
|
332
|
+
# i didn't define.
|
333
|
+
ot = OrderTree::OrderTree.new @testhash
|
334
|
+
ot.each_cons(3).with_index do |v,idx|
|
335
|
+
v.collect { |e| e.path }.should eq @order[idx..idx+2]
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
if ENV["REPORT"] == "1" then
|
2
|
+
require 'simplecov'
|
3
|
+
require 'ruby-prof'
|
4
|
+
require 'ruby-debug'
|
5
|
+
|
6
|
+
SimpleCov.start do
|
7
|
+
add_filter "spec.rb"
|
8
|
+
coverage_dir "report/coverage"
|
9
|
+
end
|
10
|
+
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.before :suite do |example|
|
13
|
+
STDOUT << '|'
|
14
|
+
RubyProf.start
|
15
|
+
end
|
16
|
+
|
17
|
+
config.around :each do |example|
|
18
|
+
STDOUT << '.'
|
19
|
+
RubyProf.resume do
|
20
|
+
example.run
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
config.after :suite do
|
25
|
+
result = RubyProf.stop
|
26
|
+
result.eliminate_methods!([/RSpec::Matchers#.*?/])
|
27
|
+
printer = RubyProf::MultiPrinter.new(result)
|
28
|
+
printer.print(:path => 'report/profile', :profile => "profile")
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Requires supporting files with custom matchers and macros, etc,
|
34
|
+
# in ./support/ and its subdirectories.
|
35
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: order_tree
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Stephen Prater
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-07-25 00:00:00.000000000 -05:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
requirement: &3691310 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *3691310
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: simplecov
|
28
|
+
requirement: &3691090 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *3691090
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: ruby-prof
|
39
|
+
requirement: &3690870 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ! '>='
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0'
|
45
|
+
type: :development
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *3690870
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: ruby-debug19
|
50
|
+
requirement: &3690650 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *3690650
|
59
|
+
description: Use OrderTree when you need both insertion order access and nested hash
|
60
|
+
path style access
|
61
|
+
email:
|
62
|
+
- stephenp@agrussell.com
|
63
|
+
executables: []
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files: []
|
66
|
+
files:
|
67
|
+
- .DS_Store
|
68
|
+
- .gitignore
|
69
|
+
- .yardoc/checksums
|
70
|
+
- .yardoc/objects/root.dat
|
71
|
+
- .yardoc/proxy_types
|
72
|
+
- Gemfile
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- lib/order_tree.rb
|
76
|
+
- lib/order_tree/order_tree.rb
|
77
|
+
- lib/order_tree/order_tree_node.rb
|
78
|
+
- lib/order_tree/unique_proxy.rb
|
79
|
+
- lib/order_tree/version.rb
|
80
|
+
- order_tree.gemspec
|
81
|
+
- spec/order_tree_spec.rb
|
82
|
+
- spec/spec_helper.rb
|
83
|
+
has_rdoc: true
|
84
|
+
homepage: http://github.com/stephenprater/order_tree
|
85
|
+
licenses: []
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
require_paths:
|
89
|
+
- lib
|
90
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubyforge_project: order_tree
|
104
|
+
rubygems_version: 1.5.2
|
105
|
+
signing_key:
|
106
|
+
specification_version: 3
|
107
|
+
summary: An unbalanced tree / nested hash which remember insertion order
|
108
|
+
test_files:
|
109
|
+
- spec/order_tree_spec.rb
|
110
|
+
- spec/spec_helper.rb
|