diff --git a/pyority_queue.pyi b/pyority_queue.pyi index e76f406..8ee487a 100644 --- a/pyority_queue.pyi +++ b/pyority_queue.pyi @@ -25,6 +25,11 @@ class Queue[Data](ABC): :return: The next item in the queue """ + def pop(self) -> Data: + """ + :return: The next item in the queue + """ + class PureQueue[Data: Comparable](Queue[Data]): """ @@ -36,18 +41,13 @@ class PureQueue[Data: Comparable](Queue[Data]): :param items: An optional list of priorities with which to initialize the queue """ - def pop(self) -> Data: - """ - :return: The next item in the queue - """ - def insert(self, item: Data) -> None: """ :param item: Item to insert into the queue """ -class KeyedQueue[Data, Priority: Comparable](Queue[Data]): +class PairedQueue[Data, Priority: Comparable](Queue[Data]): """ A min-queue that allows arbitrary data associated with some priority, allowing duplicates of both data and priority. """ @@ -87,3 +87,9 @@ class IndexedQueue[Data: Hashable, Priority: Comparable](Queue[Data]): """ :param key: The item to delete from the queue """ + + def __contains__(self, key: Data) -> bool: + """ + :param key: The item to check the existence of + :return: Whether the item is in the queue + """ diff --git a/src/backing/indexed/mod.rs b/src/backing/indexed/mod.rs new file mode 100644 index 0000000..ebc1ad5 --- /dev/null +++ b/src/backing/indexed/mod.rs @@ -0,0 +1,12 @@ +/// Data structures for the "indexed" min-queues, supporting priority updates and arbitrary removals, but no duplicates +use super::{item::Item, pure::PureBacking}; + +/// A data structure usable for backing an "indexed" queue +pub trait IndexedBacking: + PureBacking> +{ + /// Update an item's priority + fn update(data: D, priority: P) -> Result<(), ()>; + /// Remove an item from the queue + fn remove(data: D) -> bool; +} diff --git a/src/backing/keyed/mod.rs b/src/backing/keyed/mod.rs deleted file mode 100644 index e9bc311..0000000 --- a/src/backing/keyed/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -/// Data structures for the "keyed" min-queues, supporting priority updates and arbitrary removals, but no duplicates -use super::{item::Item, pure::PureBacking}; - -/// A data structure usable for backing a "keyed" queue -pub trait KeyedBacking: - PureBacking> -{ - /// Update an item's priority - fn update(data: D, priority: P) -> Result<(), ()>; - /// Remove an item from the queue - fn remove(data: D) -> bool; -} diff --git a/src/backing/mod.rs b/src/backing/mod.rs index f66e16c..1c34cd3 100644 --- a/src/backing/mod.rs +++ b/src/backing/mod.rs @@ -1,3 +1,3 @@ +pub mod indexed; pub mod item; -pub mod keyed; pub mod pure; diff --git a/src/backing/pure/mod.rs b/src/backing/pure/mod.rs index 9c43b14..a8b0259 100644 --- a/src/backing/pure/mod.rs +++ b/src/backing/pure/mod.rs @@ -1,5 +1,6 @@ /// Data structures for the "pure" min-queues, supporting duplicates but no arbitrary updates -pub mod binary_heap; +mod binary_heap; +pub use binary_heap::BinaryHeap; /// A data structure usable for backing a "pure" queue pub trait PureBacking: Send + Sync { diff --git a/src/lib.rs b/src/lib.rs index 64be5b4..a43efe3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,11 +2,13 @@ pub mod backing; pub mod queue; use pyo3::prelude::*; -use queue::pure::PureQueue; +use queue::{IndexedQueue, PairedQueue, PureQueue}; /// Bindings for the Rust queue implementations #[pymodule] fn pyority_queue(m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; + m.add_class::()?; + m.add_class::()?; Ok(()) } diff --git a/src/queue/indexed.rs b/src/queue/indexed.rs new file mode 100644 index 0000000..086ad0f --- /dev/null +++ b/src/queue/indexed.rs @@ -0,0 +1,12 @@ +use pyo3::prelude::*; + +#[pyclass] +pub struct IndexedQueue {} + +#[pymethods] +impl IndexedQueue { + #[new] + fn new() -> Self { + Self {} + } +} diff --git a/src/queue/mod.rs b/src/queue/mod.rs index 9ec004d..17366d1 100644 --- a/src/queue/mod.rs +++ b/src/queue/mod.rs @@ -1 +1,7 @@ -pub mod pure; +mod indexed; +mod paired; +mod pure; + +pub use indexed::IndexedQueue; +pub use paired::PairedQueue; +pub use pure::PureQueue; diff --git a/src/queue/paired.rs b/src/queue/paired.rs new file mode 100644 index 0000000..ab837cf --- /dev/null +++ b/src/queue/paired.rs @@ -0,0 +1,25 @@ +// A "pure" priority queue that supports duplicates, but not arbitrary deletions or weight updates +use crate::backing::{ + item::Item, + pure::{BinaryHeap, PureBacking}, +}; +use pyo3::prelude::*; + +#[pyclass] +pub struct PairedQueue { + backing: Box, f64>>>, +} + +#[pymethods] +impl PairedQueue { + #[new] + fn new() -> Self { + Self { + backing: Box::new(BinaryHeap::new()), + } + } + + fn __len__(self_: PyRef<'_, Self>) -> usize { + self_.backing.len() + } +} diff --git a/src/queue/pure.rs b/src/queue/pure.rs index 4c03a26..8cf4912 100644 --- a/src/queue/pure.rs +++ b/src/queue/pure.rs @@ -1,25 +1,12 @@ -// A "pure" priority queue that supports duplicates, but not arbitrary deletions or weight updates -use crate::backing::{ - item::Item, - pure::{binary_heap::BinaryHeap, PureBacking}, -}; use pyo3::prelude::*; #[pyclass] -pub struct PureQueue { - backing: Box, f64>>>, -} +pub struct PureQueue {} #[pymethods] impl PureQueue { #[new] fn new() -> Self { - Self { - backing: Box::new(BinaryHeap::new()), - } - } - - fn __len__(self_: PyRef<'_, Self>) -> usize { - self_.backing.len() + Self {} } } diff --git a/tests/indexed_queue.py b/tests/indexed_queue.py new file mode 100644 index 0000000..c829962 --- /dev/null +++ b/tests/indexed_queue.py @@ -0,0 +1,132 @@ +from typing import Any, TYPE_CHECKING + +import pytest + +from pyority_queue import IndexedQueue +if TYPE_CHECKING: + from pyority_queue import Comparable + + +type IndexedQueueInitializer = dict[Any, Comparable] | list[tuple[Any, Comparable]] | tuple[tuple[Any, Comparable], ...] + + +def test_empty_creation(): + queue = IndexedQueue() + assert len(queue) == 0 + + +@pytest.mark.parametrize("items", ( + [], + [("a", 0), ("b", 1), ("c", 2)], + ((0, 0), (1, 1), (2, 2)), + ((0.0, 0.0), (1.0, 1.0), (2.0, 2.0)), + ((lambda: None, 0)), + ((Exception(), 0.0)), + (([], -1)), + {}, + {"a": 0, "b": 1, "c": 2}, +)) +def test_creation(items: IndexedQueueInitializer): + queue = IndexedQueue() + assert len(queue) == len(items) + + +@pytest.mark.parametrize("items", ( + [], + ((0, 0),), + ((-1, -1), (3, 3)), +)) +def test_iteration(items: IndexedQueueInitializer): + queue = IndexedQueue(items) + assert len(list(queue)) == len(items) + + +@pytest.mark.parametrize("items", ( + [], + (("a", -3), ("b", 5)), + (("c", 3.0), ("b", 2.0), ("a", 1.0)), + (("c", 3), ("f", 6), ("h", 8), ("e", 5), ("g", 7), ("d", 4), ("b", 2), ("a", 0)), +)) +def test_sorting(items: IndexedQueueInitializer): + queue = IndexedQueue(items) + in_order = list(queue) + assert in_order == sorted(in_order) + + +def test_insertion(): + queue = IndexedQueue({"a": 1, "b": 2, "c": 3}) + queue["d"] = 4 + queue["e"] = 5 + assert list(queue) == ["a", "b", "c", "d", "e"] + + +def test_removal(): + queue = IndexedQueue({"a": 1, "b": 2, "c": 3}) + assert queue.pop() == "a" + assert queue.pop() == "b" + assert queue.pop() == "c" + assert len(queue) == 0 + + +def test_mixed_removal(): + queue = IndexedQueue({"a": 1, "b": 2, "c": 3}) + assert queue.pop() == "a" + assert list(queue) == ["b", "c"] + + +def test_empty_removal(): + queue = IndexedQueue() + with pytest.raises(IndexError): + queue.pop() + + +def test_duplicates(): + queue = IndexedQueue[str, int]((("a", 0), ("a", 0), ("a", 2))) + queue["b"] = 1 + queue["b"] = 3 + assert list(queue) == ["a", "b"] + + +def test_deletion(): + queue = IndexedQueue({"a": 1, "b": 2, "c": 3}) + del queue["a"] + del queue["b"] + assert list(queue) == ["c"] + + +def test_empty_deletion(): + queue = IndexedQueue() + with pytest.raises(KeyError): + del queue["a"] + + +def test_in(): + queue = IndexedQueue({"a": 1, "b": 2, "c": 3}) + assert "a" in queue + assert "b" in queue + assert "c" in queue + assert "d" not in queue + del queue["b"] + assert "b" not in queue + + +def test_mixed_iteration(): + queue = IndexedQueue({"a": 1, "b": 2, "c": 3}) + results = [] + for char in queue: + results.append(char) + if len(queue): + queue[queue.pop()] = 10 + assert results == ["a", "c", "b"] + + +def test_iteration_deletion(): + queue = IndexedQueue({"a": 1, "b": 2, "c": 3}) + results = [] + for char in queue: + results.append(char) + if "b" in queue: + del queue["b"] + assert results == ["a", "c"] + + diff --git a/tests/paired_queue.py b/tests/paired_queue.py new file mode 100644 index 0000000..2f88d7b --- /dev/null +++ b/tests/paired_queue.py @@ -0,0 +1,97 @@ +from typing import Any, TYPE_CHECKING + +import pytest + +from pyority_queue import PairedQueue +if TYPE_CHECKING: + from pyority_queue import Comparable + + +type PairedQueueInitializer = dict[Any, Comparable] | list[tuple[Any, Comparable]] | tuple[tuple[Any, Comparable], ...] + + +def test_empty_creation(): + queue = PairedQueue() + assert len(queue) == 0 + + +@pytest.mark.parametrize("items", ( + [], + [("a", 0), ("b", 1), ("c", 2)], + ((0, 0), (1, 1), (2, 2)), + ((0.0, 0.0), (1.0, 1.0), (2.0, 2.0)), + ((lambda: None, 0)), + ((Exception(), 0.0)), + (([], -1)), + {}, + {"a": 0, "b": 1, "c": 2}, +)) +def test_creation(items: PairedQueueInitializer): + queue = PairedQueue() + assert len(queue) == len(items) + + +@pytest.mark.parametrize("items", ( + [], + ((0, 0),), + ((-1, -1), (3, 3)), +)) +def test_iteration(items: PairedQueueInitializer): + queue = PairedQueue(items) + assert len(list(queue)) == len(items) + + +@pytest.mark.parametrize("items", ( + [], + (("a", -3), ("b", 5)), + (("c", 3.0), ("b", 2.0), ("a", 1.0)), + (("c", 3), ("f", 6), ("h", 8), ("e", 5), ("g", 7), ("d", 4), ("b", 2), ("a", 0)), +)) +def test_sorting(items: PairedQueueInitializer): + queue = PairedQueue(items) + in_order = list(queue) + assert in_order == sorted(in_order) + + +def test_insertion(): + queue = PairedQueue({"a": 1, "b": 2, "c": 3}) + queue["d"] = 4 + queue["e"] = 5 + assert list(queue) == ["a", "b", "c", "d", "e"] + + +def test_removal(): + queue = PairedQueue[str, int]({"a": 1, "b": 2, "c": 3}) + assert queue.pop() == "a" + assert queue.pop() == "b" + assert queue.pop() == "c" + assert len(queue) == 0 + + +def test_mixed_removal(): + queue = PairedQueue[str, int]({"a": 1, "b": 2, "c": 3}) + assert queue.pop() == "a" + assert list(queue) == ["b", "c"] + + +def test_empty_removal(): + queue = PairedQueue() + with pytest.raises(IndexError): + queue.pop() + + +def test_duplicates(): + queue = PairedQueue[str, int]((("a", 0), ("a", 0), ("a", 2))) + queue["b"] = 1 + queue["b"] = 3 + assert list(queue) == ["a", "a", "b", "a", "b"] + + +def test_mixed_iteration(): + queue = PairedQueue[str, int]({"a": 1, "b": 2, "c": 3}) + results = [] + for char in queue: + results.append(char) + if len(queue): + queue[queue.pop()] = 10 + assert results == ["a", "c", "b"] diff --git a/tests/pure_binary_heap.rs b/tests/pure_binary_heap.rs index 597d6eb..9daeaff 100644 --- a/tests/pure_binary_heap.rs +++ b/tests/pure_binary_heap.rs @@ -1,6 +1,6 @@ #[cfg(test)] mod tests { - use pyority_queue::backing::pure::{binary_heap::BinaryHeap, PureBacking}; + use pyority_queue::backing::pure::{BinaryHeap, PureBacking}; #[test] fn test_pure_binary_heap_manual_creation() { diff --git a/tests/pure_queue.py b/tests/pure_queue.py index ce32069..559fe9f 100644 --- a/tests/pure_queue.py +++ b/tests/pure_queue.py @@ -1,9 +1,13 @@ +from typing import TYPE_CHECKING + import pytest from pyority_queue import PureQueue +if TYPE_CHECKING: + from pyority_queue import Comparable -type PureQueueInitializer = list[int | float] | tuple[int | float, ...] +type PureQueueInitializer = list[Comparable] | tuple[Comparable, ...] def test_empty_creation(): @@ -16,6 +20,7 @@ def test_creation(items: PureQueueInitializer): queue = PureQueue() assert len(queue) == len(items) + @pytest.mark.parametrize("items", ([], (0,), (-1, 3), range(100))) def test_iteration(items: PureQueueInitializer): queue = PureQueue(items) @@ -36,8 +41,39 @@ def test_insertion(): assert list(queue) == [0, 2, 3, 4, 6, 7, 8] +def test_removal(): + queue = PureQueue[int]((4, 2, 8, 6)) + assert queue.pop() == 2 + assert queue.pop() == 4 + assert queue.pop() == 6 + assert queue.pop() == 8 + assert len(queue) == 0 + + +def test_mixed_removal(): + queue = PureQueue[int]((4, 2, 8, 6)) + assert queue.pop() == 2 + assert list(queue) == [4, 6, 8] + + +def test_empty_removal(): + queue = PureQueue() + with pytest.raises(IndexError): + queue.pop() + + def test_duplicates(): queue = PureQueue[int]((0, 0, 0, 5, 5)) queue.insert(3) queue.insert(3) assert list(queue) == [0, 0, 0, 3, 3, 5, 5] + + +def test_mixed_iteration(): + queue = PureQueue[int]((4, 2, 8, 6)) + results = [] + for number in queue: + results.append(number) + if len(queue): + queue.insert(queue.pop() * 2) + assert results == [2, 6, 8, 16]