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}