Skip to main content

diskann_benchmark_runner/utils/
fmt.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use std::{
7    collections::HashMap,
8    fmt::{Display, Write},
9};
10
11/// A 2-d table for formatting properly spaced values in a table.
12pub struct Table {
13    // The number of columns is implicitly described by the number of entries in `header`.
14    header: Box<[Box<dyn Display>]>,
15    body: HashMap<(usize, usize), Box<dyn Display>>,
16    nrows: usize,
17}
18
19impl Table {
20    pub fn new<I>(header: I, nrows: usize) -> Self
21    where
22        I: IntoIterator<Item: Display + 'static>,
23    {
24        fn as_dyn_display<T: Display + 'static>(x: T) -> Box<dyn Display> {
25            Box::new(x)
26        }
27
28        let header: Box<[_]> = header.into_iter().map(as_dyn_display).collect();
29        Self {
30            header,
31            body: HashMap::new(),
32            nrows,
33        }
34    }
35
36    pub fn nrows(&self) -> usize {
37        self.nrows
38    }
39
40    pub fn ncols(&self) -> usize {
41        self.header.len()
42    }
43
44    pub fn insert<T>(&mut self, item: T, row: usize, col: usize) -> bool
45    where
46        T: Display + 'static,
47    {
48        self.check_bounds(row, col);
49        self.body.insert((row, col), Box::new(item)).is_some()
50    }
51
52    pub fn get(&self, row: usize, col: usize) -> Option<&dyn Display> {
53        self.check_bounds(row, col);
54        self.body.get(&(row, col)).map(|x| &**x)
55    }
56
57    pub fn row(&mut self, row: usize) -> Row<'_> {
58        self.check_bounds(row, 0);
59        Row::new(self, row)
60    }
61
62    #[expect(clippy::panic, reason = "table interfaces are bounds checked")]
63    fn check_bounds(&self, row: usize, col: usize) {
64        if row >= self.nrows() {
65            panic!("row {} is out of bounds (max {})", row, self.nrows());
66        }
67        if col >= self.ncols() {
68            panic!("col {} is out of bounds (max {})", col, self.ncols());
69        }
70    }
71}
72
73pub struct Row<'a> {
74    table: &'a mut Table,
75    row: usize,
76}
77
78impl<'a> Row<'a> {
79    // A **private** constructor assuming that `row` is inbounds.
80    fn new(table: &'a mut Table, row: usize) -> Self {
81        Self { table, row }
82    }
83
84    /// Insert a value into the specified column of this row.
85    pub fn insert<T>(&mut self, item: T, col: usize) -> bool
86    where
87        T: Display + 'static,
88    {
89        self.table.insert(item, self.row, col)
90    }
91}
92
93impl Display for Table {
94    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95        const SEP: &str = ",   ";
96
97        // Compute the maximum width of each column.
98        struct Count(usize);
99
100        impl Write for Count {
101            fn write_str(&mut self, s: &str) -> std::fmt::Result {
102                self.0 += s.len();
103                Ok(())
104            }
105        }
106
107        fn formatted_size<T>(x: &T) -> usize
108        where
109            T: Display + ?Sized,
110        {
111            let mut buf = Count(0);
112            match write!(&mut buf, "{}", x) {
113                // Return the number of bytes "written",
114                Ok(()) => buf.0,
115                Err(_) => 0,
116            }
117        }
118
119        let mut widths: Vec<usize> = self.header.iter().map(formatted_size).collect();
120        for row in 0..self.nrows() {
121            for (col, width) in widths.iter_mut().enumerate() {
122                if let Some(v) = self.body.get(&(row, col)) {
123                    *width = (*width).max(formatted_size(v))
124                }
125            }
126        }
127
128        let header_width: usize = widths.iter().sum::<usize>() + (widths.len() - 1) * SEP.len();
129
130        let mut buf = String::new();
131        // Print the header.
132        std::iter::zip(widths.iter(), self.header.iter())
133            .enumerate()
134            .try_for_each(|(col, (width, head))| {
135                buf.clear();
136                write!(buf, "{}", head)?;
137                write!(f, "{:>width$}", buf)?;
138                if col + 1 != self.ncols() {
139                    write!(f, "{}", SEP)?;
140                }
141                Ok(())
142            })?;
143
144        // Banner
145        write!(f, "\n{:=>header_width$}\n", "")?;
146
147        // Write out each row.
148        for row in 0..self.nrows() {
149            for (col, width) in widths.iter_mut().enumerate() {
150                match self.body.get(&(row, col)) {
151                    Some(v) => {
152                        buf.clear();
153                        write!(buf, "{}", v)?;
154                        write!(f, "{:>width$}", buf)?;
155                    }
156                    None => write!(f, "{:>width$}", "")?,
157                }
158                if col + 1 != self.ncols() {
159                    write!(f, "{}", SEP)?;
160                } else {
161                    writeln!(f)?;
162                }
163            }
164        }
165        Ok(())
166    }
167}
168
169////////////
170// Banner //
171////////////
172
173pub(crate) struct Banner<'a>(&'a str);
174
175impl<'a> Banner<'a> {
176    pub(crate) fn new(message: &'a str) -> Self {
177        Self(message)
178    }
179}
180
181impl std::fmt::Display for Banner<'_> {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        let st = format!("# {} #", self.0);
184        let len = st.len();
185        writeln!(f, "{:#>len$}", "")?;
186        writeln!(f, "{}", st)?;
187        writeln!(f, "{:#>len$}", "")?;
188        Ok(())
189    }
190}
191
192////////////
193// Indent //
194////////////
195
196/// Indents each line of a string by a fixed number of spaces.
197///
198/// Each line is prefixed with `spaces` spaces and terminated with a newline.
199///
200/// # Examples
201///
202/// ```
203/// use diskann_benchmark_runner::utils::fmt::Indent;
204///
205/// let indented = Indent::new("hello\nworld", 4).to_string();
206/// assert_eq!(indented, "    hello\n    world\n");
207/// ```
208#[derive(Debug, Clone, Copy)]
209pub struct Indent<'a> {
210    string: &'a str,
211    spaces: usize,
212}
213
214impl<'a> Indent<'a> {
215    /// Create a new [`Indent`] that will prefix each line of `string` with `spaces` spaces.
216    pub fn new(string: &'a str, spaces: usize) -> Self {
217        Self { string, spaces }
218    }
219}
220
221impl std::fmt::Display for Indent<'_> {
222    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
223        let spaces = self.spaces;
224        self.string
225            .lines()
226            .try_for_each(|ln| writeln!(f, "{: >spaces$}{}", "", ln))
227    }
228}
229
230/////////////
231// Delimit //
232/////////////
233
234/// Formats an iterator with a delimiter between items and optional overrides for
235/// the final delimiter and pair formatting.
236///
237/// This is a single-use wrapper: the iterator is consumed on the first call to [`Display::fmt`].
238/// Subsequent calls will print `<missing>`.
239///
240/// Use [`Delimit::with_last`] to change the delimiter before the final item
241/// (e.g., `", and "`), which is useful for natural-language lists like
242/// `"a, b, and c"`.
243///
244/// Use [`Delimit::with_pair`] to change formatting when there are only two items.
245///
246/// # Examples
247///
248/// ```
249/// use diskann_benchmark_runner::utils::fmt::Delimit;
250///
251/// let d = Delimit::new(["a", "b", "c"], ", ").with_last(", and ");
252/// assert_eq!(d.to_string(), "a, b, and c");
253///
254/// let d = Delimit::new(["a", "b"], ", ").with_last(", and ");
255/// assert_eq!(d.to_string(), "a, and b");
256///
257/// let d = Delimit::new(["a", "b"], ", ")
258///     .with_last(", and ")
259///     .with_pair(" and ");
260/// assert_eq!(d.to_string(), "a and b");
261/// ```
262pub struct Delimit<'a, I> {
263    itr: std::cell::Cell<Option<I>>,
264    delimiter: &'a str,
265    last: &'a str,
266    pair: Option<&'a str>,
267}
268
269impl<'a, I> Delimit<'a, I> {
270    /// Create a new [`Delimit`] from an iterable and a delimiter.
271    ///
272    /// By default, the same delimiter is used between every item. Use
273    /// [`Self::with_last`] and [`Self::with_pair`] to opt into special handling
274    /// before the final item or for pairs.
275    pub fn new(itr: impl IntoIterator<IntoIter = I>, delimiter: &'a str) -> Self {
276        Self {
277            itr: std::cell::Cell::new(Some(itr.into_iter())),
278            delimiter,
279            last: delimiter,
280            pair: None,
281        }
282    }
283
284    /// Use `last` before the final item when formatting three or more items.
285    pub fn with_last(mut self, last: &'a str) -> Self {
286        self.last = last;
287        self
288    }
289
290    /// Use `pair` when formatting exactly two items.
291    pub fn with_pair(mut self, pair: &'a str) -> Self {
292        self.pair = Some(pair);
293        self
294    }
295}
296
297impl<I> std::fmt::Display for Delimit<'_, I>
298where
299    I: Iterator<Item: std::fmt::Display>,
300{
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        let Some(mut itr) = self.itr.take() else {
303            return write!(f, "<missing>");
304        };
305
306        let mut count = 0;
307        let mut current = if let Some(item) = itr.next() {
308            item
309        } else {
310            // Empty iterator
311            return Ok(());
312        };
313
314        loop {
315            match itr.next() {
316                None => {
317                    // "current" is the last item. If it is also the first, we write it
318                    // directly.
319                    //
320                    // Otherwise, we check if we've just emitted a single item so far and
321                    // use `pair` if available. Otherwise, we try `last` and finally
322                    // `delimiter`.
323                    let delimiter = if count == 0 {
324                        ""
325                    } else if count == 1 {
326                        self.pair.unwrap_or(self.last)
327                    } else {
328                        self.last
329                    };
330
331                    return write!(f, "{}{}", delimiter, current);
332                }
333                Some(next) => {
334                    // There is at least one item next. We print "current" and move on.
335                    let delimiter = if count == 0 { "" } else { self.delimiter };
336
337                    write!(f, "{}{}", delimiter, current)?;
338                    count += 1;
339                    current = next;
340                }
341            }
342        }
343    }
344}
345
346///////////
347// Quote //
348///////////
349
350/// Wraps a value in double quotes when displayed.
351///
352/// # Examples
353///
354/// ```
355/// use diskann_benchmark_runner::utils::fmt::Quote;
356///
357/// assert_eq!(Quote("hello").to_string(), "\"hello\"");
358/// assert_eq!(Quote(42).to_string(), "\"42\"");
359/// ```
360#[derive(Debug, Clone, Copy)]
361pub struct Quote<T>(pub T);
362
363impl<T> std::fmt::Display for Quote<T>
364where
365    T: std::fmt::Display,
366{
367    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
368        write!(f, "\"{}\"", self.0)
369    }
370}
371
372///////////
373// Tests //
374///////////
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_banner() {
382        let b = Banner::new("hello world");
383        let s = b.to_string();
384
385        let expected = "###############\n\
386                        # hello world #\n\
387                        ###############\n";
388
389        assert_eq!(s, expected);
390
391        let b = Banner::new("");
392        let s = b.to_string();
393
394        let expected = "####\n\
395                        #  #\n\
396                        ####\n";
397
398        assert_eq!(s, expected);
399
400        let b = Banner::new("foo");
401        let s = b.to_string();
402
403        let expected = "#######\n\
404                        # foo #\n\
405                        #######\n";
406
407        assert_eq!(s, expected);
408    }
409
410    #[test]
411    fn test_format() {
412        // One column
413        {
414            let headers = ["h 0"];
415            let mut table = Table::new(headers, 3);
416            table.insert("a", 0, 0);
417            table.insert("hello world", 1, 0);
418            table.insert(62, 2, 0);
419
420            let s = table.to_string();
421            let expected = r#"
422        h 0
423===========
424          a
425hello world
426         62
427"#;
428            assert_eq!(s, expected.strip_prefix('\n').unwrap());
429        }
430
431        // Two columns
432        {
433            let headers = ["a really really long header", "h1"];
434            let mut table = Table::new(headers, 3);
435            table.insert("a", 0, 0);
436            table.insert("b", 0, 1);
437
438            table.insert("hello world", 1, 0);
439            table.insert("hello world version 2", 1, 1);
440
441            table.insert(7, 2, 0);
442            table.insert("bar", 2, 1);
443
444            let s = table.to_string();
445            let expected = r#"
446a really really long header,                      h1
447====================================================
448                          a,                       b
449                hello world,   hello world version 2
450                          7,                     bar
451"#;
452            assert_eq!(s, expected.strip_prefix('\n').unwrap());
453        }
454    }
455
456    #[test]
457    fn test_row_api() {
458        let mut table = Table::new(["a", "b", "c"], 2);
459        let mut row = table.row(0);
460        row.insert(1, 0);
461        row.insert("long", 1);
462        row.insert("s", 2);
463
464        let mut row = table.row(1);
465        row.insert("string", 0);
466        row.insert(2, 1);
467        row.insert(3, 2);
468
469        let s = table.to_string();
470
471        let expected = r#"
472     a,      b,   c
473===================
474     1,   long,   s
475string,      2,   3
476"#;
477        assert_eq!(s, expected.strip_prefix('\n').unwrap());
478    }
479
480    #[test]
481    fn missing_values() {
482        let mut table = Table::new(["a", "loong", "c"], 1);
483        let mut row = table.row(0);
484        row.insert("string", 0);
485        row.insert("string", 2);
486
487        let s = table.to_string();
488        let expected = r#"
489     a,   loong,        c
490=========================
491string,        ,   string
492"#;
493        assert_eq!(s, expected.strip_prefix('\n').unwrap());
494    }
495
496    #[test]
497    #[should_panic(expected = "row 3 is out of bounds (max 2)")]
498    fn test_panic_row() {
499        let mut table = Table::new([1, 2, 3], 2);
500        let _ = table.row(3);
501    }
502
503    #[test]
504    #[should_panic(expected = "col 3 is out of bounds (max 2)")]
505    fn test_panic_col() {
506        let mut table = Table::new([1, 2], 1);
507        let mut row = table.row(0);
508        row.insert(1, 3);
509    }
510
511    #[test]
512    fn test_indent_single_line() {
513        let s = Indent::new("hello", 4).to_string();
514        assert_eq!(s, "    hello\n");
515    }
516
517    #[test]
518    fn test_indent_multi_line() {
519        let s = Indent::new("hello\nworld\nfoo", 2).to_string();
520        assert_eq!(s, "  hello\n  world\n  foo\n");
521    }
522
523    #[test]
524    fn test_indent_zero_spaces() {
525        let s = Indent::new("hello\nworld", 0).to_string();
526        assert_eq!(s, "hello\nworld\n");
527    }
528
529    #[test]
530    fn test_indent_empty_string() {
531        let s = Indent::new("", 4).to_string();
532        assert_eq!(s, "");
533    }
534
535    #[test]
536    fn test_delimit_empty() {
537        let d = Delimit::new(std::iter::empty::<&str>(), ", ");
538        assert_eq!(d.to_string(), "");
539    }
540
541    #[test]
542    fn test_delimit_single_item() {
543        let d = Delimit::new(["a"], ", ").with_last(", and ");
544        assert_eq!(d.to_string(), "a");
545    }
546
547    #[test]
548    fn test_delimit_two_items_with_last() {
549        let d = Delimit::new(["a", "b"], ", ").with_last(", and ");
550        assert_eq!(d.to_string(), "a, and b");
551    }
552
553    #[test]
554    fn test_delimit_two_items_with_pair() {
555        let d = Delimit::new(["a", "b"], ", ")
556            .with_last(", and ")
557            .with_pair(" and ");
558        assert_eq!(d.to_string(), "a and b");
559    }
560
561    #[test]
562    fn test_delimit_three_items_with_last() {
563        let d = Delimit::new(["a", "b", "c"], ", ")
564            .with_last(", and ")
565            .with_pair(" and ");
566        assert_eq!(d.to_string(), "a, b, and c");
567    }
568
569    #[test]
570    fn test_delimit_without_last() {
571        let d = Delimit::new(["x", "y", "z"], " | ");
572        assert_eq!(d.to_string(), "x | y | z");
573    }
574
575    #[test]
576    fn test_delimit_second_display_prints_missing() {
577        let d = Delimit::new(["a", "b"], ", ");
578        assert_eq!(d.to_string(), "a, b");
579        assert_eq!(d.to_string(), "<missing>");
580    }
581
582    #[test]
583    fn test_quote() {
584        assert_eq!(Quote("hello").to_string(), "\"hello\"");
585    }
586
587    #[test]
588    fn test_quote_with_integer() {
589        assert_eq!(Quote(42).to_string(), "\"42\"");
590    }
591
592    #[test]
593    fn test_delimit_with_quote() {
594        let d = Delimit::new(["topk", "range"].iter().map(Quote), ", ")
595            .with_last(", and ")
596            .with_pair(" and ");
597        assert_eq!(d.to_string(), "\"topk\" and \"range\"");
598    }
599}