id_pool/
lib.rs

1//! Create and recycle integer ids using a ranged pool.
2//!
3//! This crate is fairly minimalistic and so only deals
4//! with single type of numerical ids. These can be either
5//! `usize` (default), `u64`, `u32` or `u16`, chosen with
6//! the use of appropriate crate feature.
7//!
8//! The main exported structure [`IdPool`] can be
9//! initialized with a custom range and then queried for
10//! new ids that are contained within that range. During
11//! the course of the program, ids can be returned to the
12//! pool to be reused for subsequent id request calls.
13//!
14//! [`IdPool`]: struct.IdPool.html
15
16#[cfg(feature = "serde")]
17use serde::{Deserialize, Serialize};
18
19#[cfg(feature = "u16")]
20type Num = u16;
21#[cfg(feature = "u32")]
22type Num = u32;
23#[cfg(feature = "u64")]
24type Num = u64;
25#[cfg(feature = "usize")]
26type Num = usize;
27
28/// Custom range struct
29#[derive(Copy, Debug, Clone)]
30#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
31pub struct Range {
32    start: Num,
33    end: Num,
34}
35impl Range {
36    /// Calculates the length of the range.
37    pub fn len(&self) -> Num {
38        self.end - self.start
39    }
40
41    /// Calculates whether a given value is contained
42    /// within the range.
43    pub fn contains(&self, value: &Num) -> bool {
44        value >= &self.start && value <= &self.end
45    }
46}
47
48/// Keeps track of free ids within a specified range,
49/// handles requests and returns of ids based on internal
50/// state.
51///
52/// Internally, a collection of free id ranges is stored.
53/// On a request for an id from the pool, the lowest
54/// available number will be returned to the caller.
55/// Ids can also be returned to the pool to be reused by
56/// subsequent id requests.
57///
58/// # Examples
59///
60/// ```
61/// # use id_pool::IdPool;
62/// // initialize a new pool
63/// let mut pool = IdPool::new();
64/// // request some ids
65/// assert_eq!(Some(1), pool.request_id());
66/// assert_eq!(Some(2), pool.request_id());
67/// assert_eq!(Some(3), pool.request_id());
68/// // return the first id
69/// assert_eq!(Ok(()), pool.return_id(1));
70/// // next request returns recycled first id
71/// assert_eq!(Some(1), pool.request_id());
72/// // subsequent request returns the next free value
73/// assert_eq!(Some(4), pool.request_id());
74/// ```
75#[derive(Debug, Clone)]
76#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
77pub struct IdPool {
78    /// List of available id ranges
79    free: Vec<Range>,
80    /// Number of ids currently in use
81    used: usize,
82}
83
84impl IdPool {
85    /// Creates a new `IdPool` with a default range, which
86    /// starts at `1` and ends at `Num::MAX`.
87    pub fn new() -> Self {
88        Self::new_ranged(1..Num::MAX)
89    }
90
91    /// Creates a new `IdPool` with the given range.
92    pub fn new_ranged(range: std::ops::Range<Num>) -> Self {
93        let vec = vec![Range {
94            start: range.start,
95            end: range.end,
96        }];
97        Self { free: vec, used: 0 }
98    }
99
100    /// Gets the current count of used ids.
101    pub fn used_count(&self) -> usize {
102        self.used
103    }
104
105    /// Returns a new id or `None` if there are no free ids
106    /// in the pool.
107    pub fn request_id(&mut self) -> Option<Num> {
108        // short-circuit if there are no free ranges
109        if self.free.len() == 0 {
110            return None;
111        }
112        // always work on the last range on the list
113        let range = self.free.last_mut().unwrap();
114        // get the first number from the range
115        let id = range.start;
116        // increment range starting point
117        range.start += 1;
118        // if we have just emptied the range then pop it from the list
119        if range.len() == 0 {
120            self.free.pop();
121        }
122        self.used += 1;
123        Some(id)
124    }
125
126    /// Returns an id to the pool or `Err(Num)` if the
127    /// id is already in the pool.
128    pub fn return_id(&mut self, id: Num) -> Result<(), Num> {
129        // search stored ranges for the id in question
130        let position = self.free.binary_search_by(|range| {
131            // match if the id value is adjacent to the range
132            if range.start.checked_sub(1) == Some(id) || range.end == id {
133                std::cmp::Ordering::Equal
134            }
135            // match if the id value is contained within the range
136            else if range.contains(&id) {
137                std::cmp::Ordering::Equal
138            }
139            // otherwise indicate the match must be closer to the id value
140            else {
141                id.cmp(&range.start)
142            }
143        });
144        match position {
145            // range containing id in question was not found,
146            // insert a new range that includes the returned id
147            // at a point in the list that is closest to the id value
148            Err(i) => self.free.insert(
149                i,
150                Range {
151                    start: id,
152                    end: id + 1,
153                },
154            ),
155            // found range adjacent to or containing the id in question
156            Ok(i) => {
157                let range = &mut self.free[i];
158                // id value adjacent to range start point
159                if range.start.checked_sub(1) == Some(id) {
160                    range.start = id;
161                }
162                // id value adjacent to range end point
163                else if range.end == id {
164                    range.end = range.end + 1;
165                }
166                // id value contained within one of the ranges,
167                // can't return id to the pool
168                else {
169                    return Err(id);
170                }
171                // check if there exists a range before the current one
172                if let Some(before_range_idx) = i.checked_sub(1) {
173                    // if the current range's end point is the previous range's
174                    // start point, then merge the ranges into one
175                    if self.free[before_range_idx].start == self.free[i].end {
176                        self.free[before_range_idx].start = self.free[i].start;
177                        self.free.remove(i);
178                    }
179                }
180            }
181        }
182        self.used -= 1;
183        Ok(())
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn request() {
193        let mut pool = IdPool::new();
194        assert_eq!(Some(1), pool.request_id());
195        assert_eq!(Some(2), pool.request_id());
196        assert_eq!(Some(3), pool.request_id());
197    }
198
199    #[test]
200    fn request_return() {
201        let mut pool = IdPool::new();
202        assert_eq!(Some(1), pool.request_id());
203        assert_eq!(Some(2), pool.request_id());
204        assert_eq!(Some(3), pool.request_id());
205        assert_eq!(Ok(()), pool.return_id(1));
206        assert_eq!(Some(1), pool.request_id());
207        assert_eq!(Ok(()), pool.return_id(2));
208        assert_eq!(Some(2), pool.request_id());
209        assert_eq!(Some(4), pool.request_id());
210        assert_eq!(Ok(()), pool.return_id(3));
211        assert_eq!(Ok(()), pool.return_id(4));
212        assert_eq!(Err(5), pool.return_id(5));
213    }
214
215    #[test]
216    fn used_count() {
217        let mut pool = IdPool::new_ranged(1..10);
218        assert_eq!(Some(1), pool.request_id());
219        assert_eq!(Some(2), pool.request_id());
220        assert_eq!(Some(3), pool.request_id());
221        assert_eq!(Ok(()), pool.return_id(1));
222        assert_eq!(2, pool.used_count());
223    }
224
225    #[test]
226    fn return_last() {
227        let mut pool = IdPool::new_ranged(1..10);
228        assert_eq!(Some(1), pool.request_id());
229        assert_eq!(Some(2), pool.request_id());
230        assert_eq!(Some(3), pool.request_id());
231        assert_eq!(Ok(()), pool.return_id(3));
232    }
233}