Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Linked Lists in linear-data-structures #51

Closed

Conversation

rohansingh9001
Copy link

Added linked-lists.py in linear-data-structures
Contains Doubly Linked List

References to other Issues or PRs or Relevant literature

Partially fixes issue #49 which requests to add linked lists.
Only Doubly Linked List added so far Single Linked List remains.

Brief description of what is fixed or changed

API for the additions are as follows -

API for Double Linked List:

1)A Doubly linked list should be initialised with DoubleLinkedList()

2)Elements can be pushed at the start of the list using .appendleft(data)

3)Elements can be appended at the end using .appendright(data)

  1. can be popped from the list at a certain index by .pop(index)

5)__getitem__ and __setitem__ support using python syntax,
eg -
list[1]
list[5] =4

6)Print the Linked List in List form using print() function
eg -
if elements in linked list named sample_list are 1-2-3-4
>>>print(sample_list)
[1,2,3,4]

7)Return length of the Linked List using len() function.

8).popright() and .popleft() pops items from the right and the left respectively.

9).insertAt(index, data) inserts a new Node at the index.

10).insertBefore(Node, data) and .insertAfter(Node, data) inserts a new Node before and after the input Node respectively.

@codecov
Copy link

codecov bot commented Dec 18, 2019

Codecov Report

Merging #51 into master will decrease coverage by 9.239%.
The diff coverage is 18.055%.

@@             Coverage Diff              @@
##            master       #51      +/-   ##
============================================
- Coverage   98.003%   88.764%   -9.24%     
============================================
  Files           17        18       +1     
  Lines         1102      1246     +144     
============================================
+ Hits          1080      1106      +26     
- Misses          22       140     +118
Impacted Files Coverage Δ
pydatastructs/linear_data_structures/__init__.py 100% <100%> (ø) ⬆️
...datastructs/linear_data_structures/linked_lists.py 17.482% <17.482%> (ø)

Impacted file tree graph

self.prev = NoneType
#Alternative constructor for Single Linked List
@classmethod
def singleLink(obj, data):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

singleLink -> single_link

Why it's a class method?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The second method is supposed to be an alternative constructor for the Node class when applied to Single LInked Lists. In that case single LInk will be used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's analyse the situation.
If we use two separate classes for Node one for Doubly Linked lists and other for singly linked lists, then it will lead to unnecessary code.
However, if we use Node for Doubly Linked Lists to support the singly linked lists then it will lead to wastage of space.
There can be multiple links in a single node like more than 2, in that case both of the above solutions will become incompatible.
So, what about passing an extra parameter in the constructor specifying the number of links it will have. In fact, we can take input from the user, the name of each link and create the attributes dynamically, something like in this answer https://stackoverflow.com/a/1325768
I will think more about it's feasibility.

@czgdp1807
Copy link
Member

The code coverage is very low. Please add tests for this class. Shift the Node class to misc_util. Make an abstract class LinkedList as a super class and subclass it in every type of linked list. Overall the PR LGTM.

@rohansingh9001
Copy link
Author

Sure, although can you explain how code coverage works? I heard there needs to bbe unit test coverage what does it mean?

@czgdp1807
Copy link
Member

Coverage measurement is typically used to gauge the effectiveness of tests. It can show which parts of your code are being exercised by tests, and which are not.

You can use coverage.py and pytest for analysing coverage locally.
Use, python3 -m pytest --doctest-modules --cov=./ --cov-report=html and open index.html in htmlcov to see which lines are being missed by your tests. We want to keep the coverage of the code as high as possible.

@czgdp1807
Copy link
Member

Thanks for the changes. I will test in my system. Will make changes to your code and will share the diff which you can apply and push the changes later.

@czgdp1807
Copy link
Member

diff --git a/pydatastructs/linear_data_structures/__init__.py b/pydatastructs/linear_data_structures/__init__.py
index a21fe38..0fe59d9 100644
--- a/pydatastructs/linear_data_structures/__init__.py
+++ b/pydatastructs/linear_data_structures/__init__.py
@@ -9,8 +9,9 @@ from .arrays import (
     OneDimensionalArray,
     DynamicOneDimensionalArray
 )
