Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 7 additions & 19 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
strategy:
matrix:
include:
- rust: 1.63.0 # MSRV
- rust: 1.82.0 # MSRV
features:
- rust: stable
features: arbitrary
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -123,20 +113,18 @@ 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:
name: Check MSRV and minimal-versions
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
Expand Down
15 changes: 12 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down Expand Up @@ -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"
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion benches/bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<FnvHasher>;

use test::black_box;
use test::Bencher;

use ordermap::OrderMap;
Expand Down
2 changes: 2 additions & 0 deletions benches/faststring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ impl<'a, S> From<&'a S> for &'a OneShot<str>
where
S: AsRef<str>,
{
#[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<str>) }
}
}
Expand Down
10 changes: 4 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand Down
27 changes: 26 additions & 1 deletion src/map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -941,6 +941,31 @@ impl<K, V, S> OrderMap<K, V, S> {
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`.
///
Expand Down
20 changes: 10 additions & 10 deletions src/map/mutable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -148,12 +150,10 @@ impl<K, V> MutableEntryKey for IndexedEntry<'_, K, V> {
}
}

mod private {
pub trait Sealed {}
trait Sealed {}

impl<K, V, S> Sealed for super::OrderMap<K, V, S> {}
impl<K, V> Sealed for super::Entry<'_, K, V> {}
impl<K, V> Sealed for super::OccupiedEntry<'_, K, V> {}
impl<K, V> Sealed for super::VacantEntry<'_, K, V> {}
impl<K, V> Sealed for super::IndexedEntry<'_, K, V> {}
}
impl<K, V, S> Sealed for OrderMap<K, V, S> {}
impl<K, V> Sealed for Entry<'_, K, V> {}
impl<K, V> Sealed for OccupiedEntry<'_, K, V> {}
impl<K, V> Sealed for VacantEntry<'_, K, V> {}
impl<K, V> Sealed for IndexedEntry<'_, K, V> {}
33 changes: 9 additions & 24 deletions src/map/raw_entry_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<K, V, S>: private::Sealed {
#[expect(private_bounds)]
pub trait RawEntryApiV1<K, V, S>: Sealed {
/// Creates a raw immutable entry builder for the [`OrderMap`].
///
/// Raw entries provide the lowest level of control for searching and
Expand All @@ -42,21 +43,14 @@ pub trait RawEntryApiV1<K, V, S>: 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<K: Hash + ?Sized, S: BuildHasher>(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);
Expand Down Expand Up @@ -103,20 +97,13 @@ pub trait RawEntryApiV1<K, V, S>: 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<K: Hash + ?Sized, S: BuildHasher>(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!(),
Expand All @@ -134,7 +121,7 @@ pub trait RawEntryApiV1<K, V, S>: 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) => {
Expand All @@ -147,7 +134,7 @@ pub trait RawEntryApiV1<K, V, S>: 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) => {
Expand Down Expand Up @@ -607,8 +594,6 @@ impl<'a, K, V, S> RawVacantEntryMut<'a, K, V, S> {
}
}

mod private {
pub trait Sealed {}
trait Sealed {}

impl<K, V, S> Sealed for super::OrderMap<K, V, S> {}
}
impl<K, V, S> Sealed for OrderMap<K, V, S> {}
26 changes: 25 additions & 1 deletion src/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -850,6 +850,30 @@ impl<T, S> OrderSet<T, S> {
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<T> {
self.inner.pop_if(predicate)
}

/// Scan through each value in the set and keep those where the
/// closure `keep` returns `true`.
///
Expand Down
Loading