grscheller.datastructures.splitends.se

SplitEnd stack type data structure

SplitEnd Stack type and factory function:

  • class SplitEndE: Singularly linked stack with shareable data nodes
  • function SE: Factory function
    • takes variable number of args
    • returns a SpltEnd instance
  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"""### SplitEnd stack type data structure
 16
 17#### SplitEnd Stack type and factory function:
 18
 19* class **SplitEndE:** Singularly linked stack with shareable data nodes
 20* function **SE:** Factory function
 21  * takes variable number of args
 22  * returns a SpltEnd instance
 23
 24"""
 25from __future__ import annotations
 26
 27from collections.abc import Callable, Iterable, Iterator
 28from typing import cast, Never
 29from ..nodes import SL_Node
 30from grscheller.fp.err_handling import MB
 31
 32__all__ = [ 'SplitEnd', 'SE' ]
 33
 34class SplitEnd[D]():
 35    """Class SplitEnd
 36
 37    LIFO stacks which can safely share immutable data between themselves.
 38
 39    * each SplitEnd is a very simple stateful (mutable) LIFO stack
 40      * top of the stack is the "top"
 41    * data can be pushed and popped to the stack
 42    * different mutable split ends can safely share the same "tail"
 43    * each SplitEnd sees itself as a singularly linked list
 44    * bush-like datastructures can be formed using multiple SplitEnds
 45    * len() returns the number of elements on the SplitEnd stack
 46    * in boolean context, return true if split end is not empty
 47
 48    """
 49    __slots__ = '_count', '_tip'
 50
 51    def __init__(self, *dss: Iterable[D]) -> None:
 52        if length:=len(dss) < 2:
 53            self._tip: MB[SL_Node[D]] = MB()
 54            self._count: int = 0
 55            if length == 1:
 56                self.pushI(*dss)
 57        else:
 58            msg1 = 'SplitEnd: expected at most 1 '
 59            msg2 = f'iterable argument, got {length}.'
 60            raise TypeError(msg1+msg2)
 61
 62    def __iter__(self) -> Iterator[D]:
 63        if self._tip == MB():
 64            empty: tuple[D, ...] = ()
 65            return iter(empty)
 66        return iter(self._tip.get())
 67
 68    def __reversed__(self) -> Iterator[D]:
 69        return reversed(list(self))
 70
 71    def __bool__(self) -> bool:
 72        # Returns true if not a root node
 73        return bool(self._tip)
 74
 75    def __len__(self) -> int:
 76        return self._count
 77
 78    def __repr__(self) -> str:
 79        return 'SE(' + ', '.join(map(repr, reversed(self))) + ')'
 80
 81    def __str__(self) -> str:
 82        return ('>< ' + ' -> '.join(map(str, self)) + ' ||')
 83
 84    def __eq__(self, other: object, /) -> bool:
 85        if not isinstance(other, type(self)):
 86            return False
 87
 88        if self._count != other._count:
 89            return False
 90        if self._count == 0:
 91            return True
 92
 93        left = self._tip.get()
 94        right = other._tip.get()
 95        for _ in range(self._count):
 96            if left is right:
 97                return True
 98            if not left.data_eq(right):
 99                return False