+__all__.extend(arrays.__all__)
 
 from .linked_lists import (
     DoublyLinkedList
 )
-__all__.extend(arrays.__all__)
+__all__.extend(linked_lists.__all__)
diff --git a/pydatastructs/linear_data_structures/linked_lists.py b/pydatastructs/linear_data_structures/linked_lists.py
index 974ab67..5915338 100644
--- a/pydatastructs/linear_data_structures/linked_lists.py
+++ b/pydatastructs/linear_data_structures/linked_lists.py
@@ -1,330 +1,284 @@
 from __future__ import print_function, division
-from pydatastructs.utils.misc_util import _check_type, NoneType
-from pydatastructs.utils import ( 
-    Node
-)
-
-__author__ = 'rohansingh9001'
+from pydatastructs.utils.misc_util import _check_type, LinkedListNode
 
 __all__ = [
-    'DoublyLinkedList',
-    'LinkedList'
+    'DoublyLinkedList'
 ]
 
 class LinkedList(object):
     '''
-    Abstract class for Linked List in pydatastructs.
+    Abstract class for Linked List.
     '''
     pass
 
-# Class to create a Doubly Linked List 
-class DoublyLinkedList(LinkedList): 
-    
-
-    '''
-    A Doubly Linked List Class (abb. DLL)
-    
-    Parameters
-    ==========
-    
-    None
+class DoublyLinkedList(LinkedList):
+    """
+    Represents Doubly Linked List
 
     Examples
     ========
-    >>> from pydatastructs import DoublyLinkedLIst as DLL
-    >>> dll = DLL()
+
+    >>> from pydatastructs import DoublyLinkedList
+    >>> dll = DoublyLinkedList()
     >>> dll.append(6)
-    >>> arr[0]
+    >>> dll[0].data
     6
-    >>> dll.head
+    >>> dll.head.data
     6
     >>> dll.append(5)
-    >>> dll.appendleft(2)
+    >>> dll.append_left(2)
     >>> print(dll)
-    [2,6,5]
-    >>> dll[0] = 7.2
-    >>> dll[0]
-    7.2
-    >>> dll.pop(1)
+    [2, 6, 5]
+    >>> dll[0].data = 7.2
+    >>> dll.extract(1).data
     6
     >>> print(dll)
-    [2,5]
+    [7.2, 5]
 
     References
     ==========
-    
-    https://www.geeksforgeeks.org/doubly-linked-list/
 
-    '''
-    __slots__ = ['head','tail','length']
-
-    def __init__(self):
-        self.head = NoneType
-        self.tail = NoneType
-        self.length = 0
-
-    def appendleft(self,data):
-        '''
-        appendleft: 
-        takes parameters - data
-            data: type 
-                A valid object type
-                Should be convertible to string using str() method to 
-                use print() method on instance.
-        action - Pushes a new node at the start i.e. left of the DLL.
-        '''
-        self.length += 1
-        newNode = Node(data)
-        if self.head is not NoneType:
-            self.head.prev = newNode
-            newNode.next = self.head
-        self.head = newNode
-
-        if newNode.next == NoneType:
-            self.tail = newNode
-        if newNode.prev == NoneType:
-            self.head = newNode
-    
+    .. [1] https://en.wikipedia.org/wiki/Doubly_linked_list
+
+    """
+    __slots__ = ['head', 'tail', 'size']
+
+    def __new__(cls):
+        obj = object.__new__(cls)
+        obj.head = None
+        obj.tail = None
+        obj.size = 0
+        return obj
+
+    def append_left(self, data):
+        """
+        Pushes a new node at the start i.e.,
+        the left of the list.
+
+        Parameters
+        ==========
+
+        data
+            Any valid data to be stored in the node.
+        """
+        self.insert_at(0, data)
+
     def append(self, data):
-        '''
-        append: 
-        takes parameters - data
-            data: type
-                A valid object type.
-                Should be convertible to string using str() method to 
-                use print() method on instance.
-        action - Appends a new node at the end i.e. the right of the DLL.
-        '''
-        self.length += 1
-        newNode = Node(data)
-        if self.tail is not NoneType:
-            self.tail.next = newNode
-            newNode.prev = self.tail
-        self.tail = newNode
-
-        if newNode.next == NoneType:
-            self.tail = newNode
-        if newNode.prev == NoneType:
-            self.head = newNode
-
-    def insertAfter(self, prevNode, data):
-        '''
-        insertAfter:
-        takes parameters - prevNode, data
-            prevNode: Node type 
-                An object of Node class 
-            data: type
-                A valid object type.
-                Should be convertible to string using str() method to
-                use print() method in instance.
-        action - Inserts a new node after the prevNode.
-        '''
-        self.length += 1
-        newNode = Node(data)
-        newNode.next = prevNode.next
-        prevNode.next = newNode
-        newNode.prev = prevNode
-        
-        if newNode.next == NoneType:
-            self.tail = newNode
-        if newNode.prev == NoneType:
-            self.head = newNode
-    
-    def insertBefore(self, nextNode, data):
-        '''
-        insertBefore:
-        takes parameters - nextNode, data
-            prevNode: Node type
-                An object of Node class
-            data: type
-                A valid object type.
-                Should be convertible to string using str() method to 
-                use print() method in instance.
-        action - Inserts a new node before the newNode.
-        '''
-        self.length += 1
-        newNode = Node(data)
-        newNode.prev = nextNode.prev
-        nextNode.prev = newNode
-        newNode.next = nextNode
-        
-        if newNode.next == NoneType:
-            self.tail = newNode
-        if newNode.prev == NoneType:
-            self.head = newNode
-    
-    def insertAt(self, index, data):
-        '''
-        insertAt:
-        takes parameters - index, data
-            index: int type
-                An integer i such that 0<= i <= length, where length 
-                refers to the length of the List.
-            data: type
-                A valid object type.
-                Should be convertible to string using str() method to
-                use print() method in instance.
-        action - Inserts a new node at the input index.
-        '''
-        if index > self.length or index < 0 or not (_check_type(index, int)):
-            raise ValueError('Index input out of range/Index is expected to be an Integer.')
+        """
+        Appends a new node at the end of the list.
+
+        Parameters
+        ==========
+
+        data
+            Any valid data to be stored in the node.
+        """
+        self.insert_at(self.size, data)
+
+    def insert_after(self, prev_node, data):
+        """
+        Inserts a new node after the prev_node.
+
+        Parameters
+        ==========
+
+        prev_node: LinkedListNode
+            The node after which the
+            new node is to be inserted.
+
+        data
+            Any valid data to be stored in the node.
+        """
+        self.size += 1
+        new_node = LinkedListNode(data,
+                                 links=['next', 'prev'],
+                                 addrs=[None, None])
+        new_node.next = prev_node.next
+        prev_node.next = new_node
+        new_node.prev = prev_node
+
+        if new_node.next is None:
+            self.tail = new_node
+
+    def insert_before(self, next_node, data):
+        """
+        Inserts a new node before the new_node.
+
+        Parameters
+        ==========
+
+        next_node: LinkedListNode
+            The node before which the
+            new node is to be inserted.
+
+        data
+            Any valid data to be stored in the node.
+        """
+        self.size += 1
+        new_node = LinkedListNode(data,
+                                 links=['next', 'prev'],
+                                 addrs=[None, None])
+        new_node.prev = next_node.prev
+        next_node.prev = new_node
+        new_node.next = next_node
+
+        if new_node.prev is None:
+            self.head = new_node
+
+    def insert_at(self, index, data):
+        """
+        Inserts a new node at the input index.
+
+        Parameters
+        ==========
+
+        index: int
+            An integer satisfying python indexing properties.
+
+        data
+            Any valid data to be stored in the node.
+        """
+        if self.size == 0 and (index in (0, -1)):
+            index = 0
+
+        if index < 0:
+            index = self.size + index
+
+        if index > self.size:
+            raise IndexError('%d index is out of range.'%(index))
+
+        self.size += 1
+        new_node = LinkedListNode(data,
+                                    links=['next', 'prev'],
+                                    addrs=[None, None])
+        if self.size == 1:
+            self.head, self.tail = \
+                new_node, new_node
         else:
-            if index == 0:
-                self.appendleft(data)
-            elif index == self.length:
-                self.appendright(data)
-            else:  
-                self.length += 1
-                newNode = Node(data)
-                counter = 0
-                currentNode = self.head
-                while counter != index:
-                    currentNode = currentNode.next
-                    counter += 1
-                currentNode.prev.next = newNode
-                newNode.prev = currentNode.prev
-                currentNode.prev = newNode
-                newNode.next = currentNode
-                    
-                if newNode.next == NoneType:
-                    self.tail = newNode
-                if newNode.prev == NoneType:
-                    self.head = newNode
-    
-    def popleft(self):
-        '''
-        popleft: 
-        takes parameters - None
-        action - Removes the Node from the left i.e. start of the DLL
-         and returns the data from the Node.
-        '''
-        self.length -= 1
-        oldHead = self.head
-        oldHead.next.prev = NoneType
-        self.head = oldHead.next
-        return oldHead.data 
-
-    def popright(self):
-        '''
-        popright:
-        takes parameters - None
-        action - Removes the Node from the right i.e. end of the DLL 
-        and returns the data from the Node.
-        '''
-        self.length -= 1
-        oldTail = self.tail
-        oldTail.prev.next = NoneType
-        self.tail = oldTail.prev
-        return oldTail.data
-
-    def pop(self, index=0):
-        '''
-        pop:
-        takes parameters - index
-            index: int type
-                An integer i such that 0<= i <= length, where length
-                 refers to the length of the List.
-        action - Removes the Node at the index of the DLL and returns
-         the data from the Node.
-        '''
-        if index > self.length or index < 0 or not (_check_type(index, int)):
-            raise ValueError('Index input out of range/Index is expected to be an Integer.') 
-        else:  
-            if index == 0:
-                self.popleft()
-            elif index == self.length:
-                self.popright()
-            else:
-                self.length -= 1
-                counter = 0
-                currentNode = self.head
-                while counter != index:
-                    currentNode = currentNode.next
-                    counter += 1
-                currentNode.prev.next = currentNode.next
-                currentNode.next.prev = currentNode.prev
-                return currentNode.data
-
-    def __getitem__(self, index): 
-        '''
-        __getitem__:
-        takes parameters - index
-            index: int type
-                An integer i such that 0<= i <= length, where length 
-                refers to the length of the List.
-        action - Returns the data of the Node at index.
-        '''
-        if index > self.length or index < 0 or not (_check_type(index, int)):
-            raise ValueError('Index input out of range/Index is expected to be an Integer.')
-        else:     
-            counter = 0
-            currentNode = self.head      
-            while counter != index:
-                currentNode = currentNode.next
-                counter += 1
-            return currentNode.data
-
-    def __setitem__(self, index, data):
-        '''
-        __setitem__:
-        takes parameters - index, data
-            index: int type
-                An integer i such that 0<= i <= length, where length 
-                refers to the length of the List.
-            data: type
-                A valid object type.
-                Should be convertible to string using str() method to use 
-                print() method in instance.
-        action - Sets the data of the Node at the index to the input data.
-        '''
-        if index > self.length or index < 0 or not (_check_type(index, int)):
-            raise ValueError('Index input out of range/Index is expected to be an Integer.')
-        else:  
             counter = 0
-            currentNode = self.head    
+            current_node = self.head
+            prev_node = None
             while counter != index:
-                currentNode = currentNode.next
+                prev_node = current_node
+                current_node = current_node.next
                 counter += 1
-            currentNode.data = data
+            new_node.prev = prev_node
+            new_node.next = current_node
+            if prev_node is not None:
+                prev_node.next = new_node
+            if current_node is not None:
+                current_node.prev = new_node
+            if new_node.next is None:
+                self.tail = new_node
+            if new_node.prev is None:
+                self.head = new_node
+
+    def pop_left(self):
+        """
+        Extracts the Node from the left
+        i.e. start of the list.
+
+        Returns
+        =======
+
+        old_head: LinkedListNode
+            The leftmost element of linked
+            list.
+        """
+        self.extract(0)
+
+    def pop_right(self):
+        """
+        Extracts the node from the right
+        of the linked list.
+
+        Returns
+        =======
+
+        old_tail: LinkedListNode
+            The leftmost element of linked
+            list.
+        """
+        self.extract(-1)
+
+    def extract(self, index):
+        """
+        Extracts the node at the index of the list.
+
+        Parameters
+        ==========
+
+        index: int
+            An integer satisfying python indexing properties.
+
+        Returns
+        =======
+
+        current_node: LinkedListNode
+            The node at index i.
+        """
+        if self.is_empty:
+            raise ValueError("The list is empty.")
+
+        if index < 0:
+            index = self.size + index
+
+        if index >= self.size:
+            raise IndexError('%d is out of range.'%(index))
+
+        self.size -= 1
+        counter = 0
+        current_node = self.head
+        prev_node = None
+        while counter != index:
+            prev_node = current_node
+            current_node = current_node.next
+            counter += 1
+        if prev_node is not None:
+            prev_node.next = current_node.next
+        if current_node.next is not None:
+            current_node.next.prev = prev_node
+        if index == 0:
+            self.head = current_node.next
+        if index == self.size:
+            self.tail = current_node.prev
+        return current_node
+
+    def __getitem__(self, index):
+        """
+        Returns
+        =======
+
+        current_node: LinkedListNode
+            The node at given index.
+        """
+        if index < 0:
+            index = self.size + index
+
+        if index >= self.size:
+            raise IndexError('%d index is out of range.'%(index))
+
+        counter = 0
+        current_node = self.head
+        while counter != index:
+            current_node = current_node.next
+            counter += 1
+        return current_node
 
     def __str__(self):
-        '''
-        __str__:
-        takes parameters - None
-        action - Prints the DLL in a list from from the start to the end.
-        '''
+        """
+        For printing the linked list.
+        """
         elements = []
-        currentNode = self.head
-        while currentNode is not NoneType:
-            elements.append(currentNode.data)
-            currentNode = currentNode.next
+        current_node = self.head
+        while current_node is not None:
+            elements.append(current_node.data)
+            current_node = current_node.next
         return str(elements)
 
     def __len__(self):
-        '''
-        __len__:
-        takes parameters - None
-        action - Returns the length of the DLL.
-        '''
-        return self.length
-
-    def isEmpty(self):
-        '''
-        isEmpty:
-        takes parameters - None
-        action - Return a bool value to check if the DLL is empty or not.
-        '''
-        return self.length == 0
-
-if __name__ == '__main__':
-    dll = DoublyLinkedList()
-    dll.append(6)
-    arr[0]
-    dll.head
-    dll.append(5)
-    dll.appendleft(2)
-    print(dll)
-    dll[0] = 7.2
-    dll[0]
-    dll.pop(1)
-    print(dll)
\ No newline at end of file
+        return self.size
+
+    @property
+    def is_empty(self):
+        return self.size == 0
diff --git a/pydatastructs/linear_data_structures/tests/test_linked_lists.py b/pydatastructs/linear_data_structures/tests/test_linked_lists.py
index e69de29..4709fd6 100644
--- a/pydatastructs/linear_data_structures/tests/test_linked_lists.py
+++ b/pydatastructs/linear_data_structures/tests/test_linked_lists.py
@@ -0,0 +1,37 @@
+from pydatastructs.linear_data_structures import DoublyLinkedList
+from pydatastructs.utils.raises_util import raises
+import copy, random
+
+def test_DoublyLinkedList():
+    random.seed(1000)
+    dll = DoublyLinkedList()
+    assert raises(IndexError, lambda: dll[2])
+    dll.append_left(5)
+    dll.append(1)
+    dll.append_left(2)
+    dll.append(3)
+    dll.insert_after(dll[1], 4)
+    dll.insert_after(dll[-1], 6)
+    dll.insert_before(dll[0], 1)
+    dll.insert_at(0, 2)
+    dll.insert_at(-1, 9)
+    dll.extract(2)
+    dll.extract(0)
+    dll.extract(-1)
+    dll[-2].data = 0
+    assert str(dll) == "[1, 5, 4, 1, 0, 9]"
+    assert len(dll) == 6
+    assert raises(IndexError, lambda: dll.insert_at(7, None))
+    assert raises(IndexError, lambda: dll.extract(20))
+    dll_copy = copy.deepcopy(dll)
+    for i in range(len(dll)):
+        if i%2 == 0:
+            dll.pop_left()
+        else:
+            dll.pop_right()
+    assert str(dll) == "[]"
+    for _ in range(len(dll_copy)):
+        index = random.randint(0, len(dll_copy) - 1)
+        dll_copy.extract(index)
+    assert str(dll_copy) == "[]"
+    assert raises(ValueError, lambda: dll_copy.extract(1))
diff --git a/pydatastructs/utils/__init__.py b/pydatastructs/utils/__init__.py
index 13c2a05..0923b7d 100644
--- a/pydatastructs/utils/__init__.py
+++ b/pydatastructs/utils/__init__.py
@@ -3,6 +3,6 @@ __all__ = []
 from . import misc_util
 from .misc_util import (
     TreeNode,
-    Node
+    LinkedListNode
 )
 __all__.extend(misc_util.__all__)
diff --git a/pydatastructs/utils/misc_util.py b/pydatastructs/utils/misc_util.py
index 707663e..b55dde7 100644
--- a/pydatastructs/utils/misc_util.py
+++ b/pydatastructs/utils/misc_util.py
@@ -1,8 +1,8 @@
 from __future__ import print_function, division
 
 __all__ = [
-    'TreeNode'
-    'Node'
+    'TreeNode',
+    'LinkedListNode'
 ]
 
 _check_type = lambda a, t: isinstance(a, t)
@@ -42,47 +42,26 @@ class TreeNode(object):
         """
         return str((self.left, self.key, self.data, self.right))
 
