312 lines
6.5 KiB
Ruby
312 lines
6.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# Released under the MIT License.
|
|
# Copyright, 2022-2024, by Samuel Williams.
|
|
|
|
module Async
|
|
# A general doublely linked list. This is used internally by {Async::Barrier} and {Async::Condition} to manage child tasks.
|
|
class List
|
|
# Initialize a new, empty, list.
|
|
def initialize
|
|
@head = self
|
|
@tail = self
|
|
@size = 0
|
|
end
|
|
|
|
# @returns [String] A short summary of the list.
|
|
def to_s
|
|
sprintf("#<%s:0x%x size=%d>", self.class.name, object_id, @size)
|
|
end
|
|
|
|
alias inspect to_s
|
|
|
|
# Fast, safe, unbounded accumulation of children.
|
|
def to_a
|
|
items = []
|
|
current = self
|
|
|
|
while current.tail != self
|
|
unless current.tail.is_a?(Iterator)
|
|
items << current.tail
|
|
end
|
|
|
|
current = current.tail
|
|
end
|
|
|
|
return items
|
|
end
|
|
|
|
# @attribute [Node | Nil] Points at the end of the list.
|
|
attr_accessor :head
|
|
|
|
# @attribute [Node | Nil] Points at the start of the list.
|
|
attr_accessor :tail
|
|
|
|
# @attribute [Integer] The number of nodes in the list.
|
|
attr :size
|
|
|
|
# A callback that is invoked when an item is added to the list.
|
|
def added(node)
|
|
@size += 1
|
|
return node
|
|
end
|
|
|
|
# Append a node to the end of the list.
|
|
def append(node)
|
|
if node.head
|
|
raise ArgumentError, "Node is already in a list!"
|
|
end
|
|
|
|
node.tail = self
|
|
@head.tail = node
|
|
node.head = @head
|
|
@head = node
|
|
|
|
return added(node)
|
|
end
|
|
|
|
# Prepend a node to the start of the list.
|
|
def prepend(node)
|
|
if node.head
|
|
raise ArgumentError, "Node is already in a list!"
|
|
end
|
|
|
|
node.head = self
|
|
@tail.head = node
|
|
node.tail = @tail
|
|
@tail = node
|
|
|
|
return added(node)
|
|
end
|
|
|
|
# Add the node, yield, and the remove the node.
|
|
# @yields {|node| ...} Yields the node.
|
|
# @returns [Object] Returns the result of the block.
|
|
def stack(node, &block)
|
|
append(node)
|
|
return yield(node)
|
|
ensure
|
|
remove!(node)
|
|
end
|
|
|
|
# A callback that is invoked when an item is removed from the list.
|
|
def removed(node)
|
|
@size -= 1
|
|
return node
|
|
end
|
|
|
|
# Remove the node if it is in a list.
|
|
#
|
|
# You should be careful to only remove nodes that are part of this list.
|
|
#
|
|
# @returns [Node] Returns the node if it was removed, otherwise nil.
|
|
def remove?(node)
|
|
if node.head
|
|
return remove!(node)
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
# Remove the node. If it was already removed, this will raise an error.
|
|
#
|
|
# You should be careful to only remove nodes that are part of this list.
|
|
#
|
|
# @raises [ArgumentError] If the node is not part of this list.
|
|
# @returns [Node] Returns the node if it was removed, otherwise nil.
|
|
def remove(node)
|
|
# One downside of this interface is we don't actually check if the node is part of the list defined by `self`. This means that there is a potential for a node to be removed from a different list using this method, which in can throw off book-keeping when lists track size, etc.
|
|
unless node.head
|
|
raise ArgumentError, "Node is not in a list!"
|
|
end
|
|
|
|
remove!(node)
|
|
end
|
|
|
|
private def remove!(node)
|
|
node.head.tail = node.tail
|
|
node.tail.head = node.head
|
|
|
|
# This marks the node as being removed, and causes remove to fail if called a 2nd time.
|
|
node.head = nil
|
|
# node.tail = nil
|
|
|
|
return removed(node)
|
|
end
|
|
|
|
# @returns [Boolean] Returns true if the list is empty.
|
|
def empty?
|
|
@size == 0
|
|
end
|
|
|
|
# def validate!(node = nil)
|
|
# previous = self
|
|
# current = @tail
|
|
# found = node.equal?(self)
|
|
|
|
# while true
|
|
# break if current.equal?(self)
|
|
|
|
# if current.head != previous
|
|
# raise "Invalid previous linked list node!"
|
|
# end
|
|
|
|
# if current.is_a?(List) and !current.equal?(self)
|
|
# raise "Invalid list in list node!"
|
|
# end
|
|
|
|
# if node
|
|
# found ||= current.equal?(node)
|
|
# end
|
|
|
|
# previous = current
|
|
# current = current.tail
|
|
# end
|
|
|
|
# if node and !found
|
|
# raise "Node not found in list!"
|
|
# end
|
|
# end
|
|
|
|
# Iterate over each node in the linked list. It is generally safe to remove the current node, any previous node or any future node during iteration.
|
|
#
|
|
# @yields {|node| ...} Yields each node in the list.
|
|
# @returns [List] Returns self.
|
|
def each(&block)
|
|
return to_enum unless block_given?
|
|
|
|
Iterator.each(self, &block)
|
|
|
|
return self
|
|
end
|
|
|
|
# Determine whether the given node is included in the list.
|
|
#
|
|
# @parameter needle [Node] The node to search for.
|
|
# @returns [Boolean] Returns true if the node is in the list.
|
|
def include?(needle)
|
|
self.each do |item|
|
|
return true if needle.equal?(item)
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
# @returns [Node] Returns the first node in the list, if it is not empty.
|
|
def first
|
|
# validate!
|
|
|
|
current = @tail
|
|
|
|
while !current.equal?(self)
|
|
if current.is_a?(Iterator)
|
|
current = current.tail
|
|
else
|
|
return current
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
# @returns [Node] Returns the last node in the list, if it is not empty.
|
|
def last
|
|
# validate!
|
|
|
|
current = @head
|
|
|
|
while !current.equal?(self)
|
|
if current.is_a?(Iterator)
|
|
current = current.head
|
|
else
|
|
return current
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
# Shift the first node off the list, if it is not empty.
|
|
def shift
|
|
if node = first
|
|
remove!(node)
|
|
end
|
|
end
|
|
|
|
# A linked list Node.
|
|
class Node
|
|
attr_accessor :head
|
|
attr_accessor :tail
|
|
|
|
alias inspect to_s
|
|
end
|
|
|
|
class Iterator < Node
|
|
def initialize(list)
|
|
@list = list
|
|
|
|
# Insert the iterator as the first item in the list:
|
|
@tail = list.tail
|
|
@tail.head = self
|
|
list.tail = self
|
|
@head = list
|
|
end
|
|
|
|
def remove!
|
|
@head.tail = @tail
|
|
@tail.head = @head
|
|
@head = nil
|
|
@tail = nil
|
|
@list = nil
|
|
end
|
|
|
|
def move_next
|
|
# Move to the next item (which could be an iterator or the end):
|
|
@tail.head = @head
|
|
@head.tail = @tail
|
|
@head = @tail
|
|
@tail = @tail.tail
|
|
@head.tail = self
|
|
@tail.head = self
|
|
end
|
|
|
|
def move_current
|
|
while true
|
|
# Are we at the end of the list?
|
|
if @tail.equal?(@list)
|
|
return nil
|
|
end
|
|
|
|
if @tail.is_a?(Iterator)
|
|
move_next
|
|
else
|
|
return @tail
|
|
end
|
|
end
|
|
end
|
|
|
|
def each
|
|
while current = move_current
|
|
yield current
|
|
|
|
if current.equal?(@tail)
|
|
move_next
|
|
end
|
|
end
|
|
end
|
|
|
|
def self.each(list, &block)
|
|
return if list.empty?
|
|
|
|
iterator = Iterator.new(list)
|
|
|
|
iterator.each(&block)
|
|
ensure
|
|
iterator&.remove!
|
|
end
|
|
end
|
|
|
|
private_constant :Iterator
|
|
end
|
|
end
|