grscheller.datastructures.nodes

Nodes for Graphs

Node classes used with graph-like data structures. API designed to be used by other data structures which contain these data structures.

  1# Copyright 2023-2024 Geoffrey R. Scheller
  2#
  3# Licensed under the Apache License, Version 2.0 (the "License");
  4# you may not use this file except in compliance with the License.
  5# You may obtain a copy of the License at
  6#
  7#     http://www.apache.org/licenses/LICENSE-2.0
  8#
  9# Unless required by applicable law or agreed to in writing, software
 10# distributed under the License is distributed on an "AS IS" BASIS,
 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12# See the License for the specific language governing permissions and
 13# limitations under the License.
 14
 15"""
 16### Nodes for Graphs
 17
 18Node classes used with graph-like data structures. API designed to be used by
 19other data structures which contain these data structures.
 20
 21"""
 22from __future__ import annotations
 23from typing import Callable, cast, Iterator, Optional
 24from grscheller.fp.err_handling import MB
 25
 26__all__ = ['SL_Node', 'DL_Node', 'Tree_Node']
 27
 28class SL_Node[D]():
 29    """
 30    Data node for rearward Pointing (tip-to-root) singularly linked graphs.
 31
 32    * for mutable and immutable linear data structures
 33    * designed so multiple instances can safely share the same data
 34    * this type of node always contain data and optionally a previous Node
 35    * nodes point towards a unique "root node" with no predecessor
 36      * in a Boolean context return false only if only at a root
 37      * multiple root nodes can exist
 38      * empty data structures can be "re-rooted"
 39    * two nodes compare as equal if
 40      * both their previous Nodes are the same
 41      * their data compares as equal
 42    * more than one node can point to the same node forming bush like graphs
 43
 44    """
 45    __slots__ = '_data', '_prev'
 46
 47    def __init__(self, data: D, prev: MB[SL_Node[D]]) -> None:
 48        self._data = data
 49        self._prev = prev
 50
 51    def __iter__(self) -> Iterator[D]:
 52        node = self
 53        while node:
 54            yield node._data
 55            node = node._prev.get()
 56        yield node._data
 57
 58    def __bool__(self) -> bool:
 59        return self._prev != MB()
 60
 61    def data_eq(self, other: SL_Node[D]) -> bool:
 62        if self._data is other._data:
 63            return True
 64        elif self._data == other._data:
 65            return True
 66        else:
 67            return False
 68
 69    def __eq__(self, other: object) -> bool:
 70        if not isinstance(other, type(self)):
 71            return False
 72
 73        if self._prev is not other._prev:
 74            return False
 75        else:
 76            return self.data_eq(other)
 77
 78    def get_data(self) -> D:
 79        return self._data
 80
 81    def fold[T](self,  f: Callable[[T, D], T], init: Optional[T] = None) -> T:
 82        """Reduce data across linked nodes
 83
 84        * with a function and an optional starting value
 85        * reduces in natural LIFO order
 86          * from self to the root
 87
 88        """
 89        if init is None:
 90            acc: T = cast(T, self._data)
 91            node = self._prev.get()
 92        else:
 93            acc = init
 94            node = self
 95
 96        while node:
 97            acc = f(acc, node._data)
 98            node = node._prev.get()
 99        acc = f(acc, node._data)