-
-# A linked list node 
-class Node: 
-
-    '''
-    Node class for Linked List and Doubly Linked List [ Intended for internal use and not to be imported]
+class LinkedListNode(object):
+    """
+    Represents node in linked lists.
 
     Parameters
     ==========
-    
-    For Doubly Linked List use Default constructor(__init__):
 
-        data: type
-            A valid object type.
-            Should be convertible to string using str() method to use print() method on instance
-
-    For Single Linked List use Alternative constructor(singleLink):
-        data: type
-            A valid object type
-            Should be convertible to string using str() method to use print() method on instance
-    
-    Note
-    ====
+    data
+        Any valid data to be stored in the node.
+    """
 
-    classmethod singleLink has been used for Node class for Single linked list due to non existence of a 
-    previous link between the nodes.
-    '''
+    # __slots__ = ['data']
 
-    __slots__ = ['data', 'next', 'prev']
-    
-    # Constructor to create a new node 
-    def __new__(self, data): 
-        self.data = data 
-        self.next = NoneType
-        self.prev = NoneType
-    #Alternative constructor for Single Linked List
-    @classmethod
-    def singleLink(obj, data):
+    def __new__(cls, data=None, links=['next'], addrs=[None]):
+        obj = object.__new__(cls)
         obj.data = data
