grscheller.datastructures.splitends.se

With use I am finding this data structure needs some sort of supporting infrastructure. Hence I split the original splitend module out to be its own subpackage.

SplitEnd Stack type and SE factory function

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

Push data onto the top of the SplitEnd.

def push(self, *ds: 'D') -> None:
115    def push(self, *ds: D) -> None:
116        """Push data onto the top of the SplitEnd."""
117        for d in ds:
118            node = SL_Node(d, self._tip)
119            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':
121    def pop(self, default: D|None = None, /) -> D|Never:
122        """Pop data off of the top of the SplitEnd.
123
124        * raises ValueError if
125          * popping from an empty SplitEnd
126          * and no default value was given
127
128        """
129        if self._count == 0:
130            if default is None:
131                raise ValueError('SE: Popping from an empty SplitEnd')
132            else:
133                return default
134
135        data, self._tip, self._count = self._tip.get().pop2() + (self._count-1,)
136        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':
138    def peak(self, default: D|None = None, /) -> D:
139        """Return the data at the top of the SplitEnd.
140
141        * does not consume the data
142        * raises ValueError if peaking at an empty SplitEnd
143
144        """
145        if self._count == 0:
146            if default is None:
147                raise ValueError('SE: Popping from an empty SplitEnd')
148            else:
149                return default
150
151        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]':
153    def copy(self) -> SplitEnd[D]:
154        """Return a copy of the SplitEnd.
155
156        * O(1) space & time complexity.
157        * returns a new instance
158
159        """
160        se: SplitEnd[D] = SE()
161        se._tip, se._count = self._tip, self._count
162        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':
164    def fold[T](self, f:Callable[[T, D], T], init: T|None = None, /) -> T|Never:
165        """Reduce with a function.
166
167        * folds in natural LIFO Order
168
169        """
170        if self._tip != MB():
171            return self._tip.get().fold(f, init)
172        elif init is not None:
173            return init
174        else:
175            msg = 'SE: Folding empty SplitEnd but no initial value supplied'
176            raise ValueError(msg)

Reduce with a function.

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