lib/linked/item.rb in linked-0.2.0 vs lib/linked/item.rb in linked-0.2.1
- old
+ new
@@ -4,21 +4,34 @@
# Item
#
# This class implements doubly linked list items, designed to work both on
# their own and as children of list.
#
- # +- - - + +------+------+ +- - - +
- # | Head | <--| prev | next |--> ... --> | Tail |
- # + - - -+ +------+------+ + - - -+
- # (optional) First Item N Items (optional)
+ # +- - - + +------+------+ +- - - +
+ # | Head | <--| prev | next |--> ... --> | Tail |
+ # + - - -+ +------+------+ + - - -+
+ # (optional) First Item N Items (optional)
#
# An object is considered a list if it responds to #head, #tail, #grow and
# #shrink. The latter facilitate counting of the items and will be called
# everytime an item is appended, prepended or deleted. #head and #tail are
# expected to return two objects that, respectivly
# a) responds to #next= and #append, or #prev= and #prepend and
# b) returns true for #nil?.
+ #
+ # Notation
+ # --------
+ #
+ # Some methods operate on chains of items, and to describe the effects of an
+ # operation the following syntax is used.
+ #
+ # A ( A <> B ) [ A <> B ]
+ # (i) (ii) (iii)
+ #
+ # Single items are denoted with capital letters (i), while chains are written
+ # as multiple connected items (ii). The parenthesis are optional. To show that
+ # one or more nodes are wraped in a list, angle brackets are used (iii).
class Item
# Access the list (if any) that the item belongs to. Writing to this
# attribute is protected and should be avoided.
#
@@ -189,20 +202,21 @@
#
# By default all items followng this one will be kept together, but if given
# the argument after: true, the split will instead happen after this item
# and it will instead be kept with those before it.
#
- # Example
+ # Example for the chain (A <> B <> C)
#
- # item_b.split(after: false) => ~item_a~ |> item_b item_c
- # item_b.split(after: true) => item_a item_b <| ~item_c~
+ # B.split(after: false) => (A), (B <> C)
+ # B.split(after: true) => (A <> B), (C)
#
# after - determine wheter to split the chain before or after this item.
#
# Returns self.
def split after: false
+ warn '[DEPRECATION] this method will be removed in the next major update. Please use #delete_before and #delete_after instead'
if after
unless last?
if @list
item = self
count = 1 + loop.count do
@@ -244,10 +258,14 @@
# Inserts the given item between this one and the one after it (if any). If
# the given item is part of a chain, all items following it will be moved to
# this one, and added to the list if one is set.
#
+ # Example for the chain (A <> C)
+ #
+ # A.append B # => (A <> B <> C)
+ #
# Alternativly the argument can be an arbitrary object, in which case a new
# item will be created around it.
#
# If this item is part of a list #grow will be called on it with the
# number of added items as an argument. Should it also be the last item
@@ -256,37 +274,35 @@
# sibling - the item to append, or an arbitrary object to be wraped in a new
# item.
#
# Returns the last item that was appended.
- def append(sibling)
- if sibling.is_a? Item
- sibling.split
+ def append(object)
+ if object.respond_to? :item
+ first_item = object.item
+ last_item = first_item.send :extract_beginning_with, @list
else
- sibling = self.class.new sibling
+ first_item = last_item = self.class.new object
+ first_item.list = @list
+ @list.send :grow if @list
end
-
- sibling.prev = self
- after_sibling = @next
- @next = sibling
-
- count = 1 + loop.count do
- sibling.list = @list
- sibling = sibling.next
- end
-
- @list.send :grow, count if @list
-
- sibling.next = after_sibling
- after_sibling.prev = sibling if after_sibling
- sibling
+
+ first_item.prev = self
+ @next.prev = last_item if @next
+ @next, last_item.next = first_item, @next
+
+ last_item
end
# Inserts the given item between this one and the one before it (if any). If
# the given item is part of a chain, all items preceeding it will be moved
# to this one, and added to the list if one is set.
#
+ # Example for the chain (A <> C)
+ #
+ # C.prepend B # => (A <> B <> C)
+ #
# Alternativly the argument can be an arbitrary object, in which case a new
# item will be created around it.
#
# If this item is part of a list #grow will be called on it with the
# number of added items as an argument. Should it also be the first item
@@ -295,33 +311,27 @@
# sibling - the item to prepend. or an arbitrary object to be wraped in a
# new item.
#
# Returns the last item that was prepended.
- def prepend(sibling)
- if sibling.is_a? Item
- sibling.split after: true
+ def prepend(object)
+ if object.respond_to? :item
+ last_item = object.item
+ first_item = last_item.send :extract_ending_with, @list
else
- sibling = self.class.new sibling
+ first_item = last_item = self.class.new object
+ first_item.list = @list
+ @list.send :grow, 1 if @list
end
-
- sibling.next = self
- before_sibling = @prev
- @prev = sibling
-
- count = 1 + loop.count do
- sibling.list = @list
- sibling = sibling.prev
- end
-
- @list.send :grow, count if @list
-
- sibling.prev = before_sibling
- before_sibling.next = sibling if before_sibling
- sibling
+
+ last_item.next = self
+ @prev.next = first_item if @prev
+ @prev, first_item.prev = last_item, @prev
+
+ first_item
end
-
+
# Remove an item from the chain. If this item is part of a list and is
# either first, last or both in that list, #next= and #prev= will be called
# on the list head and tail respectivly.
#
# If this item is part of a list #shrink will be called on it.
@@ -334,13 +344,36 @@
@list.send :shrink if @list
@next = @prev = @list = nil
self
end
+
+ # Remove all items before this one in the chain. If the items are part of a
+ # list they will be removed from it.
+ #
+ # Returns the last item in the chain that was just deleted, or nil if this
+ # is the first item.
+
+ def delete_before
+ @prev.send :extract_ending_with unless first?
+ end
+
+ # Remove all items after this one in the chain. If the items are part of a
+ # list they will be removed from it.
+ #
+ # Returns the last item in the chain that was just deleted, or nil if this
+ # is the first item.
+
+ def delete_after
+ @next.send :extract_beginning_with unless last?
+ end
# Iterates over each item before this, in reverse order. If a block is not
# given an enumerator is returned.
+ #
+ # Note that raising a StopIteraion inside the block will cause the loop to
+ # silently stop the iteration early.
def before
return to_enum(__callee__) unless block_given?
return if first?
@@ -352,10 +385,13 @@
end
end
# Iterates over each item after this. If a block is not given an enumerator
# is returned.
+ #
+ # Note that raising a StopIteraion inside the block will cause the loop to
+ # silently stop the iteration early.
def after
return to_enum(__callee__) unless block_given?
return if last?
@@ -381,8 +417,103 @@
def inspect
return yield self if block_given?
output = format '%s:0x%0x', self.class.name, object_id
value ? output + " value=#{value.inspect}" : output
+ end
+
+ # PRIVATE DANGEROUS METHOD. This method should never be called directly
+ # since it may leave the extracted item chain in an invalid state.
+ #
+ # This method extracts the item, together with the chain following it, from
+ # the list they are in (if any) and optionally facilitates moving them to a
+ # new list.
+ #
+ # Given the two lists
+ # [ A <> B <> C ] [ D ]
+ # (I) (II)
+ #
+ # calling B.extract_beginning_with(II) will result in (B <> C) being removed
+ # from (I), and (II) to be grown by two. (B <> C) will now reference (II)
+ # but they will not yet be linked to any of the items in it. It is therefore
+ # necessary to insert them directly after calling this method, or (II) will
+ # be left in an invalid state.
+ #
+ # Returns the last item of the chain.
+
+ private def extract_beginning_with(new_list = nil)
+ old_list = @list
+ # Count items and move them to the new list
+ last_item = self
+ count = 1 + loop.count do
+ last_item.list = new_list
+ last_item = last_item.next
+ end
+
+ # Make sure the old list is in a valid state
+ if old_list
+ if first?
+ old_list.send :clear
+ else
+ old_list.send :shrink, count
+ # Fix the links within in the list
+ @prev.next = last_item.next!
+ last_item.next!.prev = @prev
+ end
+ else
+ # Disconnect the item directly after the chain
+ @prev.next = nil unless first?
+ end
+
+ # Disconnect the chain from the list
+ @prev = last_item.next = nil
+
+ # Preemptivly tell the new list to grow
+ new_list.send :grow, count if new_list
+
+ last_item
+ end
+
+ # PRIVATE DANGEROUS METHOD. This method should never be called directly
+ # since it may leave the extracted item chain in an invalid state.
+ #
+ # This method extracts the item, together with the chain preceding it, from
+ # the list they are in (if any) and optionally facilitates moving them to a
+ # new list. See #extract_beginning_with for a description of the side
+ # effects from calling this method.
+ #
+ # Returns the first item in the chain.
+
+ private def extract_ending_with(new_list = nil)
+ old_list = @list
+ # Count items and move them to the new list
+ first_item = self
+ count = 1 + loop.count do
+ first_item.list = new_list
+ first_item = first_item.prev
+ end
+
+ # Make sure the old list is in a valid state
+ if old_list
+ if last?
+ old_list.send :clear
+ else
+ old_list.send :shrink, count
+ # Fix the links within in the list
+ @next.prev = first_item.prev!
+ first_item.prev!.next = @next
+ end
+ else
+ # Disconnect the item directly after the chain
+ @next.prev = nil unless last?
+ end
+
+ # Disconnect the chain from the list
+ first_item.prev = @next = nil
+
+ # Preemptivly tell the new list to grow
+ new_list.send :grow, count if new_list
+
+ first_item
end
end
end