dbg_ranges/
lib.rs

1//! Debug implementations for ranges of values
2//!
3//! This is a simple crate which helps debugging in certain scenarios. Many algorithms rely on
4//! lists of items, such as integers, and often these lists contain runs of values that are
5//! all "adjacent".
6//!
7//! For example, a filesystem implementation might store a list of block numbers that contain the
8//! data for a particular file. If some blocks are allocated sequentially, then there may be
9//! many runs of adjacent values. For example, `[42, 100, 101, 102, 103, 104, 20, 31, 32, 33, 34]`.
10//! It can be helpful to display the runs as ranges, e.g. `[42, 100-104, 20, 31-34]`. This is more
11//! compact and can help the developer spot patterns in data more quickly.
12//!
13//! This crate provides two types that display ranges more compactly, and functions which construct
14//! those types.
15//!
16//! See [`debug_adjacent`] for an example.
17
18#![forbid(unsafe_code)]
19#![warn(missing_docs)]
20#![allow(clippy::needless_lifetimes)]
21#![cfg_attr(not(test), no_std)]
22
23use core::fmt::{Debug, Formatter};
24
25/// Returns a value that implements `Debug` by collapsing runs of "adjacent" items.
26///
27/// The `IsAdjacent` trait defines whether two values in `T` are adjacent. Implementations are
28/// provided for Rust integer types.
29///
30/// # Example
31/// ```
32/// use dbg_ranges::debug_adjacent;
33///
34/// assert_eq!(
35///     format!("{:?}", debug_adjacent(&[10u32, 12, 13, 14, 15, 20])),
36///     "10, 12-15, 20"
37/// );
38/// ```
39pub fn debug_adjacent<T: Debug + IsAdjacent>(items: &[T]) -> DebugAdjacent<T> {
40    DebugAdjacent::new(items)
41}
42
43/// Returns a value that implements `Debug` by collapsing runs of "adjacent" items.
44///
45/// The `is_adjacent` parameter defines whether two values in `T` are adjacent.
46pub fn debug_adjacent_by<T: Debug, F: Fn(&T, &T) -> bool>(
47    items: &[T],
48    is_adjacent: F,
49) -> DebugAdjacentBy<T, F> {
50    DebugAdjacentBy::new(items, is_adjacent)
51}
52
53macro_rules! int_successor {
54    ($t:ty) => {
55        impl IsAdjacent for $t {
56            fn is_adjacent(&self, other: &Self) -> bool {
57                other.checked_sub(*self) == Some(1)
58            }
59        }
60    };
61}
62int_successor!(u8);
63int_successor!(u16);
64int_successor!(u32);
65int_successor!(u64);
66int_successor!(u128);
67int_successor!(i8);
68int_successor!(i16);
69int_successor!(i32);
70int_successor!(i64);
71int_successor!(i128);
72
73impl IsAdjacent for char {
74    fn is_adjacent(&self, next: &Self) -> bool {
75        if let Some(after_self) = (*self as u32).checked_add(1) {
76            if let Some(after_self) = char::from_u32(after_self) {
77                after_self == *next
78            } else {
79                false
80            }
81        } else {
82            false
83        }
84    }
85}
86
87/// Checks whether an item is "adjacent" to another item.
88///
89/// ```
90/// use dbg_ranges::IsAdjacent;
91///
92/// assert!(4.is_adjacent(&5));
93/// ```
94pub trait IsAdjacent {
95    /// Returns `true` if `self` is adjacent to `other`.
96    fn is_adjacent(&self, other: &Self) -> bool;
97}
98
99/// Displays a list of integers. If the list contains sequences of contiguous (increasing) values
100/// then these will be displayed using `start-end` notation, rather than displaying each value.
101///
102/// The user of this type provides a function which indicates whether items are "adjacent" or not.
103#[derive(Copy, Clone)]
104pub struct DebugAdjacent<'a, T> {
105    /// The items that will be displayed
106    pub items: &'a [T],
107
108    /// The separator between the first and last item in a range.
109    pub sep: &'a str,
110}
111
112impl<'a, T> DebugAdjacent<'a, T> {
113    /// Constructor
114    pub fn new(items: &'a [T]) -> Self {
115        Self { items, sep: "-" }
116    }
117}
118
119impl<'a, T> Debug for DebugAdjacent<'a, T>
120where
121    T: Debug + IsAdjacent,
122{
123    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
124        let mut need_comma = false;
125
126        let mut iter = self.items.iter().peekable();
127
128        while let Some(first) = iter.next() {
129            if need_comma {
130                f.write_str(", ")?;
131            }
132            need_comma = true;
133
134            let mut this: &T = first;
135            let mut last: Option<&T> = None;
136
137            while let Some(&next) = iter.peek() {
138                if this.is_adjacent(next) {
139                    this = next;
140                    last = Some(next);
141                    _ = iter.next();
142                } else {
143                    break;
144                }
145            }
146
147            if let Some(last) = last {
148                <T as Debug>::fmt(first, f)?;
149                f.write_str(self.sep)?;
150                <T as Debug>::fmt(last, f)?;
151            } else {
152                <T as Debug>::fmt(first, f)?;
153            }
154        }
155
156        Ok(())
157    }
158}
159
160/// Displays a list of integers. If the list contains sequences of contiguous (increasing) values
161/// then these will be displayed using `start-end` notation, rather than displaying each value.
162///
163/// The user of this type provides a function which indicates whether items are "adjacent" or not.
164#[derive(Copy, Clone)]
165pub struct DebugAdjacentBy<'a, T, F> {
166    /// The items that will be displayed
167    pub items: &'a [T],
168    /// The separator between the first and last item in a range.
169    pub sep: &'a str,
170
171    /// The function that tests for adjacency
172    pub is_adjacent: F,
173}
174
175impl<'a, T, F> DebugAdjacentBy<'a, T, F> {
176    /// Constructor
177    pub fn new(items: &'a [T], is_adjacent: F) -> Self
178    where
179        F: Fn(&T, &T) -> bool,
180    {
181        Self {
182            items,
183            is_adjacent,
184            sep: "-",
185        }
186    }
187}
188
189impl<'a, T, F> Debug for DebugAdjacentBy<'a, T, F>
190where
191    T: Debug,
192    F: Fn(&T, &T) -> bool,
193{
194    fn fmt(&self, f: &mut Formatter) -> core::fmt::Result {
195        let mut need_comma = false;
196
197        let mut iter = self.items.iter().peekable();
198
199        while let Some(first) = iter.next() {
200            if need_comma {
201                f.write_str(", ")?;
202            }
203            need_comma = true;
204
205            let mut this: &T = first;
206            let mut last: Option<&T> = None;
207
208            while let Some(&next) = iter.peek() {
209                if (self.is_adjacent)(this, next) {
210                    this = next;
211                    last = Some(next);
212                    _ = iter.next();
213                } else {
214                    break;
215                }
216            }
217
218            if let Some(last) = last {
219                <T as Debug>::fmt(first, f)?;
220                f.write_str(self.sep)?;
221                <T as Debug>::fmt(last, f)?;
222            } else {
223                <T as Debug>::fmt(first, f)?;
224            }
225        }
226
227        Ok(())
228    }
229}
230
231#[test]
232fn test_dump_ranges() {
233    macro_rules! case {
234        ($input:expr, $expected_output:expr) => {
235            let input: &[_] = &$input;
236            let dump = DebugAdjacent::new(input);
237            let actual_output = format!("{:?}", dump);
238            println!("dump_ranges: {:?} --> {:?}", input, actual_output);
239            assert_eq!(
240                actual_output.as_str(),
241                $expected_output,
242                "input: {:?}",
243                input
244            );
245        };
246    }
247
248    case!([] as [u32; 0], "");
249    case!([10u32], "10");
250    case!([10u32, 20], "10, 20");
251    case!([10u32, 11, 20], "10-11, 20");
252    case!([10u32, 12, 13, 14, 15, 20], "10, 12-15, 20");
253    case!([u32::MAX, 42], "4294967295, 42");
254    case!([i32::MIN, i32::MIN + 1, 42], "-2147483648--2147483647, 42");
255}
256
257#[test]
258fn test_dump_ranges_by() {
259    macro_rules! case {
260        ($input:expr, $expected_output:expr) => {
261            let input: &[_] = &$input;
262            let dump = DebugAdjacentBy::new(input, |&a, &b| a + 1 == b);
263            let actual_output = format!("{:?}", dump);
264            println!("dump_ranges: {:?} --> {:?}", input, actual_output);
265            assert_eq!(
266                actual_output.as_str(),
267                $expected_output,
268                "input: {:?}",
269                input
270            );
271        };
272    }
273
274    case!([] as [u32; 0], "");
275    case!([10u32], "10");
276    case!([10u32, 20], "10, 20");
277    case!([10u32, 11, 20], "10-11, 20");
278    case!([10u32, 12, 13, 14, 15, 20], "10, 12-15, 20");
279}
280
281#[test]
282fn test_dump_ranges_by_swapped() {
283    macro_rules! case {
284        ($input:expr, $expected_output:expr) => {
285            let input: &[_] = &$input;
286            let dump = DebugAdjacentBy::new(input, |&a, &b| a + 1 == b);
287            let actual_output = format!("{:?}", dump);
288            println!("dump_ranges: {:?} --> {:?}", input, actual_output);
289            assert_eq!(
290                actual_output.as_str(),
291                $expected_output,
292                "input: {:?}",
293                input
294            );
295        };
296    }
297
298    case!([] as [u32; 0], "");
299    case!([10u32], "10");
300    case!([10u32, 20], "10, 20");
301    case!([10u32, 11, 20], "10-11, 20");
302    case!([10u32, 12, 13, 14, 15, 20], "10, 12-15, 20");
303}