100            if left:
101                left = left._prev.get()
102                right = right._prev.get()
103
104        return True
105
106    def pushI(self, ds: Iterable[D], /) -> None:
107        """Push data onto the top of the SplitEnd."""
108        for d in ds:
109            node = SL_Node(d, self._tip)
110            self._tip, self._count = MB(node), self._count+1
111
112    def push(self, *ds: D) -> None:
113        """Push data onto the top of the SplitEnd."""
114        for d in ds:
115            node = SL_Node(d, self._tip)
116            self._tip, self._count = MB(node), self._count+1
117
118    def pop(self, default: D|None = None, /) -> D|Never:
119        """Pop data off of the top of the SplitEnd.
120
121        * raises ValueError if
122          * popping from an empty SplitEnd
123          * and no default value was given
124
125        """
126        if self._count == 0:
127            if default is None:
128                raise ValueError('SE: Popping from an empty SplitEnd')
129            else:
130                return default
131
132        data, self._tip, self._count = self._tip.get().pop2() + (self._count-1,)
133        return data
134
135    def peak(self, default: D|None = None, /) -> D:
136        """Return the data at the top of the SplitEnd.
137
138        * does not consume the data
139        * raises ValueError if peaking at an empty SplitEnd
140
141        """
142        if self._count == 0:
143            if default is None:
144                raise ValueError('SE: Popping from an empty SplitEnd')
145            else:
146                return default
147
148        return self._tip.get().get_data()
149
150    def copy(self) -> SplitEnd[D]:
151        """Return a copy of the SplitEnd.
152
153        * O(1) space & time complexity.
154        * returns a new instance
155
156        """
157        se: SplitEnd[D] = SE()
158        se._tip, se._count = self._tip, self._count
159        return se
160
161    def fold[T](self, f:Callable[[T, D], T], init: T|None = None, /) -> T|Never:
162        """Reduce with a function.
163
164        * folds in natural LIFO Order
165
166        """
167        if self._tip != MB():
168            return self._tip.get().fold(f, init)
169        elif init is not None:
170            return init
171        else:
172            msg = 'SE: Folding empty SplitEnd but no initial value supplied'
173            raise ValueError(msg)
174
175def SE[D](*ds: D) -> SplitEnd[D]:
176    return SplitEnd(ds)
class SplitEnd(typing.Generic[D]):
 35class SplitEnd[D]():
 36    """Class SplitEnd
 37
 38    LIFO stacks which can safely share immutable data between themselves.
 39
 40    * each SplitEnd is a very simple stateful (mutable) LIFO stack
 41      * top of the stack is the "top"
 42    * data can be pushed and popped to the stack
 43    * different mutable split ends can safely share the same "tail"
 44    * each SplitEnd sees itself as a singularly linked list
 45    * bush-like datastructures can be formed using multiple SplitEnds
 46    * len() returns the number of elements on the SplitEnd stack
 47    * in boolean context, return true if split end is not empty
 48
 49    """
 50    __slots__ = '_count', '_tip'
 51
 52    def __init__(self, *dss: Iterable[D]) -> None:
 53        if length:=len(dss) < 2:
 54            self._tip: MB[SL_Node[D]] = MB()
 55            self._count: int = 0
 56            if length == 1:
 57                self.pushI(*dss)
 58        else:
 59            msg1 = 'SplitEnd: expected at most 1 '
 60            msg2 = f'iterable argument, got {length}.'
 61            raise TypeError(msg1+msg2)
 62
 63    def __iter__(self) -> Iterator[D]:
 64        if self._tip == MB():
 65            empty: tuple[D, ...] = ()
 66            return iter(empty)
 67        return iter(self._tip.get())
 68
 69    def __reversed__(self) -> Iterator[D]:
 70        return reversed(list(self))
 71
 72    def __bool__(self) -> bool:
 73        # Returns true if not a root node
 74        return bool(self._tip)
 75
 76    def __len__(self) -> int:
 77        return self._count
 78
 79    def __repr__(self) -> str:
 80        return 'SE(' + ', '.join(map(repr, reversed(self))) + ')'
 81
 82    def __str__(self) -> str:
 83        return ('>< ' + ' -> '.join(map(str, self)) + ' ||')
 84
 85    def __eq__(self, other: object, /) -> bool:
 86        if not isinstance(other, type(self)):
 87            return False
 88
 89        if self._count != other._count:
 90            return False
 91        if self._count == 0:
 92            return True
 93
 94        left = self._tip.get()
 95        right = other._tip.get()
 96        for _ in range(self._count):
 97            if left is right:
 98                return True
 99            if not left.data_eq(right):
100                return False
101            if left:
102                left = left._prev.get()
103                right = right._prev.get()
104
105        return True
106
107    def pushI(self, ds: Iterable[D], /) -> None:
108        """Push data onto the top of the SplitEnd."""
109        for d in ds:
110            node = SL_Node(d, self._tip)
111            self._tip, self._count = MB(node), self._count+1
112
113    def push(self, *ds: D) -> None:
114        """Push data onto the top of the SplitEnd."""
115        for d in ds:
116            node = SL_Node(d, self._tip)
117            self._tip, self._count = MB(node), self._count+1
118
119    def pop(self, default: D|None = None, /) -> D|Never:
120        """Pop data off of the top of the SplitEnd.
121
122        * raises ValueError if
123          * popping from an empty SplitEnd
124          * and no default value was given
125
126        """
127        if self._count == 0:
128            if default is None:
129                raise ValueError('SE: Popping from an empty SplitEnd')
130            else:
131                return default
132
133        data, self._tip, self._count = self._tip.get().pop2() + (self._count-1,)
134        return data
135
136    def peak(self, default: D|None = None, /) -> D:
137        """Return the data at the top of the SplitEnd.
138
139        * does not consume the data
140        * raises ValueError if peaking at an empty SplitEnd
141
142        """
143        if self._count == 0:
144            if default is None:
145                raise ValueError('SE: Popping from an empty SplitEnd')
146            else:
147                return default
148
149        return self._tip.get().get_data()
150
151    def copy(self) -> SplitEnd[D]:
152        """Return a copy of the SplitEnd.
153
154        * O(1) space & time complexity.
155        * returns a new instance
156
157        """
158        se: SplitEnd[D] = SE()
159        se._tip, se._count = self._tip, self._count
160        return se
161
162    def fold[T](self, f:Callable[[T, D], T], init: T|None = None, /) -> T|Never:
163        """Reduce with a function.
164
165        * folds in natural LIFO Order
166
167        """
168        if self._tip != MB():
169            return self._tip.get().fold(f, init)
170        elif init is not None:
171            return init
172        else:
173            msg = 'SE: Folding empty SplitEnd but no initial value supplied'
174            raise ValueError(msg)

