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.
- triordinal_dlist-2.0.0/PKG-INFO +39 -0
- triordinal_dlist-2.0.0/README.md +20 -0
- triordinal_dlist-2.0.0/pyproject.toml +30 -0
- triordinal_dlist-2.0.0/setup.cfg +4 -0
- triordinal_dlist-2.0.0/tests/test_dlist.py +84 -0
- triordinal_dlist-2.0.0/triordinal/dlist/__init__.py +277 -0
- triordinal_dlist-2.0.0/triordinal_dlist.egg-info/PKG-INFO +39 -0
- triordinal_dlist-2.0.0/triordinal_dlist.egg-info/SOURCES.txt +8 -0
- triordinal_dlist-2.0.0/triordinal_dlist.egg-info/dependency_links.txt +1 -0
- triordinal_dlist-2.0.0/triordinal_dlist.egg-info/top_level.txt +1 -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,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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
triordinal
|