From b378b11458cccb4eaefda2a2050647935a736083 Mon Sep 17 00:00:00 2001 From: Calteran Date: Sat, 9 Dec 2023 07:44:08 +0100 Subject: [PATCH] feat: initial functionality of `FixedIndexVec` (#1) * chore: init rust library * feat: build initial functionality of `FixedIndexVec` * chore: setup github actions & dependabot --- .github/dependabot.yml | 9 ++ .github/workflow/build.yml | 25 +++ .gitignore | 7 + Cargo.toml | 12 ++ README.md | 72 ++++++++- src/lib.rs | 321 +++++++++++++++++++++++++++++++++++++ 6 files changed, 444 insertions(+), 2 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflow/build.yml create mode 100644 Cargo.toml create mode 100644 src/lib.rs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5ecfaf3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "daily" + time: "02:00" + timezone: "Europe/Paris" + open-pull-requests-limit: 10 \ No newline at end of file diff --git a/.github/workflow/build.yml b/.github/workflow/build.yml new file mode 100644 index 0000000..f14bcd6 --- /dev/null +++ b/.github/workflow/build.yml @@ -0,0 +1,25 @@ +name: Rust CI + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + build_and_test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build + run: cargo build --verbose + + - name: Run tests + run: cargo test --verbose diff --git a/.gitignore b/.gitignore index 6985cf1..2df3f35 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.idea/ + # Generated by Cargo # will have compiled files and executables debug/ @@ -12,3 +14,8 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4de56a6 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "fixed-index-vec" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0", optional = true, features = ["derive"] } + +[features] +enable_serde = ["serde"] \ No newline at end of file diff --git a/README.md b/README.md index 4047a03..6799b6d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,70 @@ -# fixed-index-vec -A Rust array- or vector-like data type that does not re-use indices. +# Fixed Index Vector - Rust Crate + +This crate provides `FixedIndexVec`, a Rust collection that functions like an array with immutable indices. +Each value is associated with a unique index upon insertion. +The value can be accessed, inserted, and removed with the index serving as the identifier. +An item cannot be modified, nor can it be replaced with another item, even after removal. + +With default features, `FixedIndexVec` has no dependencies outside of the Rust standard library. + +## Motivation + +I was looking for a way to do simple version control within a data structure. +The immutability of the indices in `FixedIndexVec` allows for a simple implementation of this functionality. +Items' insertion order is preserved through comparison of their indices, and the removal of an item is reflected +by indices that return `None` when accessed. + +To access the "current" version of an item, simply call `FixedIndexVec::last()`. + +There are other use cases for `FixedIndexVec` as well, but this is the one that motivated me to create it. + +## Installation + +Add the following to your `Cargo.toml`: + +```toml +[dependencies] +fixed_index_vec = "0.1.0" +``` + +Then include it in your application: + +```rust +use fixed_index_vec::FixedIndexVec; +``` + +## Functionality + +- Create a new `FixedIndexVec` with `FixedIndexVec::new()` +- Insert a new element at the end of the `FixedIndexVec` with `FixedIndexVec::push(value)` +- Remove an element at a given index with `FixedIndexVec::remove(index)`. If no element exists at that index, returns `None`. +- Return a reference to the element at a given index with `FixedIndexVec::get(index)`. If no element exists at that index, returns `None`. +- Iterate over all elements in ascending order of their indices with `FixedIndexVec::iter()`. Skips indices that do not have a corresponding value. +- Return the number of elements in the `FixedIndexVec` with `FixedIndexVec::len()` +- Check if `FixedIndexVec` contains no elements with `FixedIndexVec::is_empty()` +- Clears all values from `FixedIndexVec` with `FixedIndexVec::clear()` +- Clears all values from `FixedIndexVec` and resets the next index to 0 with `FixedIndexVec::reset()` + +## Features + +- `enable-serde`: Enables serialization and deserialization of `FixedIndexVec` with [Serde](https://serde.rs/). Requires `FixedIndexVec` to contain values that implement `Serialize` and `DeserializeOwned`. + +## Example + +```rust +use fixed_index_vec::FixedIndexVec; +let mut vec = FixedIndexVec::new(); +vec.push("value1".to_string()); vec.push("value2".to_string()); +assert_eq!(vec.get(0), Some(&"value1".to_string())); assert_eq!(vec.get(1), Some(&"value2".to_string())); +vec.remove(1); +assert_eq!(vec.get(1), None); +``` + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. + +## License + +This project is licensed under the MIT License. + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f582219 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,321 @@ +#![deny(missing_docs)] +#![doc = include_str!("../README.md")] +use std::collections::BTreeMap; + +/// A fixed-size indexed vector that maps indices to values. +/// +/// It provides a fixed-size vector-like data structure that can store values based on its +/// associated index. +/// Each value is associated with a unique index in the map. +/// The values can be +/// accessed, inserted, and removed using the index as the identifier. +/// +/// # Examples +/// +/// ``` +/// use fixed_index_vec::FixedIndexVec; +/// +/// let mut vec = FixedIndexVec::new(); +/// +/// vec.insert("value1".to_string()); +/// vec.insert("value2".to_string()); +/// +/// assert_eq!(vec.get(0), Some(&"value1".to_string())); +/// assert_eq!(vec.get(1), Some(&"value2".to_string())); +/// +/// vec.remove(1); +/// +/// assert_eq!(vec.get(1), None); +/// ``` +/// +/// # Notes +/// +/// - The `FixedIndexVec` is backed by a `BTreeMap`, so it is not as fast as a `Vec`. +/// - Index notations are supported (eg. `vec[0]`), however, accessing an index that does not +/// exist will panic. +#[derive(Clone, Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct FixedIndexVec { + map: BTreeMap, + next_index: usize, +} + +impl FixedIndexVec { + /// Creates an empty `FixedIndexVec`. + /// + /// The internal storage will not allocate until elements are pushed onto it. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// let mut vec: FixedIndexVec = FixedIndexVec::new(); + /// ``` + pub fn new() -> FixedIndexVec { + FixedIndexVec { + map: BTreeMap::new(), + next_index: 0, + } + } + + /// Inserts an element at the end of the `FixedIndexVec`. + /// + /// # Panics + /// + /// Panics if the `FixedIndexVec` is at capacity. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec = FixedIndexVec::new(); + /// vec.push(1); + /// vec.push(2); + /// assert_eq!(vec[0], 1); + /// assert_eq!(vec[1], 2); + /// ``` + pub fn push(&mut self, value: T) { + self.map.insert(self.next_index, value); + self.next_index += 1; + } + + /// Alias for `push`. + /// Inserts an element at the end of the `FixedIndexVec`. + /// + /// # Panics + /// + /// Panics if the `FixedIndexVec` is at capacity. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec = FixedIndexVec::new(); + /// vec.insert(1); + /// vec.insert(2); + /// assert_eq!(vec[0], 1); + /// assert_eq!(vec[1], 2); + /// ``` + pub fn insert(&mut self, value: T) { + self.push(value); + } + + /// Removes the element at the given index, if it exists, returning it or `None` if it does not exist. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec = FixedIndexVec::new(); + /// vec.push(1); + /// vec.push(2); + /// assert_eq!(vec.remove(0), Some(1)); + /// assert_eq!(vec.remove(0), None); + /// ``` + /// + /// # Notes + /// + /// Unlike `Vec::remove`, this does not shift elements after the removed element. + /// If index >= length, this returns `None`, the same as if the element did not exist. + pub fn remove(&mut self, index: usize) -> Option { + self.map.remove(&index) + } + + /// Returns a reference to the element at the given index, + /// if it exists, or `None` if it does not exist. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec = FixedIndexVec::new(); + /// vec.push(1); + /// vec.push(2); + /// assert_eq!(vec.get(0), Some(&1)); + /// assert_eq!(vec.get(2), None); + /// ``` + pub fn get(&self, index: usize) -> Option<&T> { + self.map.get(&index) + } + + /// An iterator visiting all elements in ascending order of their indices. + /// The index is returned along with the value. + /// The iterator skips indices that do not have a corresponding value. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec: FixedIndexVec = vec![1, 2, 3].into(); + /// vec.remove(1); + /// let mut iter = vec.iter(); + /// assert_eq!(iter.next(), Some((0, &1))); + /// assert_eq!(iter.next(), Some((2, &3))); + /// assert_eq!(iter.next(), None); + /// ``` + pub fn iter(&self) -> impl Iterator { + self.map.iter().map(|(i, v)| (*i, v)) + } + + /// Returns the number of elements in the `FixedIndexVec`. + /// This is not the same as the value of the largest index, unless no elements have been removed. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec: FixedIndexVec = vec![1, 2, 3].into(); + /// vec.remove(1); + /// assert_eq!(vec.len(), 2); + /// ``` + pub fn len(&self) -> usize { + self.map.len() + } + + /// Returns `true` if the `FixedIndexVec` contains no elements. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec: FixedIndexVec = vec![1, 2, 3].into(); + /// vec.remove(1); + /// assert_eq!(vec.is_empty(), false); + /// ``` + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// let vec: FixedIndexVec = FixedIndexVec::new(); + /// assert_eq!(vec.is_empty(), true); + /// ``` + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + + /// Clears the `FixedIndexVec`, removing all values. + /// Keeps the allocated memory for reuse. + /// This is equivalent to calling `remove` on every index. + /// The next index will *not* be reset to 0. + /// + /// # Examples + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec: FixedIndexVec = vec![1, 2, 3].into(); + /// vec.clear(); + /// assert_eq!(vec.len(), 0); + /// assert_eq!(vec.next_index(), 3); + /// ``` + pub fn clear(&mut self) { + self.map.clear(); + } + + /// Clears the `FixedIndexVec`, removing all values and resetting the next index to 0. + /// Keeps the allocated memory for reuse. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec: FixedIndexVec = vec![1, 2, 3].into(); + /// vec.reset(); + /// assert_eq!(vec.len(), 0); + /// assert_eq!(vec.next_index(), 0); + /// ``` + pub fn reset(&mut self) { + self.map.clear(); + self.next_index = 0; + } + + /// Returns the next index that will be used when inserting an element. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec: FixedIndexVec = vec![1, 2, 3].into(); + /// vec.remove(1); + /// assert_eq!(vec.next_index(), 3); + /// ``` + pub fn next_index(&self) -> usize { + self.next_index + } + + /// Returns the index and a reference to the element at the smallest populated index, or `None` + /// if the `FixedIndexVec` is empty. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec: FixedIndexVec = vec![1, 2, 3].into(); + /// vec.remove(0); + /// assert_eq!(vec.first(), Some((1, &2))); + /// ``` + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let vec: FixedIndexVec = FixedIndexVec::new(); + /// assert_eq!(vec.first(), None); + pub fn first(&self) -> Option<(usize, &T)> { + self.iter().next() + } + + /// Returns the index and a reference to the element at the largest populated index, or `None` + /// if the `FixedIndexVec` is empty. + /// + /// # Examples + /// + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let mut vec: FixedIndexVec = vec![1, 2, 3].into(); + /// vec.remove(2); + /// assert_eq!(vec.last(), Some((1, &2))); + /// ``` + /// ``` + /// use fixed_index_vec::FixedIndexVec; + /// + /// let vec: FixedIndexVec = FixedIndexVec::new(); + /// assert_eq!(vec.last(), None); + /// ``` + pub fn last(&self) -> Option<(usize, &T)> { + self.iter().last() + } +} + +impl std::ops::Index for FixedIndexVec { + type Output = T; + + fn index(&self, index: usize) -> &T { + self.get(index).unwrap() + } +} + +impl FromIterator for FixedIndexVec { + fn from_iter>(iter: I) -> FixedIndexVec { + let mut map = BTreeMap::new(); + for (i, v) in iter.into_iter().enumerate() { + map.insert(i, v); + } + FixedIndexVec { + next_index: map.len(), + map, + } + } +} + +impl From> for FixedIndexVec { + fn from(vec: Vec) -> FixedIndexVec { + vec.into_iter().collect() + } +}