Class SplitEnd

LIFO stacks which can safely share immutable data between themselves.

  • each SplitEnd is a very simple stateful (mutable) LIFO stack
    • top of the stack is the "top"
  • data can be pushed and popped to the stack
  • different mutable split ends can safely share the same "tail"
  • each SplitEnd sees itself as a singularly linked list
  • bush-like datastructures can be formed using multiple SplitEnds
  • len() returns the number of elements on the SplitEnd stack
  • in boolean context, return true if split end is not empty
SplitEnd(*dss: 'Iterable[D]')
52    def __init__(self, *dss: Iterable[D]) -> None:
53        if length:=len(dss) < 2:
54            self._tip: MB[SL_Node[D]] = MB()
55            self._count: int = 0
56            if length == 1:
57                self.pushI(*dss)
58        else:
59            msg1 = 'SplitEnd: expected at most 1 '
60            msg2 = f'iterable argument, got {length}.'
61            raise TypeError(msg1+msg2)
def pushI(self, ds: 'Iterable[D]', /) -> None:
107    def pushI(self, ds: Iterable[D], /) -> None:
108        """Push data onto the top of the SplitEnd."""
109        for d in ds:
110            node = SL_Node(d, self._tip)
111            self._tip, self._count = MB(node), self._count+1

Push data onto the top of the SplitEnd.

def push(self, *ds: 'D') -> None:
113    def push(self, *ds: D) -> None:
114        """Push data onto the top of the SplitEnd."""
115        for d in ds:
116            node = SL_Node(d, self._tip)
117            self._tip, self._count = MB(node), self._count+1

Push data onto the top of the SplitEnd.

def pop(self, default: 'D | None' = None, /) -> 'D | Never':
119    def pop(self, default: D|None = None, /) -> D|Never:
120        """Pop data off of the top of the SplitEnd.
121
122        * raises ValueError if
123          * popping from an empty SplitEnd
124          * and no default value was given
125
126        """
127        if self._count == 0:
128            if default is None:
129                raise ValueError('SE: Popping from an empty SplitEnd')
130            else:
131                return default
132
133        data, self._tip, self._count = self._tip.get().pop2() + (self._count-1,)
134        return data

Pop data off of the top of the SplitEnd.

  • raises ValueError if
    • popping from an empty SplitEnd
    • and no default value was given
def peak(self, default: 'D | None' = None, /) -> 'D':
136    def peak(self, default: D|None = None, /) -> D:
137        """Return the data at the top of the SplitEnd.
138
139        * does not consume the data
140        * raises ValueError if peaking at an empty SplitEnd
141
142        """
143        if self._count == 0:
144            if default is None:
145                raise ValueError('SE: Popping from an empty SplitEnd')
146            else:
147                return default
148
149        return self._tip.get().get_data()

Return the data at the top of the SplitEnd.

  • does not consume the data
  • raises ValueError if peaking at an empty SplitEnd
def copy(self) -> 'SplitEnd[D]':
151    def copy(self) -> SplitEnd[D]:
152        """Return a copy of the SplitEnd.
153
154        * O(1) space & time complexity.
155        * returns a new instance
156
157        """
158        se: SplitEnd[D] = SE()
159        se._tip, se._count = self._tip, self._count
160        return se

Return a copy of the SplitEnd.

  • O(1) space & time complexity.
  • returns a new instance
def fold( self, f: 'Callable[[T, D], T]', init: 'T | None' = None, /) -> 'T | Never':
162    def fold[T](self, f:Callable[[T, D], T], init: T|None = None, /) -> T|Never:
163        """Reduce with a function.
164
165        * folds in natural LIFO Order
166
167        """
168        if self._tip != MB():
169            return self._tip.get().fold(f, init)
170        elif init is not None:
171            return init
172        else:
173            msg = 'SE: Folding empty SplitEnd but no initial value supplied'
174            raise ValueError(msg)

Reduce with a function.

  • folds in natural LIFO Order
def SE(*ds: 'D') -> 'SplitEnd[D]':
176def SE[D](*ds: D) -> SplitEnd[D]:
177    return SplitEnd(ds)