diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e166236..81290b3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: strategy: matrix: include: - - rust: 1.63.0 # MSRV + - rust: 1.82.0 # MSRV features: - rust: stable features: arbitrary @@ -40,13 +40,8 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/cache@v4 - if: matrix.rust == '1.63.0' - with: - path: ~/.cargo/registry/index - key: cargo-git-index - name: Lock MSRV-compatible dependencies - if: matrix.rust == '1.63.0' + if: matrix.rust == '1.82.0' env: CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback # Note that this uses the runner's pre-installed stable cargo @@ -77,20 +72,15 @@ jobs: strategy: matrix: include: - - rust: 1.63.0 + - rust: 1.82.0 target: thumbv6m-none-eabi - rust: stable target: thumbv6m-none-eabi steps: - uses: actions/checkout@v4 - - uses: actions/cache@v4 - if: matrix.rust == '1.63.0' - with: - path: ~/.cargo/registry/index - key: cargo-git-index - name: Lock MSRV-compatible dependencies - if: matrix.rust == '1.63.0' + if: matrix.rust == '1.82.0' env: CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS: fallback # Note that this uses the runner's pre-installed stable cargo @@ -123,7 +113,9 @@ jobs: - uses: taiki-e/install-action@v2 with: tool: cargo-nextest + if: github.event_name == 'merge_group' - run: cargo miri nextest run + if: github.event_name == 'merge_group' - run: cargo miri test --doc minimal-versions: @@ -131,12 +123,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/cache@v4 - with: - path: ~/.cargo/registry/index - key: cargo-git-index - uses: dtolnay/rust-toolchain@nightly - - uses: dtolnay/rust-toolchain@1.63.0 # MSRV + - uses: dtolnay/rust-toolchain@1.82.0 # MSRV - uses: taiki-e/install-action@v2 with: tool: cargo-hack diff --git a/Cargo.toml b/Cargo.toml index d632e69..bd3705f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,20 @@ [package] name = "ordermap" edition = "2021" -version = "0.5.12" +version = "1.0.0" documentation = "https://docs.rs/ordermap/" repository = "https://github.com/indexmap-rs/ordermap" license = "Apache-2.0 OR MIT" description = "A hash table with consistent order and fast iteration." keywords = ["hashmap", "no_std"] categories = ["data-structures", "no-std"] -rust-version = "1.63" +rust-version = "1.82" [lib] bench = false [dependencies] -indexmap = { version = "2.11.3", default-features = false } +indexmap = { version = "2.12.0", default-features = false } arbitrary = { version = "1.0", optional = true, default-features = false } quickcheck = { version = "1.0", optional = true, default-features = false } @@ -61,5 +61,14 @@ rustdoc-args = ["--cfg", "docsrs"] [workspace] members = ["test-nostd", "test-serde", "test-sval"] +[lints.rust] +private-bounds = "deny" +private-interfaces = "deny" +unnameable-types = "deny" +unreachable-pub = "deny" +unsafe-code = "deny" + +rust-2018-idioms = "warn" + [lints.clippy] style = "allow" diff --git a/README.md b/README.md index 100c9a7..f4675ce 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![build status](https://github.com/indexmap-rs/ordermap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/indexmap-rs/ordermap/actions) [![crates.io](https://img.shields.io/crates/v/ordermap.svg)](https://crates.io/crates/ordermap) [![docs](https://docs.rs/ordermap/badge.svg)](https://docs.rs/ordermap) -[![rustc](https://img.shields.io/badge/rust-1.63%2B-orange.svg)](https://img.shields.io/badge/rust-1.63%2B-orange.svg) +[![rustc](https://img.shields.io/badge/rust-1.82%2B-orange.svg)](https://img.shields.io/badge/rust-1.82%2B-orange.svg) A pure-Rust hash table which preserves (in a limited sense) insertion order. diff --git a/RELEASES.md b/RELEASES.md index a1cd2d1..cdac4e7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,12 @@ # Releases +## 1.0.0 (2025-10-17) + +- **MSRV**: Rust 1.82.0 or later is now required. +- Updated the `indexmap` dependency to version 2.12.0. +- Added `pop_if` methods to `OrderMap` and `OrderSet`, similar to the + method for `Vec` added in Rust 1.86. + ## 0.5.12 (2025-09-15) - Make the minimum `serde` version only apply when "serde" is enabled. diff --git a/benches/bench.rs b/benches/bench.rs index 6233413..5d867df 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -5,10 +5,10 @@ extern crate test; use fnv::FnvHasher; use std::hash::BuildHasherDefault; use std::hash::Hash; +use std::hint::black_box; use std::sync::LazyLock; type FnvBuilder = BuildHasherDefault; -use test::black_box; use test::Bencher; use ordermap::OrderMap; diff --git a/benches/faststring.rs b/benches/faststring.rs index 53f35a8..c1d9760 100644 --- a/benches/faststring.rs +++ b/benches/faststring.rs @@ -33,8 +33,10 @@ impl<'a, S> From<&'a S> for &'a OneShot where S: AsRef, { + #[allow(unsafe_code)] fn from(s: &'a S) -> Self { let s: &str = s.as_ref(); + // SAFETY: OneShot is a `repr(transparent)` wrapper unsafe { &*(s as *const str as *const OneShot) } } } diff --git a/src/lib.rs b/src/lib.rs index c6a6f0a..291eaf4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,3 @@ -#![forbid(unsafe_code)] -#![warn(rust_2018_idioms)] #![no_std] //! [`OrderMap`] is a hash table where the iteration order of the key-value @@ -55,7 +53,7 @@ //! ### Alternate Hashers //! //! [`OrderMap`] and [`OrderSet`] have a default hasher type -//! [`S = RandomState`][std::collections::hash_map::RandomState], +//! [`S = RandomState`][std::hash::RandomState], //! just like the standard `HashMap` and `HashSet`, which is resistant to //! HashDoS attacks but not the most performant. Type aliases can make it easier //! to use alternate hashers: @@ -74,10 +72,10 @@ //! //! ### Rust Version //! -//! This version of ordermap requires Rust 1.63 or later. +//! This version of ordermap requires Rust 1.82 or later. //! -//! The ordermap 0.x release series will use a carefully considered version -//! upgrade policy, where in a later 0.x version, we will raise the minimum +//! The ordermap 1.x release series will use a carefully considered version +//! upgrade policy, where in a later 1.x version, we will raise the minimum //! required Rust version. //! //! ## No Standard Library Targets diff --git a/src/map.rs b/src/map.rs index 91804a1..43d93ad 100644 --- a/src/map.rs +++ b/src/map.rs @@ -47,7 +47,7 @@ use indexmap::IndexMap; use alloc::vec::Vec; #[cfg(feature = "std")] -use std::collections::hash_map::RandomState; +use std::hash::RandomState; use crate::{Equivalent, GetDisjointMutError, TryReserveError}; @@ -941,6 +941,31 @@ impl OrderMap { self.inner.pop() } + /// Removes and returns the last key-value pair from a map if the predicate + /// returns `true`, or [`None`] if the predicate returns false or the map + /// is empty (the predicate will not be called in that case). + /// + /// This preserves the order of the remaining elements. + /// + /// Computes in **O(1)** time (average). + /// + /// # Examples + /// + /// ``` + /// use ordermap::OrderMap; + /// + /// let init = [(1, 'a'), (2, 'b'), (3, 'c'), (4, 'd')]; + /// let mut map = OrderMap::from(init); + /// let pred = |key: &i32, _value: &mut char| *key % 2 == 0; + /// + /// assert_eq!(map.pop_if(pred), Some((4, 'd'))); + /// assert_eq!(map.as_slice(), &init[..3]); + /// assert_eq!(map.pop_if(pred), None); + /// ``` + pub fn pop_if(&mut self, predicate: impl FnOnce(&K, &mut V) -> bool) -> Option<(K, V)> { + self.inner.pop_if(predicate) + } + /// Scan through each key-value pair in the map and keep those where the /// closure `keep` returns `true`. /// diff --git a/src/map/mutable.rs b/src/map/mutable.rs index c81bdf3..a57998d 100644 --- a/src/map/mutable.rs +++ b/src/map/mutable.rs @@ -17,7 +17,8 @@ use indexmap::map::MutableKeys as _; /// `use` this trait to enable its methods for `OrderMap`. /// /// This trait is sealed and cannot be implemented for types outside this crate. -pub trait MutableKeys: private::Sealed { +#[expect(private_bounds)] +pub trait MutableKeys: Sealed { type Key; type Value; @@ -97,7 +98,8 @@ where /// `use` this trait to enable its methods for `Entry`. /// /// This trait is sealed and cannot be implemented for types outside this crate. -pub trait MutableEntryKey: private::Sealed { +#[expect(private_bounds)] +pub trait MutableEntryKey: Sealed { type Key; /// Gets a mutable reference to the entry's key, either within the map if occupied, @@ -148,12 +150,10 @@ impl MutableEntryKey for IndexedEntry<'_, K, V> { } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::OrderMap {} - impl Sealed for super::Entry<'_, K, V> {} - impl Sealed for super::OccupiedEntry<'_, K, V> {} - impl Sealed for super::VacantEntry<'_, K, V> {} - impl Sealed for super::IndexedEntry<'_, K, V> {} -} +impl Sealed for OrderMap {} +impl Sealed for Entry<'_, K, V> {} +impl Sealed for OccupiedEntry<'_, K, V> {} +impl Sealed for VacantEntry<'_, K, V> {} +impl Sealed for IndexedEntry<'_, K, V> {} diff --git a/src/map/raw_entry_v1.rs b/src/map/raw_entry_v1.rs index d67e4d9..a05f540 100644 --- a/src/map/raw_entry_v1.rs +++ b/src/map/raw_entry_v1.rs @@ -21,7 +21,8 @@ use alloc::vec::Vec; /// Opt-in access to the experimental raw entry API. /// /// See the [`raw_entry_v1`][self] module documentation for more information. -pub trait RawEntryApiV1: private::Sealed { +#[expect(private_bounds)] +pub trait RawEntryApiV1: Sealed { /// Creates a raw immutable entry builder for the [`OrderMap`]. /// /// Raw entries provide the lowest level of control for searching and @@ -42,21 +43,14 @@ pub trait RawEntryApiV1: private::Sealed { /// # Examples /// /// ``` - /// use core::hash::{BuildHasher, Hash}; + /// use core::hash::BuildHasher; /// use ordermap::map::{OrderMap, RawEntryApiV1}; /// /// let mut map = OrderMap::new(); /// map.extend([("a", 100), ("b", 200), ("c", 300)]); /// - /// fn compute_hash(hash_builder: &S, key: &K) -> u64 { - /// use core::hash::Hasher; - /// let mut state = hash_builder.build_hasher(); - /// key.hash(&mut state); - /// state.finish() - /// } - /// /// for k in ["a", "b", "c", "d", "e", "f"] { - /// let hash = compute_hash(map.hasher(), k); + /// let hash = map.hasher().hash_one(k); /// let i = map.get_index_of(k); /// let v = map.get(k); /// let kv = map.get_key_value(k); @@ -103,20 +97,13 @@ pub trait RawEntryApiV1: private::Sealed { /// # Examples /// /// ``` - /// use core::hash::{BuildHasher, Hash}; + /// use core::hash::BuildHasher; /// use ordermap::map::{OrderMap, RawEntryApiV1}; /// use ordermap::map::raw_entry_v1::RawEntryMut; /// /// let mut map = OrderMap::new(); /// map.extend([("a", 100), ("b", 200), ("c", 300)]); /// - /// fn compute_hash(hash_builder: &S, key: &K) -> u64 { - /// use core::hash::Hasher; - /// let mut state = hash_builder.build_hasher(); - /// key.hash(&mut state); - /// state.finish() - /// } - /// /// // Existing key (insert and update) /// match map.raw_entry_mut_v1().from_key("a") { /// RawEntryMut::Vacant(_) => unreachable!(), @@ -134,7 +121,7 @@ pub trait RawEntryApiV1: private::Sealed { /// assert_eq!(map.len(), 3); /// /// // Existing key (take) - /// let hash = compute_hash(map.hasher(), "c"); + /// let hash = map.hasher().hash_one("c"); /// match map.raw_entry_mut_v1().from_key_hashed_nocheck(hash, "c") { /// RawEntryMut::Vacant(_) => unreachable!(), /// RawEntryMut::Occupied(view) => { @@ -147,7 +134,7 @@ pub trait RawEntryApiV1: private::Sealed { /// /// // Nonexistent key (insert and update) /// let key = "d"; - /// let hash = compute_hash(map.hasher(), key); + /// let hash = map.hasher().hash_one(key); /// match map.raw_entry_mut_v1().from_hash(hash, |q| *q == key) { /// RawEntryMut::Occupied(_) => unreachable!(), /// RawEntryMut::Vacant(view) => { @@ -607,8 +594,6 @@ impl<'a, K, V, S> RawVacantEntryMut<'a, K, V, S> { } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::OrderMap {} -} +impl Sealed for OrderMap {} diff --git a/src/set.rs b/src/set.rs index b2d5967..4c5410d 100644 --- a/src/set.rs +++ b/src/set.rs @@ -37,7 +37,7 @@ use indexmap::IndexSet; use alloc::vec::Vec; #[cfg(feature = "std")] -use std::collections::hash_map::RandomState; +use std::hash::RandomState; use crate::{Equivalent, TryReserveError}; @@ -850,6 +850,30 @@ impl OrderSet { self.inner.pop() } + /// Removes and returns the last value from a set if the predicate + /// returns `true`, or [`None`] if the predicate returns false or the set + /// is empty (the predicate will not be called in that case). + /// + /// This preserves the order of the remaining elements. + /// + /// Computes in **O(1)** time (average). + /// + /// # Examples + /// + /// ``` + /// use ordermap::OrderSet; + /// + /// let mut set = OrderSet::from([1, 2, 3, 4]); + /// let pred = |x: &i32| *x % 2 == 0; + /// + /// assert_eq!(set.pop_if(pred), Some(4)); + /// assert_eq!(set.as_slice(), &[1, 2, 3]); + /// assert_eq!(set.pop_if(pred), None); + /// ``` + pub fn pop_if(&mut self, predicate: impl FnOnce(&T) -> bool) -> Option { + self.inner.pop_if(predicate) + } + /// Scan through each value in the set and keep those where the /// closure `keep` returns `true`. /// diff --git a/src/set/mutable.rs b/src/set/mutable.rs index 3256924..fbfe434 100644 --- a/src/set/mutable.rs +++ b/src/set/mutable.rs @@ -16,7 +16,8 @@ use indexmap::set::MutableValues as _; /// `use` this trait to enable its methods for `OrderSet`. /// /// This trait is sealed and cannot be implemented for types outside this crate. -pub trait MutableValues: private::Sealed { +#[expect(private_bounds)] +pub trait MutableValues: Sealed { type Value; /// Return item index and mutable reference to the value @@ -72,8 +73,6 @@ where } } -mod private { - pub trait Sealed {} +trait Sealed {} - impl Sealed for super::OrderSet {} -} +impl Sealed for OrderSet {} diff --git a/tests/quick.rs b/tests/quick.rs index 75bfa25..dcd694b 100644 --- a/tests/quick.rs +++ b/tests/quick.rs @@ -833,8 +833,7 @@ quickcheck_limit! { fn assert_sorted_by_key(iterable: I, key: Key) where - I: IntoIterator, - I::Item: Ord + Clone + Debug, + I: IntoIterator, Key: Fn(&I::Item) -> X, X: Ord, {