triordinal-dlist 2.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: triordinal-dlist
3
+ Version: 2.0.0
4
+ Summary: DList (part of Triordinal): doubly linked list with rational positions and an O(1) precedence test, shared by triordinal-dtree and triordinal-dgraph
5
+ Author-email: Enis Olgac <enis@digraphs.info>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/EnisOlgac/Triordinal
8
+ Project-URL: Changelog, https://github.com/EnisOlgac/Triordinal/blob/main/CHANGELOG.md
9
+ Keywords: order maintenance,linked list,rational positions,precedence,data structure
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # DList for Python (triordinal-dlist)
21
+
22
+ The list shared by [Triordinal](https://github.com/EnisOlgac/Triordinal)'s DTree and DGraph: a doubly linked list of names with rational positions, so that "does a come before b?" is a single comparison, O(1).
23
+
24
+ ```
25
+ pip install triordinal-dlist
26
+ ```
27
+
28
+ ```python
29
+ from triordinal.dlist import DList
30
+
31
+ q = DList("Q")
32
+ q.insert_after("a", "[") # "[" is the start marker (SOURCE)
33
+ q.insert_after("b", "a")
34
+ q.precedes("a", "b") # True
35
+ ```
36
+
37
+ A new element gets the mediant (a+c)/(b+d) of its neighbors a/b and c/d, so normally no other element moves. Only when a denominator would exceed 64 bits is the smallest surrounding region with enough room renumbered; larger regions must be sparser before they are accepted (as in Bender et al., ESA 2002). Rational positions were Enis Olgac's idea.
38
+
39
+ Most users install `triordinal-dtree` (or later `triordinal-dgraph`), which pulls this package in. MIT License.
@@ -0,0 +1,20 @@
1
+ # DList for Python (triordinal-dlist)
2
+
3
+ The list shared by [Triordinal](https://github.com/EnisOlgac/Triordinal)'s DTree and DGraph: a doubly linked list of names with rational positions, so that "does a come before b?" is a single comparison, O(1).
4
+
5
+ ```
6
+ pip install triordinal-dlist
7
+ ```
8
+
9
+ ```python
10
+ from triordinal.dlist import DList
11
+
12
+ q = DList("Q")
13
+ q.insert_after("a", "[") # "[" is the start marker (SOURCE)
14
+ q.insert_after("b", "a")
15
+ q.precedes("a", "b") # True
16
+ ```
17
+
18
+ A new element gets the mediant (a+c)/(b+d) of its neighbors a/b and c/d, so normally no other element moves. Only when a denominator would exceed 64 bits is the smallest surrounding region with enough room renumbered; larger regions must be sparser before they are accepted (as in Bender et al., ESA 2002). Rational positions were Enis Olgac's idea.
19
+
20
+ Most users install `triordinal-dtree` (or later `triordinal-dgraph`), which pulls this package in. MIT License.
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "triordinal-dlist"
7
+ version = "2.0.0"
8
+ description = "DList (part of Triordinal): doubly linked list with rational positions and an O(1) precedence test, shared by triordinal-dtree and triordinal-dgraph"
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Enis Olgac", email = "enis@digraphs.info" }]
13
+ keywords = ["order maintenance", "linked list", "rational positions", "precedence", "data structure"]
14
+ classifiers = [
15
+ "Development Status :: 5 - Production/Stable",
16
+ "Intended Audience :: Science/Research",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent",
20
+ "Programming Language :: Python :: 3",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+
24
+ [project.urls]
25
+ Homepage = "https://github.com/EnisOlgac/Triordinal"
26
+ Changelog = "https://github.com/EnisOlgac/Triordinal/blob/main/CHANGELOG.md"
27
+
28
+ [tool.setuptools.packages.find]
29
+ include = ["triordinal.dlist*"]
30
+ namespaces = true
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,84 @@
1
+ """Tests for DList on its own: order, precedence and renumbering against a plain Python list."""
2
+
3
+ import os
4
+ import random
5
+ import sys
6
+ import unittest
7
+
8
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
9
+
10
+ from triordinal.dlist import SINK, SOURCE, DList # noqa: E402
11
+
12
+
13
+ class TestDList(unittest.TestCase):
14
+ def check(self, q, ref):
15
+ self.assertEqual(list(q), ref)
16
+ for _ in range(200):
17
+ a, b = random.sample(ref, 2) if len(ref) > 1 else (ref[0], ref[0])
18
+ self.assertEqual(q.precedes(a, b), ref.index(a) < ref.index(b))
19
+
20
+ def test_random_inserts_and_removes(self):
21
+ rnd = random.Random(1)
22
+ q, ref = DList("Q"), []
23
+ for i in range(3000):
24
+ name = f"n{i}"
25
+ if ref and rnd.random() < 0.2:
26
+ victim = rnd.choice(ref)
27
+ q.remove(victim)
28
+ ref.remove(victim)
29
+ continue
30
+ if not ref or rnd.random() < 0.5:
31
+ anchor = rnd.choice([SOURCE] + ref)
32
+ q.insert_after(name, anchor)
33
+ ref.insert(0 if anchor == SOURCE else ref.index(anchor) + 1, name)
34
+ else:
35
+ anchor = rnd.choice(ref + [SINK])
36
+ q.insert_before(name, anchor)
37
+ ref.insert(len(ref) if anchor == SINK else ref.index(anchor), name)
38
+ self.check(q, ref)
39
+
40
+ def test_alternating_worst_case_renumbers_and_keeps_order(self):
41
+ q, ref = DList("Q"), ["a", "b"]
42
+ q.insert_after("a", SOURCE)
43
+ q.insert_after("b", "a")
44
+ left, right = "a", "b"
45
+ for i in range(3000):
46
+ name = f"x{i}"
47
+ q.insert_after(name, left)
48
+ ref.insert(ref.index(left) + 1, name)
49
+ left, right = (name, right) if i % 2 else (left, name)
50
+ self.assertGreater(q.renumber_count, 0)
51
+ self.check(q, ref)
52
+
53
+ def test_markers_cannot_be_inserted_moved_or_removed(self):
54
+ q = DList("Q")
55
+ q.insert_after("a", SOURCE)
56
+ for f in (lambda: q.insert_before(SINK, "a"), lambda: q.insert_after(SOURCE, "a"),
57
+ lambda: q.insert_after(SINK, "a"), lambda: q.insert_before(SOURCE, "a"),
58
+ lambda: q.remove(SOURCE), lambda: q.remove(SINK)):
59
+ with self.assertRaises(ValueError):
60
+ f()
61
+ self.assertEqual(q.to_list(with_markers=True), [SOURCE, "a", SINK])
62
+
63
+ def test_many_end_insertions_then_one_gap(self):
64
+ # the reviewer's pattern: values grow with every insertion at the end, then one gap
65
+ # is filled alternately; Python's integers do not overflow, the order must hold
66
+ q = DList("Q")
67
+ q.insert_after("r", SOURCE)
68
+ prev = None
69
+ for i in range(200_000):
70
+ q.insert_before(f"c{i}", SINK)
71
+ if prev:
72
+ q.remove(prev)
73
+ prev = f"c{i}"
74
+ left, right, ref = "r", prev, ["r", prev]
75
+ for i in range(300):
76
+ name = f"s{i}"
77
+ q.insert_after(name, left)
78
+ ref.insert(ref.index(left) + 1, name)
79
+ left, right = (name, right) if i % 2 else (left, name)
80
+ self.check(q, ref)
81
+
82
+
83
+ if __name__ == "__main__":
84
+ unittest.main()
@@ -0,0 +1,277 @@
1
+ """DList: a doubly linked list with rational positions and an O(1) precedence test.
2
+
3
+ Shared by triordinal-dtree and triordinal-dgraph. Positions are fractions: a new
4
+ element gets the mediant (a+c)/(b+d) of its neighbors a/b and c/d, which always lies
5
+ strictly between them, so normally no other element moves. Rational positions were
6
+ Enis Olgac's idea. Only when a denominator would exceed 64 bits is the smallest
7
+ surrounding region with enough room renumbered with simple fractions k/2**32; larger
8
+ regions must be sparser before they are accepted (as in Bender et al., ESA 2002).
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from fractions import Fraction
14
+ from typing import Dict, Iterable, Iterator, List, Optional
15
+
16
+ SOURCE = "["
17
+ SINK = "]"
18
+
19
+ _Q = 1 << 32 # denominator used when a crowded region is renumbered
20
+ _LIMIT = 64 # renumber when a denominator would exceed this many bits
21
+ _SLACK = 16 # after renumbering, neighbors are at least this many 1/_Q apart
22
+ _GROWTH = 2 # each doubling of the region raises the required spacing by this factor
23
+
24
+
25
+ class _Cell:
26
+ __slots__ = ("name", "prev", "next", "num", "den", "node")
27
+
28
+ def __init__(self, name: str) -> None:
29
+ self.name = name
30
+ self.prev: Optional[_Cell] = None
31
+ self.next: Optional[_Cell] = None
32
+ self.num = 0
33
+ self.den = 1
34
+ self.node: Optional["DNode"] = None # the tree node owning this cell (DTree only)
35
+
36
+
37
+ class DList:
38
+ """Doubly linked list with rational positions (O(1) precedence test).
39
+
40
+ Positions are fractions num/den. A new node gets the mediant
41
+ (a+c)/(b+d) of its neighbors a/b and c/d, which always lies strictly
42
+ between them; SOURCE and SINK act as -1/0 and +1/0. Nothing else moves.
43
+ Only when a denominator would exceed _LIMIT bits is a small surrounding
44
+ region renumbered with simple fractions k/_Q.
45
+ """
46
+
47
+ def __init__(self, name: str) -> None:
48
+ self.name = name
49
+ self._cells: Dict[str, _Cell] = {}
50
+ self.first = self._new(SOURCE)
51
+ self.last = self._new(SINK)
52
+ self.first.next = self.last
53
+ self.last.prev = self.first
54
+ self.first.num, self.first.den = -1, 0 # -infinity
55
+ self.last.num, self.last.den = 1, 0 # +infinity
56
+ # Statistics: how often positions were renumbered and how many moved.
57
+ self.renumber_count = 0
58
+ self.renumber_moved = 0
59
+
60
+ # -- internals --------------------------------------------------------
61
+ def _new(self, name: str) -> _Cell:
62
+ cell = _Cell(name)
63
+ self._cells[name] = cell
64
+ return cell
65
+
66
+ @staticmethod
67
+ def _lt(x: _Cell, y: _Cell) -> bool:
68
+ """x lies strictly left of y, by comparing x.num/x.den with y.num/y.den."""
69
+ if x.den and y.den:
70
+ return x.num * y.den < y.num * x.den
71
+ if x.den == 0 and y.den == 0:
72
+ return x.num < y.num
73
+ return (x.num if x.den == 0 else 0) < (y.num if y.den == 0 else 0)
74
+
75
+ def _link_after(self, cell: _Cell, prev: _Cell) -> None:
76
+ nxt = prev.next
77
+ cell.prev, cell.next = prev, nxt
78
+ prev.next = cell
79
+ nxt.prev = cell
80
+ if prev is self.first and nxt is self.last:
81
+ cell.num, cell.den = 0, 1 # first node of an empty list
82
+ return
83
+ # Mediant of the neighbors. Next to SOURCE (-1/0) or SINK (+1/0) this
84
+ # steps by one whole unit, so a list can grow at either end forever
85
+ # without renumbering (positives to the right, negatives to the left).
86
+ cell.num = prev.num + nxt.num
87
+ cell.den = prev.den + nxt.den
88
+ if cell.den.bit_length() > _LIMIT:
89
+ self._rebalance(cell)
90
+
91
+ def _rebalance(self, cell: _Cell) -> None:
92
+ """Renumber the smallest region around ``cell`` that has room, using fractions k/_Q."""
93
+ q = _Q
94
+ lo = hi = cell
95
+ m, k, need = 1, 1, _SLACK
96
+ while True:
97
+ for _ in range(k):
98
+ if lo.prev is not self.first:
99
+ lo = lo.prev
100
+ m += 1
101
+ if hi.next is not self.last:
102
+ hi = hi.next
103
+ m += 1
104
+ left, right = lo.prev, hi.next
105
+ if left is self.first and right is self.last:
106
+ ks = [(j - m // 2) * q for j in range(m)]
107
+ break
108
+ if left is self.first:
109
+ top = -((-right.num * q) // right.den) # ceil(right * q)
110
+ ks = [top - (m - j) * q for j in range(m)]
111
+ break
112
+ if right is self.last:
113
+ bottom = (left.num * q) // left.den # floor(left * q)
114
+ ks = [bottom + (j + 1) * q for j in range(m)]
115
+ break
116
+ bottom = (left.num * q) // left.den
117
+ top = -((-right.num * q) // right.den)
118
+ room = top - bottom - 1 # integers k with left < k/q < right
119
+ if room >= m * need:
120
+ ks = [bottom + ((j + 1) * (room + 1)) // (m + 1) for j in range(m)]
121
+ break
122
+ # Larger regions must be sparser before they are accepted (as in
123
+ # Bender et al.), so a renumbered region keeps spare room and the
124
+ # next overflow nearby can again be handled locally.
125
+ k *= 2
126
+ need *= _GROWTH
127
+ c = lo
128
+ for kv in ks:
129
+ c.num, c.den = kv, q
130
+ c = c.next
131
+ self.renumber_count += 1
132
+ self.renumber_moved += m
133
+
134
+ def _unlink(self, cell: _Cell) -> None:
135
+ cell.prev.next = cell.next
136
+ cell.next.prev = cell.prev
137
+ cell.prev = cell.next = None
138
+
139
+ # -- public -----------------------------------------------------------
140
+ def __contains__(self, name: str) -> bool:
141
+ return name in self._cells
142
+
143
+ # Names follow List.cs: Source/Sink, First/Last, Prev/Next, Index,
144
+ # IsAllocated, IsLeftOf ... IsSelfOrRightOf, InsAfter/InsBefore, IsSameAs.
145
+
146
+ @property
147
+ def source(self) -> str:
148
+ return self.first.name
149
+
150
+ @property
151
+ def sink(self) -> str:
152
+ return self.last.name
153
+
154
+ def index(self, name: str):
155
+ """Position of ``name`` as a Fraction (SOURCE/SINK: -inf/+inf), or None if absent (List.cs: Index)."""
156
+ cell = self._cells.get(name)
157
+ if cell is None:
158
+ return None
159
+ if cell.den == 0:
160
+ return float("inf") if cell.num > 0 else float("-inf")
161
+ return Fraction(cell.num, cell.den)
162
+
163
+ label = index
164
+
165
+ def is_allocated(self, name: str) -> bool:
166
+ return name in self._cells
167
+
168
+ def is_not_allocated(self, name: str) -> bool:
169
+ return name not in self._cells
170
+
171
+ def next(self, name: str) -> Optional[str]:
172
+ nxt = self._cells[name].next
173
+ return nxt.name if nxt else None
174
+
175
+ def prev(self, name: str) -> Optional[str]:
176
+ prv = self._cells[name].prev
177
+ return prv.name if prv else None
178
+
179
+ def is_left_of(self, v: str, u: str) -> bool:
180
+ return self._lt(self._cells[v], self._cells[u])
181
+
182
+ def is_self_or_left_of(self, v: str, u: str) -> bool:
183
+ return v == u or self._lt(self._cells[v], self._cells[u])
184
+
185
+ def is_right_of(self, v: str, u: str) -> bool:
186
+ return self._lt(self._cells[u], self._cells[v])
187
+
188
+ def is_self_or_right_of(self, v: str, u: str) -> bool:
189
+ return v == u or self._lt(self._cells[u], self._cells[v])
190
+
191
+ def precedes(self, u: str, v: str) -> bool:
192
+ """Strict order: u lies left of v (same as is_left_of)."""
193
+ x, y = self._cells[u], self._cells[v]
194
+ if x.den and y.den:
195
+ return x.num * y.den < y.num * x.den
196
+ return self._lt(x, y)
197
+
198
+ def _cell_for(self, name: str) -> _Cell:
199
+ """Existing cell (unlinked first, so it is moved) or a new one."""
200
+ cell = self._cells.get(name)
201
+ if cell is None:
202
+ return self._new(name)
203
+ self._unlink(cell)
204
+ return cell
205
+
206
+ def insert_after(self, name: str, anchor: str) -> None:
207
+ """Place ``name`` directly after ``anchor``; moves it if already present (List.cs: InsAfter)."""
208
+ if name in (SOURCE, SINK):
209
+ raise ValueError(f"the markers {SOURCE} and {SINK} cannot be inserted or moved")
210
+ if name == anchor:
211
+ return
212
+ if anchor == SINK:
213
+ return self.insert_before(name, SINK)
214
+ a = self._cells[anchor]
215
+ if a.next is not None and a.next.name == name:
216
+ return
217
+ cell = self._cell_for(name)
218
+ self._link_after(cell, self._cells[anchor])
219
+
220
+ def insert_before(self, name: str, anchor: str) -> None:
221
+ """Place ``name`` directly before ``anchor``; moves it if already present (List.cs: InsBefore)."""
222
+ if name in (SOURCE, SINK):
223
+ raise ValueError(f"the markers {SOURCE} and {SINK} cannot be inserted or moved")
224
+ if name == anchor:
225
+ return
226
+ if anchor == SOURCE:
227
+ return self.insert_after(name, SOURCE)
228
+ a = self._cells[anchor]
229
+ if a.prev is not None and a.prev.name == name:
230
+ return
231
+ cell = self._cell_for(name)
232
+ self._link_after(cell, self._cells[anchor].prev)
233
+
234
+ def is_same_as(self, other: "DList") -> bool:
235
+ """True if both lists hold the same nodes in the same order (List.cs: IsSameAs)."""
236
+ return list(self) == list(other)
237
+
238
+ def remove(self, name: str) -> None:
239
+ if name in (SOURCE, SINK):
240
+ raise ValueError("markers cannot be removed")
241
+ self._unlink(self._cells.pop(name))
242
+
243
+ def cut_block(self, first: str, last: str) -> List[str]:
244
+ """Remove the contiguous block first..last and return its names in order."""
245
+ a, b = self._cells[first], self._cells[last]
246
+ names, cell = [], a
247
+ while True:
248
+ names.append(cell.name)
249
+ if cell is b:
250
+ break
251
+ cell = cell.next
252
+ a.prev.next = b.next
253
+ b.next.prev = a.prev
254
+ for n in names:
255
+ self._cells.pop(n)
256
+ return names
257
+
258
+ def paste_block_after(self, names: List[str], anchor: str) -> None:
259
+ for n in names:
260
+ self.insert_after(n, anchor)
261
+ anchor = n
262
+
263
+ def __iter__(self) -> Iterator[str]:
264
+ cell = self.first.next
265
+ while cell is not self.last:
266
+ yield cell.name
267
+ cell = cell.next
268
+
269
+ def to_list(self, with_markers: bool = False) -> List[str]:
270
+ body = list(self)
271
+ return [SOURCE, *body, SINK] if with_markers else body
272
+
273
+ def __repr__(self) -> str:
274
+ return f"{self.name}: [{', '.join(self)}]"
275
+
276
+ __all__ = ["DList", "SOURCE", "SINK"]
277
+ __version__ = "2.0.0"
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: triordinal-dlist
3
+ Version: 2.0.0
4
+ Summary: DList (part of Triordinal): doubly linked list with rational positions and an O(1) precedence test, shared by triordinal-dtree and triordinal-dgraph
5
+ Author-email: Enis Olgac <enis@digraphs.info>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/EnisOlgac/Triordinal
8
+ Project-URL: Changelog, https://github.com/EnisOlgac/Triordinal/blob/main/CHANGELOG.md
9
+ Keywords: order maintenance,linked list,rational positions,precedence,data structure
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # DList for Python (triordinal-dlist)
21
+
22
+ The list shared by [Triordinal](https://github.com/EnisOlgac/Triordinal)'s DTree and DGraph: a doubly linked list of names with rational positions, so that "does a come before b?" is a single comparison, O(1).
23
+
24
+ ```
25
+ pip install triordinal-dlist
26
+ ```
27
+
28
+ ```python
29
+ from triordinal.dlist import DList
30
+
31
+ q = DList("Q")
32
+ q.insert_after("a", "[") # "[" is the start marker (SOURCE)
33
+ q.insert_after("b", "a")
34
+ q.precedes("a", "b") # True
35
+ ```
36
+
37
+ A new element gets the mediant (a+c)/(b+d) of its neighbors a/b and c/d, so normally no other element moves. Only when a denominator would exceed 64 bits is the smallest surrounding region with enough room renumbered; larger regions must be sparser before they are accepted (as in Bender et al., ESA 2002). Rational positions were Enis Olgac's idea.
38
+
39
+ Most users install `triordinal-dtree` (or later `triordinal-dgraph`), which pulls this package in. MIT License.
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ tests/test_dlist.py
4
+ triordinal/dlist/__init__.py
5
+ triordinal_dlist.egg-info/PKG-INFO
6
+ triordinal_dlist.egg-info/SOURCES.txt
7
+ triordinal_dlist.egg-info/dependency_links.txt
8
+ triordinal_dlist.egg-info/top_level.txt