100        return acc
101
102    def pop2(self) -> tuple[D, MB[SL_Node[D]]]:
103        """Return the *head* and, if it exists, the top node of the *tail*."""
104        return self._data, self._prev
105
106    def push_data(self, data: D) -> SL_Node[D]:
107        """Push data onto the stack and return a new node containing the data"""
108        return SL_Node(data, MB(self))
109
110class DL_Node[D]():
111    """
112    #### Doubly Linked Node
113
114    Doubly linked nodes for graph-like data structures.
115
116    * this type of node always contain data, even if that data is None
117      * in a Boolean context return true if both left and right nodes exist
118    * not __iter__ method provided, too application specific
119    * doubly link lists possible
120    * circular graphs are possible
121    * simple recursive binary trees possible
122
123    """
124    __slots__ = '_left', '_data', '_right'
125
126    def __init__(self, left: MB[DL_Node[D]], data: D, right: MB[DL_Node[D]]):
127        self._left = left
128        self._data = data
129        self._right = right
130
131    def __bool__(self) -> bool:
132        if self._left == MB() or self._right == MB():
133            return False
134        return True
135
136    def __eq__(self, other: object) -> bool:
137        if not isinstance(other, type(self)):
138            return False
139
140        if self._left is not other._left:
141            return False
142        if self._right is not other._right:
143            return False
144        if self._data is other._data:
145            return True
146        elif self._data == other._data:
147            return True
148
149        return False
150
151    def has_left(self) -> bool:
152        return self._left != MB()
153
154    def has_right(self) -> bool:
155        return self._right != MB()
156
157class Tree_Node[D, M]():
158    """Binary Tree Node with metadata.
159
160    Nodes useful for binary trees
161
162    * this type of node always contain data, even if that data is None
163    * in a Boolean context return true if not at the top of the tree
164    * potential uses of metadata can be for re-balancing or repeat counts
165    """
166    __slots__ = '_data', '_left', '_right', '_up'
167
168    def __init__(self, data: D,
169                 up: MB[Tree_Node[D,M]],
170                 left: MB[Tree_Node[D,M]],
171                 right: MB[Tree_Node[D,M]],
172                 meta: tuple[M, ...] = ()):
173        self._data = data
174        self._up = up
175        self._left = left
176        self._right = right
177
178    def __bool__(self) -> bool:
179        if self._up == MB():
180            return False
181        else:
182            return True
183
184    def is_top(self) -> bool:
185        return self._up == MB()
class SL_Node(typing.Generic[D]):
 29class SL_Node[D]():
 30    """
 31    Data node for rearward Pointing (tip-to-root) singularly linked graphs.
 32
 33    * for mutable and immutable linear data structures
 34    * designed so multiple instances can safely share the same data
 35    * this type of node always contain data and optionally a previous Node
 36    * nodes point towards a unique "root node" with no predecessor
 37      * in a Boolean context return false only if only at a root
 38      * multiple root nodes can exist
 39      * empty data structures can be "re-rooted"
 40    * two nodes compare as equal if
 41      * both their previous Nodes are the same
 42      * their data compares as equal
 43    * more than one node can point to the same node forming bush like graphs
 44
 45    """
 46    __slots__ = '_data', '_prev'
 47
 48    def __init__(self, data: D, prev: MB[SL_Node[D]]) -> None:
 49        self._data = data
 50        self._prev = prev
 51
 52    def __iter__(self) -> Iterator[D]:
 53        node = self
 54        while node:
 55            yield node._data
 56            node = node._prev.get()
 57        yield node._data
 58
 59    def __bool__(self) -> bool:
 60        return self._prev != MB()
 61
 62    def data_eq(self, other: SL_Node[D]) -> bool:
 63        if self._data is other._data:
 64            return True
 65        elif self._data == other._data:
 66            return True
 67        else:
 68            return False
 69
 70    def __eq__(self, other: object) -> bool:
 71        if not isinstance(other, type(self)):
 72            return False
 73
 74        if self._prev is not other._prev:
 75            return False
 76        else:
 77            return self.data_eq(other)
 78
 79    def get_data(self) -> D:
 80        return self._data
 81
 82    def fold[T](self,  f: Callable[[T, D], T], init: Optional[T] = None) -> T:
 83        """Reduce data across linked nodes
 84
 85        * with a function and an optional starting value
 86        * reduces in natural LIFO order
 87          * from self to the root
 88
 89        """
 90        if init is None:
 91            acc: T = cast(T, self._data)
 92            node = self._prev.get()
 93        else:
 94            acc = init
 95            node = self
 96
 97        while node:
 98            acc = f(acc, node._data)
 99            node = node._prev.get()
100        acc = f(acc, node._data)
101        return acc
102
103    def pop2(self) -> tuple[D, MB[SL_Node[D]]]:
104        """Return the *head* and, if it exists, the top node of the *tail*."""
105        return self._data, self._prev
106
107    def push_data(self, data: D) -> SL_Node[D]:
108        """Push data onto the stack and return a new node containing the data"""
109        return SL_Node(data, MB(self))

Data node for rearward Pointing (tip-to-root) singularly linked graphs.

  • for mutable and immutable linear data structures
  • designed so multiple instances can safely share the same data
  • this type of node always contain data and optionally a previous Node
  • nodes point towards a unique "root node" with no predecessor
    • in a Boolean context return false only if only at a root
    • multiple root nodes can exist
    • empty data structures can be "re-rooted"
  • two nodes compare as equal if
    • both their previous Nodes are the same
    • their data compares as equal
  • more than one node can point to the same node forming bush like graphs
SL_Node(data: 'D', prev: 'MB[SL_Node[D]]')
48    def __init__(self, data: D, prev: MB[SL_Node[D]]) -> None:
49        self._data = data
50        self._prev = prev
def data_eq(self, other: 'SL_Node[D]') -> bool:
62    def data_eq(self, other: SL_Node[D]) -> bool:
63        if self._data is other._data:
64            return True
65        elif self._data == other._data:
66            return True
67        else:
68            return False
def get_data(self) -> 'D':
79    def get_data(self) -> D:
80        return self._data
def fold(self, f: 'Callable[[T, D], T]', init: 'Optional[T]' = None) -> 'T':
 82    def fold[T](self,  f: Callable[[T, D], T], init: Optional[T] = None) -> T:
 83        """Reduce data across linked nodes
 84
 85        * with a function and an optional starting value
 86        * reduces in natural LIFO order
 87          * from self to the root
 88
 89        """
 90        if init is None:
 91            acc: T = cast(T, self._data)
 92            node = self._prev.get()
 93        else:
 94            acc = init
 95            node = self
 96
 97        while node:
 98            acc = f(acc, node._data)
 99            node = node._prev.get()
