linked 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9f849d6e81849d33fc12f3d7d6812b9e73a59ad7
4
- data.tar.gz: c236972593f1ab6e4e677fd554cf1bb50fbddddc
3
+ metadata.gz: 05e94a861ba2e494ea20dc84572c5dac578421eb
4
+ data.tar.gz: 849a8b134e95bcd6d444782c167f83ff6f982be4
5
5
  SHA512:
6
- metadata.gz: f035c1eefc48f188b208632a6bbc80135be6b7b7110b3cb8faa02a26b417a096754fcd1f7593991ba7265026f0485fa0b3e49022fdc40fa7276e5499ebee3e4a
7
- data.tar.gz: 31b1561e5d7c6dab409533cb1b263bec7a6db13c4a889acb99605997702cd52b577828673617f23bb27b26d84966c41a20181225f10d150a25ba52defc10d591
6
+ metadata.gz: 255a18d48ba2d395e12394aa8a6a72880100a1c2677ad3f5e3005c7412ea74011c4b902947b390b85077675c6a0447c112b6bed36ba4da8bf03dcd5769bf7f73
7
+ data.tar.gz: e2e9b8f9a72c06074c1aa3af9cbecff9c3fbb8adc2bc6674f50f86d3a089abec934ca5ee91815e9156f4647c374ec3bea87fec583e80236a5b010c9939c0cbf8
data/README.md CHANGED
@@ -36,13 +36,8 @@ A basic use case is show below. For more details, for now, see the docs.
36
36
  ```ruby
37
37
  require 'linked'
38
38
 
39
- # Include the List module in a class
40
- class ListLike
41
- include Linked::List
42
- end
43
-
44
39
  # Create a list
45
- list = ListLike.new
40
+ list = Linked::List.new
46
41
 
47
42
  # Append values
48
43
  list << :value
data/lib/linked/item.rb CHANGED
@@ -1,58 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # TODO: Think about moving some of basic functionallity of Item into Item::Base.
4
-
5
3
  module Linked
6
4
  # Item
7
5
  #
8
- # This class implements doubly linked list items, designed to work both on
9
- # their own and as children of list.
10
- #
11
- # +- - - + +------+------+ +- - - +
12
- # | Head | <--| prev | next |--> ... --> | Tail |
13
- # + - - -+ +------+------+ + - - -+
14
- # (optional) First Item N Items (optional)
15
- #
16
- # An object is considered a list if it responds to #head, #tail, #grow and
17
- # #shrink. The latter facilitate counting of the items and will be called
18
- # everytime an item is appended, prepended or deleted. #head and #tail are
19
- # expected to return two objects that, respectivly
20
- # a) responds to #next= and #append, or #prev= and #prepend and
21
- # b) returns true for #nil?.
22
- #
23
- # Notation
24
- # --------
25
- #
26
- # Some methods operate on chains of items, and to describe the effects of an
27
- # operation the following syntax is used.
28
- #
29
- # A ( A <> B ) [ A <> B ]
30
- # (i) (ii) (iii)
31
- #
32
- # Single items are denoted with capital letters (i), while chains are written
33
- # as multiple connected items (ii). The parenthesis are optional. To show that
34
- # one or more nodes are wraped in a list, angle brackets are used (iii).
6
+ # This class implements a listable value object that wraps an arbitrary value
7
+ # an can be stored in a list.
35
8
 
36
9
  class Item
37
- # Access the list (if any) that the item belongs to. Writing to this
38
- # attribute is protected and should be avoided.
39
- #
40
- # Returns the item's list, or nil
41
-
42
- attr_writer :list
43
- protected :list=
10
+ include Listable
44
11
 
45
12
  # The Item can hold an arbitrary object as its value and it will stay with
46
13
  # the item.
47
14
 
48
15
  attr_accessor :value
49
16
 
50
- # Calling either #prev= or #next= directly is not recommended, since can
51
- # corrupt the chain.
52
-
53
- attr_writer :prev, :next
54
- protected :prev=, :next=
55
-
56
17
  # Creates a new item. If a list is given the item will be considered a part
57
18
  # of that list and appended to the end of it.
58
19
  #
@@ -61,15 +22,9 @@ module Linked
61
22
  #
62
23
  # Returns a new Item.
63
24
 
64
- def initialize(value = nil, list: nil)
25
+ def initialize(value = nil)
65
26
  @value = value
66
- @list = list
67
- if list
68
- list.tail.append self
69
- else
70
- @next = nil
71
- @prev = nil
72
- end
27
+ super()
73
28
  end
74
29
 
75
30
  # Calling #dup on an item returns a copy that is no longer connected to the
@@ -78,7 +33,6 @@ module Linked
78
33
  # Returns a new Item.
79
34
 
80
35
  def initialize_dup(source)
81
- @next = @prev = @list = nil
82
36
  @value = begin
83
37
  source.value.dup
84
38
  rescue TypeError
@@ -87,272 +41,27 @@ module Linked
87
41
  super
88
42
  end
89
43
 
90
- # Identity method that simply return the item. This method mirrors List#item
91
- # and allows other methods that work on Item objects to easily and
92
- # interchangebly accept both lists and items as arguments.
93
- #
94
- # Returns the item itself.
95
-
96
- def item
97
- self
98
- end
99
-
100
- # Access the list that the item is part of. If the item is not in a list a
101
- # NoMethodError will be raised. This mirrors the behaviour of List#list and
102
- # allows other methods that work on List objects to easily and
103
- # interchangeably accept both lists and items as arguments.
104
- #
105
- # Returns the list that the item is part of.
106
-
107
- def list
108
- raise NoMethodError unless @list
109
- @list
110
- end
111
-
112
- # Check it the item is part of a list.
113
- #
114
- # Returns true if the item is in a list.
115
-
116
- def in_list?
117
- @list ? true : false
118
- end
119
-
120
- # Check if this is the first item in the list. It is crucial that tail#nil?
121
- # returns true for the first item to be identified correctly.
122
- #
123
- # Returns true if no item come before this one.
124
-
125
- def first?
126
- @prev.nil?
127
- end
128
-
129
- # Check if this is the last item in the list. It is crucial that head#nil?
130
- # returns true for the last item to be identified correctly.
131
- #
132
- # Returns true if no item come after this one.
133
-
134
- def last?
135
- @next.nil?
136
- end
137
-
138
- # Check if the item is in the given list.
139
- #
140
- # list - any object.
141
- #
142
- # Returns true if the item is part of the given list.
143
-
144
- def in?(list)
145
- @list.equal? list
146
- end
147
-
148
- # Access the next item in the list. If this is the last one a StopIteration
149
- # will be raised, so that items may be iterated over safely in a loop.
150
- #
151
- # Example
152
- # loop do
153
- # item = item.next
154
- # end
155
- #
156
- # Returns the item that come after this.
157
-
158
- def next
159
- raise StopIteration if last?
160
- @next
161
- end
162
-
163
- # Unsafe accessor of the next item in the list. It is preferable to use
164
- # #next.
165
- #
166
- # Returns the item that come after this, or nil if this is the last one.
167
-
168
- def next!
169
- @next
170
- end
171
-
172
- # Access the previous item in the list. If this is the first one a
173
- # StopIteration will be raised, so that items may be iterated over safely in
174
- # a loop.
175
- #
176
- # Example
177
- # loop do
178
- # item = item.prev
179
- # end
180
- #
181
- # Returns the item that come before this.
182
-
183
- def prev
184
- raise StopIteration if first?
185
- @prev
186
- end
187
-
188
- alias previous prev
189
-
190
- # Unsafe accessor of the previous item in the list. It is preferable to use
191
- # #prev.
192
- #
193
- # Returns the item that come before this, or nil if this is the first one.
194
-
195
- def prev!
196
- @prev
197
- end
198
-
199
- alias previous! prev!
200
-
201
- # Inserts the given item between this one and the one after it (if any). If
202
- # the given item is part of a chain, all items following it will be moved to
203
- # this one, and added to the list if one is set.
204
- #
205
- # Example for the chain (A <> C)
206
- #
207
- # A.append B # => (A <> B <> C)
208
- #
209
- # Alternativly the argument can be an arbitrary object, in which case a new
210
- # item will be created around it.
211
- #
212
- # If this item is part of a list #grow will be called on it with the
213
- # number of added items as an argument. Should it also be the last item
214
- # #prev= will be called on the list tail.
215
- #
216
- # object - the item to append, or an arbitrary object to be wraped in a new
217
- # item. If in a list it will be asked to create the new item via
218
- # List#create_item.
219
- #
220
- # Returns the last item that was appended.
221
-
222
- def append(object)
223
- if object.respond_to? :item
224
- first_item = object.item
225
- last_item = first_item.send :extract_beginning_with, @list
226
- else
227
- if @list
228
- first_item = last_item = @list.send :create_item, object
229
- first_item.list = @list
230
- @list.send :grow
231
- else
232
- first_item = last_item = self.class.new object
233
- end
234
- end
235
-
236
- first_item.prev = self
237
- @next.prev = last_item if @next
238
- @next, last_item.next = first_item, @next
239
-
240
- last_item
241
- end
242
-
243
- # Inserts the given item between this one and the one before it (if any). If
244
- # the given item is part of a chain, all items preceeding it will be moved
245
- # to this one, and added to the list if one is set.
246
- #
247
- # Example for the chain (A <> C)
248
- #
249
- # C.prepend B # => (A <> B <> C)
44
+ # Item equality is solely determined by tha value. If the other object
45
+ # responds to #value, and its value is equal (#==) to this value, the
46
+ # objects are considered equal.
250
47
  #
251
- # Alternativly the argument can be an arbitrary object, in which case a new
252
- # item will be created around it.
48
+ # other - any object.
253
49
  #
254
- # If this item is part of a list #grow will be called on it with the
255
- # number of added items as an argument. Should it also be the first item
256
- # #next= will be called on the list head.
257
- #
258
- # object - the item to prepend. or an arbitrary object to be wraped in a
259
- # new item. If in a list it will be asked to create the new item
260
- # via List#create_item.
261
- #
262
- # Returns the last item that was prepended.
263
-
264
- def prepend(object)
265
- if object.respond_to? :item
266
- last_item = object.item
267
- first_item = last_item.send :extract_ending_with, @list
268
- else
269
- if @list
270
- first_item = last_item = @list.send :create_item, object
271
- first_item.list = @list
272
- @list.send :grow
273
- else
274
- first_item = last_item = self.class.new object
275
- end
276
- end
50
+ # Returns true if the objects are considered equal.
277
51
 
278
- last_item.next = self
279
- @prev.next = first_item if @prev
280
- @prev, first_item.prev = last_item, @prev
281
-
282
- first_item
283
- end
284
-
285
- # Remove an item from the chain. If this item is part of a list and is
286
- # either first, last or both in that list, #next= and #prev= will be called
287
- # on the list head and tail respectivly.
288
- #
289
- # If this item is part of a list #shrink will be called on it.
290
- #
291
- # Returns self.
292
-
293
- def delete
294
- @next.prev = @prev if @next
295
- @prev.next = @next if @prev
296
- @list.send :shrink if @list
297
-
298
- @next = @prev = @list = nil
299
- self
52
+ def ==(other)
53
+ return false unless other.respond_to? :value
54
+ value == other.value
300
55
  end
301
56
 
302
- # Remove all items before this one in the chain. If the items are part of a
303
- # list they will be removed from it.
304
- #
305
- # Returns the last item in the chain that was just deleted, or nil if this
306
- # is the first item.
307
-
308
- def delete_before
309
- @prev.send :extract_ending_with unless first?
310
- end
57
+ alias eql? ==
311
58
 
312
- # Remove all items after this one in the chain. If the items are part of a
313
- # list they will be removed from it.
59
+ # Uses the hash value of the item value.
314
60
  #
315
- # Returns the last item in the chain that was just deleted, or nil if this
316
- # is the first item.
61
+ # Returns a fixnum that can be used by Hash to identify the item.
317
62
 
318
- def delete_after
319
- @next.send :extract_beginning_with unless last?
320
- end
321
-
322
- # Iterates over each item before this, in reverse order. If a block is not
323
- # given an enumerator is returned.
324
- #
325
- # Note that raising a StopIteraion inside the block will cause the loop to
326
- # silently stop the iteration early.
327
-
328
- def before
329
- return to_enum(__callee__) unless block_given?
330
- return if first?
331
-
332
- item = self.prev
333
-
334
- loop do
335
- yield item
336
- item = item.prev
337
- end
338
- end
339
-
340
- # Iterates over each item after this. If a block is not given an enumerator
341
- # is returned.
342
- #
343
- # Note that raising a StopIteraion inside the block will cause the loop to
344
- # silently stop the iteration early.
345
-
346
- def after
347
- return to_enum(__callee__) unless block_given?
348
- return if last?
349
-
350
- item = self.next
351
-
352
- loop do
353
- yield item
354
- item = item.next
355
- end
63
+ def hash
64
+ value.hash
356
65
  end
357
66
 
358
67
  # Freezes the value, as well as making the list item itself immutable.
@@ -372,100 +81,5 @@ module Linked
372
81
  output = format '%s:0x%0x', self.class.name, object_id
373
82
  value ? output + " value=#{value.inspect}" : output
374
83
  end
375
-
376
- # PRIVATE DANGEROUS METHOD. This method should never be called directly
377
- # since it may leave the extracted item chain in an invalid state.
378
- #
379
- # This method extracts the item, together with the chain following it, from
380
- # the list they are in (if any) and optionally facilitates moving them to a
381
- # new list.
382
- #
383
- # Given the two lists
384
- # [ A <> B <> C ] [ D ]
385
- # (I) (II)
386
- #
387
- # calling B.extract_beginning_with(II) will result in (B <> C) being removed
388
- # from (I), and (II) to be grown by two. (B <> C) will now reference (II)
389
- # but they will not yet be linked to any of the items in it. It is therefore
390
- # necessary to insert them directly after calling this method, or (II) will
391
- # be left in an invalid state.
392
- #
393
- # Returns the last item of the chain.
394
-
395
- private def extract_beginning_with(new_list = nil)
396
- old_list = @list
397
- # Count items and move them to the new list
398
- last_item = self
399
- count = 1 + loop.count do
400
- last_item.list = new_list
401
- last_item = last_item.next
402
- end
403
-
404
- # Make sure the old list is in a valid state
405
- if old_list
406
- if first?
407
- old_list.send :clear
408
- else
409
- old_list.send :shrink, count
410
- # Fix the links within in the list
411
- @prev.next = last_item.next!
412
- last_item.next!.prev = @prev
413
- end
414
- else
415
- # Disconnect the item directly after the chain
416
- @prev.next = nil unless first?
417
- end
418
-
419
- # Disconnect the chain from the list
420
- @prev = last_item.next = nil
421
-
422
- # Preemptivly tell the new list to grow
423
- new_list.send :grow, count if new_list
424
-
425
- last_item
426
- end
427
-
428
- # PRIVATE DANGEROUS METHOD. This method should never be called directly
429
- # since it may leave the extracted item chain in an invalid state.
430
- #
431
- # This method extracts the item, together with the chain preceding it, from
432
- # the list they are in (if any) and optionally facilitates moving them to a
433
- # new list. See #extract_beginning_with for a description of the side
434
- # effects from calling this method.
435
- #
436
- # Returns the first item in the chain.
437
-
438
- private def extract_ending_with(new_list = nil)
439
- old_list = @list
440
- # Count items and move them to the new list
441
- first_item = self
442
- count = 1 + loop.count do
443
- first_item.list = new_list
444
- first_item = first_item.prev
445
- end
446
-
447
- # Make sure the old list is in a valid state
448
- if old_list
449
- if last?
450
- old_list.send :clear
451
- else
452
- old_list.send :shrink, count
453
- # Fix the links within in the list
454
- @next.prev = first_item.prev!
455
- first_item.prev!.next = @next
456
- end
457
- else
458
- # Disconnect the item directly after the chain
459
- @next.prev = nil unless last?
460
- end
461
-
462
- # Disconnect the chain from the list
463
- first_item.prev = @next = nil
464
-
465
- # Preemptivly tell the new list to grow
466
- new_list.send :grow, count if new_list
467
-
468
- first_item
469
- end
470
84
  end
471
85
  end
@@ -1,13 +1,13 @@
1
1
  module Linked
2
- module List
2
+ class List
3
3
  # End Of List
4
4
  #
5
5
  # This class implements a special list item that is placed at both the end
6
- # and the beginning of a chain of regular items to form a list. The naming
6
+ # and the beginning of a chain of listable items to form a list. The naming
7
7
  # (end of list) comes from the fact that this object, by returning true for
8
8
  # calls to #nil?, signifies the end of a list of Items. In both directions
9
9
  # as a matter of fact, which is why the head and tail objects defined by
10
- # Item is combined into one.
10
+ # Item are combined into one.
11
11
  #
12
12
  # In a nutshell, the structure looks something like this:
13
13
  #
@@ -18,13 +18,23 @@ module Linked
18
18
  # +-| prev | next |<- ... ->| prev | next |-+
19
19
  # +------+------+ +------+------+
20
20
 
21
- class EOL < Item
22
- private :value, :value=, :delete, :first?, :last?
21
+ class EOL
22
+ include Listable
23
23
 
24
- def initialize(list:)
24
+ # Calling #delete on the EOL is not supported and would break the
25
+ # connection between the list and its items.
26
+
27
+ undef delete
28
+
29
+ # Creates a new enf-of-list, as part of a list, with no items yet added to
30
+ # it.
31
+ #
32
+ # list - a List object.
33
+
34
+ def initialize(list)
25
35
  super()
26
- @list = list
27
- @prev = @next = self
36
+ self.list = list
37
+ reset
28
38
  end
29
39
 
30
40
  # EOL objects will return true when asked if they are nil. This is
@@ -43,7 +53,7 @@ module Linked
43
53
  # See Item#append for more details.
44
54
 
45
55
  def append(object)
46
- empty? ? super : @prev.append(object)
56
+ empty? ? super : prev!.append(object)
47
57
  end
48
58
 
49
59
  # Inserts one or more items at the beginning of the list.
@@ -51,7 +61,15 @@ module Linked
51
61
  # See Item#append for more details.
52
62
 
53
63
  def prepend(object)
54
- empty? ? super : @next.prepend(object)
64
+ empty? ? super : next!.prepend(object)
65
+ end
66
+
67
+ # Private helper to reset the EOL to its initial state. This method should
68
+ # never be called directly as it leaves the both the list and the items in
69
+ # an inconsistant state.
70
+
71
+ private def reset
72
+ self.prev = self.next = self
55
73
  end
56
74
 
57
75
  # Private helper to check if the item chain is empty.
@@ -59,7 +77,7 @@ module Linked
59
77
  # Return true if the chain is empty, otherwise nil.
60
78
 
61
79
  private def empty?
62
- @prev == self
80
+ prev!.equal? self
63
81
  end
64
82
  end
65
83
  end
data/lib/linked/list.rb CHANGED
@@ -3,49 +3,20 @@
3
3
  module Linked
4
4
  # List
5
5
  #
6
- # This module can be included in any class to give it list like behaviour.
7
- # Most importantly, the methods #head, #tail, #grow and #shrink are
8
- # implemented to comply with the requirements defined by Item.
9
- #
10
- # Example
11
- #
12
- # class ListLike
13
- # include Linked::List
14
- #
15
- # def initialize
16
- # super
17
- # ...
18
- #
19
- # A key implementation detail is the End-Of-List, or EOL object that sits
20
- # between the list and the actual list items. It provides separation between
21
- # the list and the actual list items.
6
+ # This class implements a linked list. Most importantly, the methods #head,
7
+ # #tail, #grow, #shrink and #create_item are implemented to comply with the
8
+ # requirements defined by Listable.
22
9
 
23
- module List
10
+ class List
24
11
  include Enumerable
25
12
 
26
- # Private accessor method for the End-Of-List object.
27
- #
28
- # Returns a List::EOL object.
29
-
30
- attr_reader :eol
31
- private :eol
32
-
33
- # Returns an object that responds to #next= and #prepend.
34
-
35
- alias head eol
36
-
37
- # Returns an object that responds to #prev= and #append.
38
-
39
- alias tail eol
13
+ # Initializes the list. It is important that this method be called during
14
+ # the initialization of the including class, and that the instance variables
15
+ # @_item_count and @_eol never be accessed directly.
40
16
 
41
- # Initializes the list by setting the two instance variable @item_count and
42
- # @eol. It is important that this method be called during the initialization
43
- # of the including class, and that the instance variables never be accessed
44
- # directly.
45
-
46
- def initialize(*)
47
- @eol = EOL.new list: self
48
- @item_count = 0
17
+ def initialize
18
+ @_eol = EOL.new self
19
+ @_item_count = 0
49
20
 
50
21
  super
51
22
  end
@@ -55,8 +26,8 @@ module Linked
55
26
  # this operation quite expensive.
56
27
 
57
28
  def initialize_dup(source)
58
- @eol = EOL.new list: self
59
- @item_count = 0
29
+ @_eol = EOL.new self
30
+ @_item_count = 0
60
31
 
61
32
  source.each_item { |item| push item.dup }
62
33
 
@@ -82,9 +53,25 @@ module Linked
82
53
 
83
54
  def item
84
55
  raise NoMethodError if empty?
85
- eol.next
56
+ head.next
86
57
  end
87
58
 
59
+ # Two lists are considered equal if the n:th item from each list are equal.
60
+ #
61
+ # other - any object.
62
+ #
63
+ # Returns true if the given object is a list and the items are equal.
64
+
65
+ def ==(other)
66
+ return false unless other.is_a? self.class
67
+ return false unless other.count == @_item_count
68
+
69
+ other_items = other.each_item
70
+ each_item.all? { |item| item == other_items.next }
71
+ end
72
+
73
+ alias eql? ==
74
+
88
75
  # Access the first n item(s) in the list. If a block is given each item will
89
76
  # be yielded to it. The first item, starting from the first in the list, for
90
77
  # which the block returns true and the n - 1 items directly following it
@@ -101,9 +88,9 @@ module Linked
101
88
  def first(n = 1)
102
89
  raise ArgumentError, 'n cannot be negative' if n < 0
103
90
 
104
- return first_item_after eol, n, count unless block_given?
91
+ return first_item_after head, n, count unless block_given?
105
92
 
106
- item = eol
93
+ item = head
107
94
  items_left = count
108
95
 
109
96
  items_left.times do
@@ -131,9 +118,9 @@ module Linked
131
118
  def last(n = 1)
132
119
  raise ArgumentError, 'n cannot be negative' if n < 0
133
120
 
134
- return last_item_before eol, n, count unless block_given?
121
+ return last_item_before tail, n, count unless block_given?
135
122
 
136
- item = eol
123
+ item = tail
137
124
  items_left = count
138
125
 
139
126
  items_left.times do
@@ -155,7 +142,7 @@ module Linked
155
142
 
156
143
  def count(*args)
157
144
  if args.empty? && !block_given?
158
- @item_count
145
+ @_item_count
159
146
  else
160
147
  super
161
148
  end
@@ -164,7 +151,7 @@ module Linked
164
151
  # Returns true if the list does not contain any items.
165
152
 
166
153
  def empty?
167
- @item_count == 0
154
+ @_item_count == 0
168
155
  end
169
156
 
170
157
  # Insert an item at the end of the list. If the given object is not an
@@ -178,7 +165,7 @@ module Linked
178
165
  # Returns self.
179
166
 
180
167
  def push(object)
181
- eol.append object
168
+ tail.append object
182
169
  self
183
170
  end
184
171
 
@@ -204,7 +191,7 @@ module Linked
204
191
  # Returns self.
205
192
 
206
193
  def unshift(object)
207
- eol.prepend object
194
+ head.prepend object
208
195
  self
209
196
  end
210
197
 
@@ -236,7 +223,7 @@ module Linked
236
223
  return to_enum(__method__) { count } unless block_given?
237
224
  return if empty?
238
225
 
239
- item = eol
226
+ item = head
240
227
  loop { yield item = item.next }
241
228
  end
242
229
 
@@ -249,7 +236,7 @@ module Linked
249
236
  return to_enum(__method__) { count } unless block_given?
250
237
  return if empty?
251
238
 
252
- item = eol
239
+ item = tail
253
240
  loop { yield item = item.prev }
254
241
  end
255
242
 
@@ -259,7 +246,7 @@ module Linked
259
246
  # (eol).
260
247
 
261
248
  def freeze
262
- eol.freeze
249
+ @_eol.freeze
263
250
  each_item(&:freeze)
264
251
  super
265
252
  end
@@ -289,7 +276,7 @@ module Linked
289
276
  # method is called whenever an arbitrary object is pushed or unshifted onto
290
277
  # the list and need to be wraped inside an Item.
291
278
  #
292
- # This method can be overridden to suport different Item types.
279
+ # This method can be overridden to support different Item types.
293
280
  #
294
281
  # args - any arguments will be passed on to Item.new.
295
282
  #
@@ -299,6 +286,16 @@ module Linked
299
286
  Item.new(*args)
300
287
  end
301
288
 
289
+ # Returns an object that responds to #next= and #prepend.
290
+
291
+ protected def head
292
+ @_eol
293
+ end
294
+
295
+ # Returns an object that responds to #prev= and #append.
296
+
297
+ alias tail head
298
+
302
299
  # Internal method to grow the list with n elements. Never call this method
303
300
  # without also inserting the n elements.
304
301
  #
@@ -307,7 +304,7 @@ module Linked
307
304
  # Returns updated the item count.
308
305
 
309
306
  private def grow(n = 1)
310
- @item_count += n
307
+ @_item_count += n
311
308
  end
312
309
 
313
310
  # Internal method to shrink the list with n elements. Never call this method
@@ -318,7 +315,7 @@ module Linked
318
315
  # Returns updated the item count.
319
316
 
320
317
  private def shrink(n = 1)
321
- @item_count -= n
318
+ @_item_count -= n
322
319
  end
323
320
 
324
321
  # Private method to clear the list. Never call this method without also
@@ -328,13 +325,11 @@ module Linked
328
325
  # b) clear the `next` pointer of the last item.
329
326
 
330
327
  private def clear
331
- head.send :next=, tail
332
- tail.send :prev=, head
333
-
334
- @item_count = 0
328
+ @_eol.send :reset
329
+ @_item_count = 0
335
330
  end
336
331
 
337
- # Protected helper method that returns the first n items, starting just
332
+ # Private helper method that returns the first n items, starting just
338
333
  # after item, given that there are items_left items left. Knowing the exact
339
334
  # number of items left is not cruicial but does impact speed. The number
340
335
  # should not be lower than the actual ammount. The following must
@@ -351,10 +346,10 @@ module Linked
351
346
  # n == 1) an item if items_left > 0 or nil
352
347
  # n > 1) an array of items if items_left > 0 or an empty array
353
348
 
354
- protected def first_item_after(item, n, items_left = @item_count)
349
+ private def first_item_after(item, n, items_left = @_item_count)
355
350
  # Optimize for these cases
356
351
  return nil if n == 0
357
- return n > 1 ? [] : nil if item.next!.nil?
352
+ return n > 1 ? [] : nil if item.last?
358
353
  return item.next if n == 1
359
354
 
360
355
  n = items_left if n > items_left
@@ -366,7 +361,7 @@ module Linked
366
361
  arr.compact! || arr
367
362
  end
368
363
 
369
- # Protected helper method that returns the last n items, ending just before
364
+ # Private helper method that returns the last n items, ending just before
370
365
  # item, given that there are items_left items left. Knowing the exact
371
366
  # number of items left is not cruicial but does impact speed. The number
372
367
  # should not be lower than the actual ammount. The following must hold for
@@ -383,10 +378,10 @@ module Linked
383
378
  # n == 1) an item if items_left > 0 or nil
384
379
  # n > 1) an array of items if items_left > 0 or an empty array
385
380
 
386
- protected def last_item_before(item, n, items_left = @item_count)
381
+ private def last_item_before(item, n, items_left = @_item_count)
387
382
  # Optimize for these cases
388
383
  return nil if n == 0
389
- return n > 1 ? [] : nil if item.prev!.nil?
384
+ return n > 1 ? [] : nil if item.first?
390
385
  return item.prev if n == 1
391
386
 
392
387
  n = items_left if n > items_left
@@ -397,13 +392,5 @@ module Linked
397
392
  rescue StopIteration
398
393
  arr.compact! || arr
399
394
  end
400
-
401
- # This method is called whenever the module is included somewhere. In the
402
- # special case when List is included in an Item the #item method must be
403
- # changed to return self.
404
-
405
- def self.included(klass)
406
- klass.send(:define_method, :item) { self } if klass < Item
407
- end
408
395
  end
409
396
  end
@@ -0,0 +1,441 @@
1
+ module Linked
2
+ # Listable
3
+ # List (optional)
4
+ # +----^-----+
5
+ # prev <- | Listable | -> next
6
+ # +----------+
7
+ #
8
+ # This module implements doubly linked list items, designed to work both on
9
+ # their own in a chain, and as children of list.
10
+ #
11
+ # An object is considered a list if it responds to #grow, #shrink and
12
+ # #create_item. The former two facilitate counting of the items and will be
13
+ # called everytime an item is appended, prepended or deleted. #create_item is
14
+ # expected to return a new object that is compatible with the list.
15
+ #
16
+ # Notation
17
+ # --------
18
+ #
19
+ # Some methods operate on chains of items, and to describe the effects of an
20
+ # operation the following syntax is used.
21
+ #
22
+ # A ( A <> B ) [ A <> B ]
23
+ # (i) (ii) (iii)
24
+ #
25
+ # Single items are denoted with capital letters (i), while chains are written
26
+ # as multiple connected items (ii). The parenthesis are optional. To show that
27
+ # one or more nodes are wraped in a list, angle brackets are used (iii).
28
+
29
+ module Listable
30
+ # Creates a new item. Always make a call to super whenever implementing this
31
+ # method in a subclass.
32
+
33
+ def initialize(*)
34
+ @_next = @_prev = @_list = nil
35
+ super
36
+ end
37
+
38
+ # Calling #dup on an item returns a copy that is no longer connected to the
39
+ # original item chain, or the list. The value will also be copied.
40
+ #
41
+ # Returns a new Item.
42
+
43
+ def initialize_copy(*)
44
+ @_next = @_prev = @_list = nil
45
+ super
46
+ end
47
+
48
+ # Identity method that simply return the item. This method mirrors List#item
49
+ # and allows other methods that work on Item objects to easily and
50
+ # interchangebly accept both lists and items as arguments.
51
+ #
52
+ # Returns the item itself.
53
+
54
+ def item
55
+ self
56
+ end
57
+
58
+ # Access the list that the item is part of. If the item is not in a list a
59
+ # NoMethodError will be raised. This mirrors the behaviour of List#list and
60
+ # allows other methods that work on List objects to easily and
61
+ # interchangeably accept both lists and items as arguments.
62
+ #
63
+ # Returns the list that the item is part of.
64
+
65
+ def list
66
+ raise NoMethodError unless @_list
67
+ @_list
68
+ end
69
+
70
+ # Check it the item is part of a list.
71
+ #
72
+ # Returns true if the item is in a list.
73
+
74
+ def in_list?
75
+ @_list ? true : false
76
+ end
77
+
78
+ # Check if this is the first item in the list. It is crucial that tail#nil?
79
+ # returns true for the first item to be identified correctly.
80
+ #
81
+ # Returns true if no item come before this one.
82
+
83
+ def first?
84
+ @_prev.nil?
85
+ end
86
+
87
+ # Check if this is the last item in the list. It is crucial that head#nil?
88
+ # returns true for the last item to be identified correctly.
89
+ #
90
+ # Returns true if no item come after this one.
91
+
92
+ def last?
93
+ @_next.nil?
94
+ end
95
+
96
+ # Check if the item is in the given list.
97
+ #
98
+ # list - any object.
99
+ #
100
+ # Returns true if the item is part of the given list.
101
+
102
+ def in?(list)
103
+ @_list.equal? list
104
+ end
105
+
106
+ # Access the next item in the list. If this is the last one a StopIteration
107
+ # will be raised, so that items may be iterated over safely in a loop.
108
+ #
109
+ # Example
110
+ # loop do
111
+ # item = item.next
112
+ # end
113
+ #
114
+ # Returns the item that come after this.
115
+
116
+ def next
117
+ raise StopIteration if last?
118
+ @_next
119
+ end
120
+
121
+ # Access the previous item in the list. If this is the first one a
122
+ # StopIteration will be raised, so that items may be iterated over safely in
123
+ # a loop.
124
+ #
125
+ # Example
126
+ # loop do
127
+ # item = item.prev
128
+ # end
129
+ #
130
+ # Returns the item that come before this.
131
+
132
+ def prev
133
+ raise StopIteration if first?
134
+ @_prev
135
+ end
136
+
137
+ alias previous prev
138
+
139
+ # Inserts the given item between this one and the one after it (if any). If
140
+ # the given item is part of a chain, all items following it will be moved to
141
+ # this one, and added to the list if one is set.
142
+ #
143
+ # Example for the chain (A <> C)
144
+ #
145
+ # A.append B # => (A <> B <> C)
146
+ #
147
+ # Alternativly the argument can be an arbitrary object, in which case a new
148
+ # item will be created around it.
149
+ #
150
+ # If this item is part of a list #grow will be called on it with the
151
+ # number of added items as an argument. Should it also be the last item
152
+ # #prev= will be called on the list tail.
153
+ #
154
+ # object - the item to append, or an arbitrary object to be wraped in a new
155
+ # item. If in a list it will be asked to create the new item via
156
+ # List#create_item.
157
+ #
158
+ # Returns the last item that was appended.
159
+
160
+ def append(object)
161
+ if object.respond_to? :item
162
+ first_item = object.item
163
+ last_item = first_item.send :extract_beginning_with, @_list
164
+ else
165
+ if @_list
166
+ first_item = last_item = @_list.send :create_item, object
167
+ first_item.list = @_list
168
+ @_list.send :grow
169
+ else
170
+ first_item = last_item = create_item object
171
+ end
172
+ end
173
+
174
+ first_item.prev = self
175
+ @_next.prev = last_item if @_next
176
+ @_next, last_item.next = first_item, @_next
177
+
178
+ last_item
179
+ end
180
+
181
+ # Inserts the given item between this one and the one before it (if any). If
182
+ # the given item is part of a chain, all items preceeding it will be moved
183
+ # to this one, and added to the list if one is set.
184
+ #
185
+ # Example for the chain (A <> C)
186
+ #
187
+ # C.prepend B # => (A <> B <> C)
188
+ #
189
+ # Alternativly the argument can be an arbitrary object, in which case a new
190
+ # item will be created around it.
191
+ #
192
+ # If this item is part of a list #grow will be called on it with the
193
+ # number of added items as an argument. Should it also be the first item
194
+ # #next= will be called on the list head.
195
+ #
196
+ # object - the item to prepend. or an arbitrary object to be wraped in a
197
+ # new item. If in a list it will be asked to create the new item
198
+ # via List#create_item.
199
+ #
200
+ # Returns the last item that was prepended.
201
+
202
+ def prepend(object)
203
+ if object.respond_to? :item
204
+ last_item = object.item
205
+ first_item = last_item.send :extract_ending_with, @_list
206
+ else
207
+ if @_list
208
+ first_item = last_item = @_list.send :create_item, object
209
+ first_item.list = @_list
210
+ @_list.send :grow
211
+ else
212
+ first_item = last_item = create_item object
213
+ end
214
+ end
215
+
216
+ last_item.next = self
217
+ @_prev.next = first_item if @_prev
218
+ @_prev, first_item.prev = last_item, @_prev
219
+
220
+ first_item
221
+ end
222
+
223
+ # Remove an item from the chain. If this item is part of a list and is
224
+ # either first, last or both in that list, #next= and #prev= will be called
225
+ # on the list head and tail respectivly.
226
+ #
227
+ # If this item is part of a list #shrink will be called on it.
228
+ #
229
+ # Returns self.
230
+
231
+ def delete
232
+ @_next.prev = @_prev if @_next
233
+ @_prev.next = @_next if @_prev
234
+ @_list.send :shrink if @_list
235
+
236
+ @_next = @_prev = @_list = nil
237
+ self
238
+ end
239
+
240
+ # Remove all items before this one in the chain. If the items are part of a
241
+ # list they will be removed from it.
242
+ #
243
+ # Returns the last item in the chain that was just deleted, or nil if this
244
+ # is the first item.
245
+
246
+ def delete_before
247
+ @_prev.send :extract_ending_with unless first?
248
+ end
249
+
250
+ # Remove all items after this one in the chain. If the items are part of a
251
+ # list they will be removed from it.
252
+ #
253
+ # Returns the last item in the chain that was just deleted, or nil if this
254
+ # is the first item.
255
+
256
+ def delete_after
257
+ @_next.send :extract_beginning_with unless last?
258
+ end
259
+
260
+ # Iterates over each item before this, in reverse order. If a block is not
261
+ # given an enumerator is returned.
262
+ #
263
+ # Note that raising a StopIteraion inside the block will cause the loop to
264
+ # silently stop the iteration early.
265
+
266
+ def before
267
+ return to_enum(__callee__) unless block_given?
268
+ return if first?
269
+
270
+ item = self.prev
271
+
272
+ loop do
273
+ yield item
274
+ item = item.prev
275
+ end
276
+ end
277
+
278
+ # Iterates over each item after this. If a block is not given an enumerator
279
+ # is returned.
280
+ #
281
+ # Note that raising a StopIteraion inside the block will cause the loop to
282
+ # silently stop the iteration early.
283
+
284
+ def after
285
+ return to_enum(__callee__) unless block_given?
286
+ return if last?
287
+
288
+ item = self.next
289
+
290
+ loop do
291
+ yield item
292
+ item = item.next
293
+ end
294
+ end
295
+
296
+ # Protected factory method for creating items compatible with this listable
297
+ # item. This method is called whenever an arbitrary object is appended or
298
+ # prepended onto this item and need to be wraped/converted.
299
+ #
300
+ # This method can be overridden to support different behaviours.
301
+ #
302
+ # args - any arguments will be passed on to .new.
303
+ #
304
+ # Returns a new Listable object.
305
+
306
+ protected def create_item(*args)
307
+ self.class.new(*args)
308
+ end
309
+
310
+ # Protected unsafe accessor of the next item in the list. It is preferable
311
+ # to use #next, possibly in conjunction with #last?.
312
+ #
313
+ # Returns the item that come after this, or nil if this is the last one.
314
+
315
+ protected def next!
316
+ @_next
317
+ end
318
+
319
+ # Calling #next= directly is not recommended since it may corrupt the chain.
320
+
321
+ protected def next=(other)
322
+ @_next = other
323
+ end
324
+
325
+ # Protected, unsafe accessor of the previous item in the list. It is
326
+ # preferable to use #prev, possibly in conjunction with #first?.
327
+ #
328
+ # Returns the item that come before this, or nil if this is the first one.
329
+
330
+ protected def prev!
331
+ @_prev
332
+ end
333
+
334
+ # Calling #prev= directly is not recommended since it may corrupt the chain.
335
+
336
+ protected def prev=(other)
337
+ @_prev = other
338
+ end
339
+
340
+ # Calling #list= directly is not recommended since it may corrupt the list.
341
+
342
+ protected def list=(list)
343
+ @_list = list
344
+ end
345
+
346
+ # PRIVATE DANGEROUS METHOD. This method should never be called directly
347
+ # since it may leave the extracted item chain in an invalid state.
348
+ #
349
+ # This method extracts the item, together with the chain following it, from
350
+ # the list they are in (if any) and optionally facilitates moving them to a
351
+ # new list.
352
+ #
353
+ # Given the two lists
354
+ # [ A <> B <> C ] [ D ]
355
+ # (I) (II)
356
+ #
357
+ # calling B.extract_beginning_with(II) will result in (B <> C) being removed
358
+ # from (I), and (II) to be grown by two. (B <> C) will now reference (II)
359
+ # but they will not yet be linked to any of the items in it. It is therefore
360
+ # necessary to insert them directly after calling this method, or (II) will
361
+ # be left in an invalid state.
362
+ #
363
+ # Returns the last item of the chain.
364
+
365
+ private def extract_beginning_with(new_list = nil)
366
+ old_list = @_list
367
+ # Count items and move them to the new list
368
+ last_item = self
369
+ count = 1 + loop.count do
370
+ last_item.list = new_list
371
+ last_item = last_item.next
372
+ end
373
+
374
+ # Make sure the old list is in a valid state
375
+ if old_list
376
+ if first?
377
+ old_list.send :clear
378
+ else
379
+ old_list.send :shrink, count
380
+ # Fix the links within in the list
381
+ @_prev.next = last_item.next!
382
+ last_item.next!.prev = @_prev
383
+ end
384
+ else
385
+ # Disconnect the item directly after the chain
386
+ @_prev.next = nil unless first?
387
+ end
388
+
389
+ # Disconnect the chain from the list
390
+ @_prev = last_item.next = nil
391
+
392
+ # Preemptivly tell the new list to grow
393
+ new_list.send :grow, count if new_list
394
+
395
+ last_item
396
+ end
397
+
398
+ # PRIVATE DANGEROUS METHOD. This method should never be called directly
399
+ # since it may leave the extracted item chain in an invalid state.
400
+ #
401
+ # This method extracts the item, together with the chain preceding it, from
402
+ # the list they are in (if any) and optionally facilitates moving them to a
403
+ # new list. See #extract_beginning_with for a description of the side
404
+ # effects from calling this method.
405
+ #
406
+ # Returns the first item in the chain.
407
+
408
+ private def extract_ending_with(new_list = nil)
409
+ old_list = @_list
410
+ # Count items and move them to the new list
411
+ first_item = self
412
+ count = 1 + loop.count do
413
+ first_item.list = new_list
414
+ first_item = first_item.prev
415
+ end
416
+
417
+ # Make sure the old list is in a valid state
418
+ if old_list
419
+ if last?
420
+ old_list.send :clear
421
+ else
422
+ old_list.send :shrink, count
423
+ # Fix the links within in the list
424
+ @_next.prev = first_item.prev!
425
+ first_item.prev!.next = @_next
426
+ end
427
+ else
428
+ # Disconnect the item directly after the chain
429
+ @_next.prev = nil unless last?
430
+ end
431
+
432
+ # Disconnect the chain from the list
433
+ first_item.prev = @_next = nil
434
+
435
+ # Preemptivly tell the new list to grow
436
+ new_list.send :grow, count if new_list
437
+
438
+ first_item
439
+ end
440
+ end
441
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Linked
4
- VERSION = '0.3.1'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/linked.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'linked/version'
4
+ require 'linked/listable'
4
5
  require 'linked/item'
5
6
  require 'linked/list/eol'
6
7
  require 'linked/list'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: linked
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sebastian Lindberg
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-08-10 00:00:00.000000000 Z
11
+ date: 2016-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -87,6 +87,7 @@ files:
87
87
  - lib/linked/item.rb
88
88
  - lib/linked/list.rb
89
89
  - lib/linked/list/eol.rb
90
+ - lib/linked/listable.rb
90
91
  - lib/linked/version.rb
91
92
  - linked.gemspec
92
93
  homepage: https://github.com/seblindberg/ruby-linked