linked 0.3.1 → 0.4.0

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 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