100        acc = f(acc, node._data)
101        return acc

Reduce data across linked nodes

  • with a function and an optional starting value
  • reduces in natural LIFO order
    • from self to the root
def pop2(self) -> 'tuple[D, MB[SL_Node[D]]]':
103    def pop2(self) -> tuple[D, MB[SL_Node[D]]]:
104        """Return the *head* and, if it exists, the top node of the *tail*."""
105        return self._data, self._prev

Return the head and, if it exists, the top node of the tail.

def push_data(self, data: 'D') -> 'SL_Node[D]':
107    def push_data(self, data: D) -> SL_Node[D]:
108        """Push data onto the stack and return a new node containing the data"""
109        return SL_Node(data, MB(self))

Push data onto the stack and return a new node containing the data

class DL_Node(typing.Generic[D]):
111class DL_Node[D]():
112    """
113    #### Doubly Linked Node
114
115    Doubly linked nodes for graph-like data structures.
116
117    * this type of node always contain data, even if that data is None
118      * in a Boolean context return true if both left and right nodes exist
119    * not __iter__ method provided, too application specific
120    * doubly link lists possible
121    * circular graphs are possible
122    * simple recursive binary trees possible
123
124    """
125    __slots__ = '_left', '_data', '_right'
126
127    def __init__(self, left: MB[DL_Node[D]], data: D, right: MB[DL_Node[D]]):
128        self._left = left
129        self._data = data
130        self._right = right
131
132    def __bool__(self) -> bool:
133        if self._left == MB() or self._right == MB():
134            return False
135        return True
136
137    def __eq__(self, other: object) -> bool:
138        if not isinstance(other, type(self)):
139            return False
140
141        if self._left is not other._left:
142            return False
143        if self._right is not other._right:
144            return False
145        if self._data is other._data:
146            return True
147        elif self._data == other._data:
148            return True
149
150        return False
151
152    def has_left(self) -> bool:
153        return self._left != MB()
154
155    def has_right(self) -> bool:
156        return self._right != MB()

Doubly Linked Node

Doubly linked nodes for graph-like data structures.

  • this type of node always contain data, even if that data is None
    • in a Boolean context return true if both left and right nodes exist
  • not __iter__ method provided, too application specific
  • doubly link lists possible
  • circular graphs are possible
  • simple recursive binary trees possible
DL_Node(left: 'MB[DL_Node[D]]', data: 'D', right: 'MB[DL_Node[D]]')
127    def __init__(self, left: MB[DL_Node[D]], data: D, right: MB[DL_Node[D]]):
128        self._left = left
129        self._data = data
130        self._right = right
def has_left(self) -> bool:
152    def has_left(self) -> bool:
153        return self._left != MB()
def has_right(self) -> bool:
155    def has_right(self) -> bool:
156        return self._right != MB()
class Tree_Node(typing.Generic[D, M]):
158class Tree_Node[D, M]():
159    """Binary Tree Node with metadata.
160
161    Nodes useful for binary trees
162
163    * this type of node always contain data, even if that data is None
164    * in a Boolean context return true if not at the top of the tree
165    * potential uses of metadata can be for re-balancing or repeat counts
166    """
167    __slots__ = '_data', '_left', '_right', '_up'
168
169    def __init__(self, data: D,
170                 up: MB[Tree_Node[D,M]],
171                 left: MB[Tree_Node[D,M]],
172                 right: MB[Tree_Node[D,M]],
173                 meta: tuple[M, ...] = ()):
174        self._data = data
175        self._up = up
176        self._left = left
177        self._right = right
178
179    def __bool__(self) -> bool:
180        if self._up == MB():
181            return False
182        else:
183            return True
184
185    def is_top(self) -> bool:
186        return self._up == MB()

Binary Tree Node with metadata.

Nodes useful for binary trees

  • this type of node always contain data, even if that data is None
  • in a Boolean context return true if not at the top of the tree
  • potential uses of metadata can be for re-balancing or repeat counts
Tree_Node( data: 'D', up: 'MB[Tree_Node[D, M]]', left: 'MB[Tree_Node[D, M]]', right: 'MB[Tree_Node[D, M]]', meta: 'tuple[M, ...]' = ())
169    def __init__(self, data: D,
170                 up: MB[Tree_Node[D,M]],
171                 left: MB[Tree_Node[D,M]],
172                 right: MB[Tree_Node[D,M]],
173                 meta: tuple[M, ...] = ()):
174        self._data = data
175        self._up = up
176        self._left = left
177        self._right = right
def is_top(self) -> bool:
185    def is_top(self) -> bool:
186        return self._up == MB()