vortex_array/array/display/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3//! Convert an array into a human-readable string representation.
4
5mod tree;
6
7use std::fmt::Display;
8
9use itertools::Itertools as _;
10use tree::TreeDisplayWrapper;
11use vortex_error::VortexExpect as _;
12
13use crate::Array;
14
15/// Describe how to convert an array to a string.
16///
17/// See also:
18/// [Array::display_as](../trait.Array.html#method.display_as)
19/// and [DisplayArrayAs].
20pub enum DisplayOptions {
21    /// Only the top-level encoding id and limited metadata: `vortex.primitive(i16, len=5)`.
22    ///
23    /// ```
24    /// # use vortex_array::display::DisplayOptions;
25    /// # use vortex_array::IntoArray;
26    /// # use vortex_buffer::buffer;
27    /// let array = buffer![0_i16, 1, 2, 3, 4].into_array();
28    /// assert_eq!(
29    ///     format!("{}", array.display_as(DisplayOptions::MetadataOnly)),
30    ///     "vortex.primitive(i16, len=5)",
31    /// );
32    /// ```
33    MetadataOnly,
34    /// Only the logical values of the array: `[0i16, 1i16, 2i16, 3i16, 4i16]`.
35    ///
36    /// ```
37    /// # use vortex_array::display::DisplayOptions;
38    /// # use vortex_array::IntoArray;
39    /// # use vortex_buffer::buffer;
40    /// let array = buffer![0_i16, 1, 2, 3, 4].into_array();
41    /// assert_eq!(
42    ///     format!("{}", array.display_as(DisplayOptions::default())),
43    ///     "[0i16, 1i16, 2i16, 3i16, 4i16]",
44    /// );
45    /// assert_eq!(
46    ///     format!("{}", array.display_as(DisplayOptions::default())),
47    ///     format!("{}", array.display_values()),
48    /// );
49    /// ```
50    CommaSeparatedScalars { omit_comma_after_space: bool },
51    /// The tree of encodings and all metadata but no values.
52    ///
53    /// ```
54    /// # use vortex_array::display::DisplayOptions;
55    /// # use vortex_array::IntoArray;
56    /// # use vortex_buffer::buffer;
57    /// let array = buffer![0_i16, 1, 2, 3, 4].into_array();
58    /// let expected = "root: vortex.primitive(i16, len=5) nbytes=10 B (100.00%)
59    ///   metadata: EmptyMetadata
60    ///   buffer (align=2): 10 B (100.00%)
61    /// ";
62    /// assert_eq!(format!("{}", array.display_as(DisplayOptions::TreeDisplay)), expected);
63    /// ```
64    TreeDisplay,
65    /// Display values in a formatted table with columns.
66    ///
67    /// For struct arrays, displays a column for each field in the struct.
68    /// For regular arrays, displays a single column with values.
69    ///
70    /// ```
71    /// # use vortex_array::display::DisplayOptions;
72    /// # use vortex_array::arrays::StructArray;
73    /// # use vortex_array::IntoArray;
74    /// # use vortex_buffer::buffer;
75    /// let s = StructArray::from_fields(&[
76    ///     ("x", buffer![1, 2].into_array()),
77    ///     ("y", buffer![3, 4].into_array()),
78    /// ]).unwrap().into_array();
79    /// let expected = "
80    /// ┌──────┬──────┐
81    /// │  x   │  y   │
82    /// ├──────┼──────┤
83    /// │ 1i32 │ 3i32 │
84    /// ├──────┼──────┤
85    /// │ 2i32 │ 4i32 │
86    /// └──────┴──────┘".trim();
87    /// assert_eq!(format!("{}", s.display_as(DisplayOptions::TableDisplay)), expected);
88    /// ```
89    #[cfg(feature = "table-display")]
90    TableDisplay,
91}
92
93impl Default for DisplayOptions {
94    fn default() -> Self {
95        Self::CommaSeparatedScalars {
96            omit_comma_after_space: false,
97        }
98    }
99}
100
101/// A shim used to display an array as specified in the options.
102///
103/// See also:
104/// [Array::display_as](../trait.Array.html#method.display_as)
105/// and [DisplayOptions].
106pub struct DisplayArrayAs<'a>(pub &'a dyn Array, pub DisplayOptions);
107
108impl Display for DisplayArrayAs<'_> {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        self.0.fmt_as(f, &self.1)
111    }
112}
113
114/// Display the encoding and limited metadata of this array.
115///
116/// # Examples
117/// ```
118/// # use vortex_array::IntoArray;
119/// # use vortex_buffer::buffer;
120/// let array = buffer![0_i16, 1, 2, 3, 4].into_array();
121/// assert_eq!(
122///     format!("{}", array),
123///     "vortex.primitive(i16, len=5)",
124/// );
125/// ```
126impl Display for dyn Array + '_ {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        self.fmt_as(f, &DisplayOptions::MetadataOnly)
129    }
130}
131
132impl dyn Array + '_ {
133    /// Display logical values of the array
134    ///
135    /// For example, an `i16` typed array containing the first five non-negative integers is displayed
136    /// as: `[0i16, 1i16, 2i16, 3i16, 4i16]`.
137    ///
138    /// # Examples
139    ///
140    /// ```
141    /// # use vortex_array::IntoArray;
142    /// # use vortex_buffer::buffer;
143    /// let array = buffer![0_i16, 1, 2, 3, 4].into_array();
144    /// assert_eq!(
145    ///     format!("{}", array.display_values()),
146    ///     "[0i16, 1i16, 2i16, 3i16, 4i16]",
147    /// )
148    /// ```
149    ///
150    /// See also:
151    /// [Array::display_as](..//trait.Array.html#method.display_as),
152    /// [DisplayArrayAs], and [DisplayOptions].
153    pub fn display_values(&self) -> DisplayArrayAs<'_> {
154        DisplayArrayAs(
155            self,
156            DisplayOptions::CommaSeparatedScalars {
157                omit_comma_after_space: false,
158            },
159        )
160    }
161
162    /// Display the array as specified by the options.
163    ///
164    /// See [DisplayOptions] for examples.
165    pub fn display_as(&self, options: DisplayOptions) -> DisplayArrayAs<'_> {
166        DisplayArrayAs(self, options)
167    }
168
169    /// Display the tree of encodings of this array as an indented lists.
170    ///
171    /// While some metadata (such as length, bytes and validity-rate) are included, the logical
172    /// values of the array are not displayed. To view the logical values see
173    /// [Array::display_as](../trait.Array.html#method.display_as)
174    /// and [DisplayOptions].
175    ///
176    /// # Examples
177    /// ```
178    /// # use vortex_array::display::DisplayOptions;
179    /// # use vortex_array::IntoArray;
180    /// # use vortex_buffer::buffer;
181    /// let array = buffer![0_i16, 1, 2, 3, 4].into_array();
182    /// let expected = "root: vortex.primitive(i16, len=5) nbytes=10 B (100.00%)
183    ///   metadata: EmptyMetadata
184    ///   buffer (align=2): 10 B (100.00%)
185    /// ";
186    /// assert_eq!(format!("{}", array.display_tree()), expected);
187    /// ```
188    pub fn display_tree(&self) -> impl Display {
189        DisplayArrayAs(self, DisplayOptions::TreeDisplay)
190    }
191
192    /// Display the array as a formatted table.
193    ///
194    /// For struct arrays, displays a column for each field in the struct.
195    /// For regular arrays, displays a single column with values.
196    ///
197    /// # Examples
198    /// ```
199    /// # #[cfg(feature = "table-display")]
200    /// # {
201    /// # use vortex_array::arrays::StructArray;
202    /// # use vortex_array::IntoArray;
203    /// # use vortex_buffer::buffer;
204    /// let s = StructArray::from_fields(&[
205    ///     ("x", buffer![1, 2].into_array()),
206    ///     ("y", buffer![3, 4].into_array()),
207    /// ]).unwrap().into_array();
208    /// let expected = "
209    /// ┌──────┬──────┐
210    /// │  x   │  y   │
211    /// ├──────┼──────┤
212    /// │ 1i32 │ 3i32 │
213    /// ├──────┼──────┤
214    /// │ 2i32 │ 4i32 │
215    /// └──────┴──────┘".trim();
216    /// assert_eq!(format!("{}", s.display_table()), expected);
217    /// # }
218    /// ```
219    #[cfg(feature = "table-display")]
220    pub fn display_table(&self) -> impl Display {
221        DisplayArrayAs(self, DisplayOptions::TableDisplay)
222    }
223
224    fn fmt_as(&self, f: &mut std::fmt::Formatter, options: &DisplayOptions) -> std::fmt::Result {
225        match options {
226            DisplayOptions::MetadataOnly => {
227                write!(
228                    f,
229                    "{}({}, len={})",
230                    self.encoding_id(),
231                    self.dtype(),
232                    self.len()
233                )
234            }
235            DisplayOptions::CommaSeparatedScalars {
236                omit_comma_after_space,
237            } => {
238                write!(f, "[")?;
239                let sep = if *omit_comma_after_space { "," } else { ", " };
240                write!(
241                    f,
242                    "{}",
243                    (0..self.len())
244                        .map(|i| self.scalar_at(i).vortex_expect("index is in bounds"))
245                        .format(sep)
246                )?;
247                write!(f, "]")
248            }
249            DisplayOptions::TreeDisplay => write!(f, "{}", TreeDisplayWrapper(self.to_array())),
250            #[cfg(feature = "table-display")]
251            DisplayOptions::TableDisplay => {
252                use crate::canonical::ToCanonical;
253                let mut builder = tabled::builder::Builder::default();
254
255                let table = match self.dtype() {
256                    vortex_dtype::DType::Struct(sf, _) => {
257                        let struct_ = self.to_struct().vortex_expect("struct array");
258                        builder.push_record(sf.names().iter().map(|name| name.to_string()));
259
260                        for row_idx in 0..self.len() {
261                            if !self.is_valid(row_idx).vortex_expect("index in bounds") {
262                                let null_row = vec!["null".to_string(); sf.names().len()];
263                                builder.push_record(null_row);
264                            } else {
265                                let mut row = Vec::new();
266                                for field_array in struct_.fields() {
267                                    let value = field_array
268                                        .scalar_at(row_idx)
269                                        .vortex_expect("index in bounds");
270                                    row.push(value.to_string());
271                                }
272                                builder.push_record(row);
273                            }
274                        }
275
276                        let mut table = builder.build();
277                        table.with(tabled::settings::Style::modern());
278
279                        // Center headers
280                        for col_idx in 0..sf.names().len() {
281                            table.modify((0, col_idx), tabled::settings::Alignment::center());
282                        }
283
284                        for row_idx in 0..self.len() {
285                            if !self.is_valid(row_idx).vortex_expect("index is in bounds") {
286                                table.modify(
287                                    (1 + row_idx, 0),
288                                    tabled::settings::Span::column(sf.names().len() as isize),
289                                );
290                                table.modify(
291                                    (1 + row_idx, 0),
292                                    tabled::settings::Alignment::center(),
293                                );
294                            }
295                        }
296                        table
297                    }
298                    _ => {
299                        // For non-struct arrays, display a single column table without header
300                        for row_idx in 0..self.len() {
301                            let value = self.scalar_at(row_idx).vortex_expect("index is in bounds");
302                            builder.push_record([value.to_string()]);
303                        }
304
305                        let mut table = builder.build();
306                        table.with(tabled::settings::Style::modern());
307
308                        table
309                    }
310                };
311                write!(f, "{table}")
312            }
313        }
314    }
315}
316
317#[cfg(test)]
318mod test {
319    use vortex_buffer::{Buffer, buffer};
320    use vortex_dtype::FieldNames;
321
322    use crate::IntoArray as _;
323    use crate::arrays::{BoolArray, ListArray, StructArray};
324    use crate::validity::Validity;
325
326    #[test]
327    fn test_primitive() {
328        let x = Buffer::<u32>::empty().into_array();
329        assert_eq!(x.display_values().to_string(), "[]");
330
331        let x = buffer![1].into_array();
332        assert_eq!(x.display_values().to_string(), "[1i32]");
333
334        let x = buffer![1, 2, 3, 4].into_array();
335        assert_eq!(x.display_values().to_string(), "[1i32, 2i32, 3i32, 4i32]");
336    }
337
338    #[test]
339    fn test_empty_struct() {
340        let s = StructArray::try_new(
341            FieldNames::from(vec![]),
342            vec![],
343            3,
344            Validity::Array(BoolArray::from_iter([true, false, true]).into_array()),
345        )
346        .unwrap()
347        .into_array();
348        assert_eq!(s.display_values().to_string(), "[{}, null, {}]");
349    }
350
351    #[test]
352    fn test_simple_struct() {
353        let s = StructArray::from_fields(&[
354            ("x", buffer![1, 2, 3, 4].into_array()),
355            ("y", buffer![-1, -2, -3, -4].into_array()),
356        ])
357        .unwrap()
358        .into_array();
359        assert_eq!(
360            s.display_values().to_string(),
361            "[{x: 1i32, y: -1i32}, {x: 2i32, y: -2i32}, {x: 3i32, y: -3i32}, {x: 4i32, y: -4i32}]"
362        );
363    }
364
365    #[test]
366    fn test_list() {
367        let x = ListArray::try_new(
368            buffer![1, 2, 3, 4].into_array(),
369            buffer![0, 0, 1, 1, 2, 4].into_array(),
370            Validity::Array(BoolArray::from_iter([true, true, false, true, true]).into_array()),
371        )
372        .unwrap()
373        .into_array();
374        assert_eq!(
375            x.display_values().to_string(),
376            "[[], [1i32], null, [2i32], [3i32, 4i32]]"
377        );
378    }
379
380    #[test]
381    #[cfg(feature = "table-display")]
382    fn test_table_display_primitive() {
383        use crate::display::DisplayOptions;
384
385        let array = buffer![1, 2, 3, 4].into_array();
386        let table_display = array.display_as(DisplayOptions::TableDisplay);
387        assert_eq!(
388            table_display.to_string(),
389            r"
390┌──────┐
391│ 1i32 │
392├──────┤
393│ 2i32 │
394├──────┤
395│ 3i32 │
396├──────┤
397│ 4i32 │
398└──────┘"
399                .trim()
400        );
401    }
402
403    #[test]
404    #[cfg(feature = "table-display")]
405    fn test_table_display() {
406        use crate::display::DisplayOptions;
407
408        let array = crate::arrays::PrimitiveArray::from_option_iter(vec![
409            Some(-1),
410            Some(-2),
411            Some(-3),
412            None,
413        ])
414        .into_array();
415
416        let struct_ = StructArray::try_from_iter_with_validity(
417            [("x", buffer![1, 2, 3, 4].into_array()), ("y", array)],
418            Validity::Array(BoolArray::from_iter([true, false, true, true]).into_array()),
419        )
420        .unwrap()
421        .into_array();
422
423        let table_display = struct_.display_as(DisplayOptions::TableDisplay);
424        assert_eq!(
425            table_display.to_string(),
426            r"
427┌──────┬───────┐
428│  x   │   y   │
429├──────┼───────┤
430│ 1i32 │ -1i32 │
431├──────┼───────┤
432│     null     │
433├──────┼───────┤
434│ 3i32 │ -3i32 │
435├──────┼───────┤
436│ 4i32 │ null  │
437└──────┴───────┘"
438                .trim()
439        );
440    }
441}