-        obj.next = NoneType
+        for link, addr in zip(links, addrs):
+            obj.__setattr__(link, addr)
+        obj.__slots__ = ['data'] + links
         return obj
-    
+
     def __str__(self):
         return str(self.data)
diff --git a/pydatastructs/utils/raises_util.py b/pydatastructs/utils/raises_util.py
index 3d55b55..3a324d3 100644
--- a/pydatastructs/utils/raises_util.py
+++ b/pydatastructs/utils/raises_util.py
@@ -14,3 +14,4 @@ def raises(exception, code):
     """
     with pytest.raises(exception):
         code()
+    return True

Apply the above diff on your branch i.e., linked-list branch and push the changes here.
Use the txt file below if the above diff gives errors but make sure rename it with diff extension. Use git apply <file_name> to apply the diff.
pydatastructs_diff.txt

Some feedbacks,

  1. Please raise the correct type of errors like, IndexError should be raised if the index is going out of range and not ValueError.
  2. Please follow the numpydoc format in your documentation. Follow the coding style thoroughly.
  3. Most importantly, always test your code on corner cases. Think about test cases which can fail your code.
  4. Never repeat yourself in your code. Try to think how a function can be reused somewhere else in your class.

Thanks for the contribution. The test should pass once you push the changes here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants