Skip to main content

alimentar/tui/
format.rs

1//! Cell formatting utilities for TUI display
2//!
3//! Provides safe formatting of Arrow array values to display strings.
4
5use arrow::{
6    array::{
7        Array, BinaryArray, BooleanArray, Date32Array, Date64Array, Float32Array, Float64Array,
8        Int16Array, Int32Array, Int64Array, Int8Array, LargeBinaryArray, LargeStringArray,
9        StringArray, TimestampMicrosecondArray, TimestampMillisecondArray,
10        TimestampNanosecondArray, TimestampSecondArray, UInt16Array, UInt32Array, UInt64Array,
11        UInt8Array,
12    },
13    datatypes::DataType,
14};
15
16use super::error::TuiResult;
17
18/// Format an Arrow array value at the given row index as a display string
19///
20/// Returns `None` if the value is null, `Some(formatted_string)` otherwise.
21/// Returns an error only for truly unsupported types.
22///
23/// # Arguments
24/// * `array` - The Arrow array to read from
25/// * `row` - The row index within the array
26///
27/// # Example
28/// ```ignore
29/// let array = StringArray::from(vec!["hello", "world"]);
30/// let formatted = format_array_value(&array, 0)?;
31/// assert_eq!(formatted, Some("hello".to_string()));
32/// ```
33pub fn format_array_value(array: &dyn Array, row: usize) -> TuiResult<Option<String>> {
34    // Check bounds first
35    if row >= array.len() {
36        return Ok(None);
37    }
38
39    // Handle null values
40    if array.is_null(row) {
41        return Ok(Some("NULL".to_string()));
42    }
43
44    let formatted = match array.data_type() {
45        // String types
46        DataType::Utf8 => format_utf8(array, row),
47        DataType::LargeUtf8 => format_large_utf8(array, row),
48
49        // Integer types
50        DataType::Int8 => format_int8(array, row),
51        DataType::Int16 => format_int16(array, row),
52        DataType::Int32 => format_int32(array, row),
53        DataType::Int64 => format_int64(array, row),
54        DataType::UInt8 => format_uint8(array, row),
55        DataType::UInt16 => format_uint16(array, row),
56        DataType::UInt32 => format_uint32(array, row),
57        DataType::UInt64 => format_uint64(array, row),
58
59        // Float types
60        DataType::Float32 => format_float32(array, row),
61        DataType::Float64 => format_float64(array, row),
62
63        // Boolean
64        DataType::Boolean => format_boolean(array, row),
65
66        // Binary types
67        DataType::Binary => format_binary(array, row),
68        DataType::LargeBinary => format_large_binary(array, row),
69
70        // Date types
71        DataType::Date32 => format_date32(array, row),
72        DataType::Date64 => format_date64(array, row),
73
74        // Timestamp types
75        DataType::Timestamp(unit, _) => format_timestamp(array, row, *unit),
76
77        // Null type
78        DataType::Null => Some("NULL".to_string()),
79
80        // Unsupported types - return placeholder
81        other => Some(format!("<{}>", type_name(other))),
82    };
83
84    Ok(formatted)
85}
86
87/// Get a human-readable type name
88fn type_name(dt: &DataType) -> &'static str {
89    match dt {
90        DataType::Null => "null",
91        DataType::Boolean => "bool",
92        DataType::Int8 => "i8",
93        DataType::Int16 => "i16",
94        DataType::Int32 => "i32",
95        DataType::Int64 => "i64",
96        DataType::UInt8 => "u8",
97        DataType::UInt16 => "u16",
98        DataType::UInt32 => "u32",
99        DataType::UInt64 => "u64",
100        DataType::Float32 => "f32",
101        DataType::Float64 => "f64",
102        DataType::Utf8 => "string",
103        DataType::LargeUtf8 => "large_string",
104        DataType::Binary => "binary",
105        DataType::LargeBinary => "large_binary",
106        DataType::Date32 => "date32",
107        DataType::Date64 => "date64",
108        DataType::Timestamp(_, _) => "timestamp",
109        DataType::List(_) => "list",
110        DataType::LargeList(_) => "large_list",
111        DataType::Struct(_) => "struct",
112        DataType::Map(_, _) => "map",
113        DataType::Dictionary(_, _) => "dict",
114        _ => "unknown",
115    }
116}
117
118// Individual format functions for each type
119
120fn format_utf8(array: &dyn Array, row: usize) -> Option<String> {
121    array
122        .as_any()
123        .downcast_ref::<StringArray>()
124        .map(|arr| arr.value(row).to_string())
125}
126
127fn format_large_utf8(array: &dyn Array, row: usize) -> Option<String> {
128    array
129        .as_any()
130        .downcast_ref::<LargeStringArray>()
131        .map(|arr| arr.value(row).to_string())
132}
133
134fn format_int8(array: &dyn Array, row: usize) -> Option<String> {
135    array
136        .as_any()
137        .downcast_ref::<Int8Array>()
138        .map(|arr| arr.value(row).to_string())
139}
140
141fn format_int16(array: &dyn Array, row: usize) -> Option<String> {
142    array
143        .as_any()
144        .downcast_ref::<Int16Array>()
145        .map(|arr| arr.value(row).to_string())
146}
147
148fn format_int32(array: &dyn Array, row: usize) -> Option<String> {
149    array
150        .as_any()
151        .downcast_ref::<Int32Array>()
152        .map(|arr| arr.value(row).to_string())
153}
154
155fn format_int64(array: &dyn Array, row: usize) -> Option<String> {
156    array
157        .as_any()
158        .downcast_ref::<Int64Array>()
159        .map(|arr| arr.value(row).to_string())
160}
161
162fn format_uint8(array: &dyn Array, row: usize) -> Option<String> {
163    array
164        .as_any()
165        .downcast_ref::<UInt8Array>()
166        .map(|arr| arr.value(row).to_string())
167}
168
169fn format_uint16(array: &dyn Array, row: usize) -> Option<String> {
170    array
171        .as_any()
172        .downcast_ref::<UInt16Array>()
173        .map(|arr| arr.value(row).to_string())
174}
175
176fn format_uint32(array: &dyn Array, row: usize) -> Option<String> {
177    array
178        .as_any()
179        .downcast_ref::<UInt32Array>()
180        .map(|arr| arr.value(row).to_string())
181}
182
183fn format_uint64(array: &dyn Array, row: usize) -> Option<String> {
184    array
185        .as_any()
186        .downcast_ref::<UInt64Array>()
187        .map(|arr| arr.value(row).to_string())
188}
189
190fn format_float32(array: &dyn Array, row: usize) -> Option<String> {
191    array
192        .as_any()
193        .downcast_ref::<Float32Array>()
194        .map(|arr| format!("{:.2}", arr.value(row)))
195}
196
197fn format_float64(array: &dyn Array, row: usize) -> Option<String> {
198    array
199        .as_any()
200        .downcast_ref::<Float64Array>()
201        .map(|arr| format!("{:.4}", arr.value(row)))
202}
203
204fn format_boolean(array: &dyn Array, row: usize) -> Option<String> {
205    array
206        .as_any()
207        .downcast_ref::<BooleanArray>()
208        .map(|arr| if arr.value(row) { "true" } else { "false" }.to_string())
209}
210
211fn format_binary(array: &dyn Array, row: usize) -> Option<String> {
212    array.as_any().downcast_ref::<BinaryArray>().map(|arr| {
213        let bytes = arr.value(row);
214        format_bytes_preview(bytes)
215    })
216}
217
218fn format_large_binary(array: &dyn Array, row: usize) -> Option<String> {
219    array
220        .as_any()
221        .downcast_ref::<LargeBinaryArray>()
222        .map(|arr| {
223            let bytes = arr.value(row);
224            format_bytes_preview(bytes)
225        })
226}
227
228/// Format binary data as hex preview
229fn format_bytes_preview(bytes: &[u8]) -> String {
230    if bytes.len() <= 8 {
231        format!("0x{}", hex_encode(bytes))
232    } else {
233        format!("0x{}... ({} bytes)", hex_encode(&bytes[..8]), bytes.len())
234    }
235}
236
237/// Simple hex encoding without external dependency
238fn hex_encode(bytes: &[u8]) -> String {
239    use std::fmt::Write;
240    let mut result = String::with_capacity(bytes.len() * 2);
241    for b in bytes {
242        let _ = write!(result, "{b:02x}");
243    }
244    result
245}
246
247fn format_date32(array: &dyn Array, row: usize) -> Option<String> {
248    array.as_any().downcast_ref::<Date32Array>().map(|arr| {
249        let days = arr.value(row);
250        // Days since Unix epoch
251        format!("date:{days}")
252    })
253}
254
255fn format_date64(array: &dyn Array, row: usize) -> Option<String> {
256    array.as_any().downcast_ref::<Date64Array>().map(|arr| {
257        let millis = arr.value(row);
258        format!("date64:{millis}")
259    })
260}
261
262fn format_timestamp(
263    array: &dyn Array,
264    row: usize,
265    unit: arrow::datatypes::TimeUnit,
266) -> Option<String> {
267    use arrow::datatypes::TimeUnit;
268
269    match unit {
270        TimeUnit::Second => array
271            .as_any()
272            .downcast_ref::<TimestampSecondArray>()
273            .map(|arr| format!("ts:{}", arr.value(row))),
274        TimeUnit::Millisecond => array
275            .as_any()
276            .downcast_ref::<TimestampMillisecondArray>()
277            .map(|arr| format!("ts:{}", arr.value(row))),
278        TimeUnit::Microsecond => array
279            .as_any()
280            .downcast_ref::<TimestampMicrosecondArray>()
281            .map(|arr| format!("ts:{}", arr.value(row))),
282        TimeUnit::Nanosecond => array
283            .as_any()
284            .downcast_ref::<TimestampNanosecondArray>()
285            .map(|arr| format!("ts:{}", arr.value(row))),
286    }
287}
288
289/// Truncate a string to fit within a maximum display width
290///
291/// Handles Unicode properly by counting graphemes, not bytes.
292/// Adds ellipsis if truncation occurs.
293///
294/// # Arguments
295/// * `s` - The string to truncate
296/// * `max_width` - Maximum display width in characters
297///
298/// # Returns
299/// The truncated string, or the original if it fits
300pub fn truncate_string(s: &str, max_width: usize) -> String {
301    if max_width < 3 {
302        return s.chars().take(max_width).collect();
303    }
304
305    let char_count = s.chars().count();
306    if char_count <= max_width {
307        return s.to_string();
308    }
309
310    // Reserve space for ellipsis
311    let truncate_at = max_width.saturating_sub(2);
312    let mut result: String = s.chars().take(truncate_at).collect();
313    result.push_str("..");
314    result
315}
316
317/// Calculate the display width of a string
318///
319/// For now, this is a simple character count.
320/// Could be enhanced with unicode-width crate for proper CJK handling.
321pub fn display_width(s: &str) -> usize {
322    s.chars().count()
323}
324
325#[cfg(test)]
326mod tests {
327    use std::sync::Arc;
328
329    use arrow::array::{
330        ArrayRef, BinaryArray, BooleanArray, Date32Array, Date64Array, Float32Array, Float64Array,
331        Int16Array, Int32Array, Int64Array, Int8Array, LargeBinaryArray, LargeStringArray,
332        NullArray, StringArray, TimestampMillisecondArray, UInt16Array, UInt32Array, UInt64Array,
333        UInt8Array,
334    };
335
336    use super::*;
337
338    fn make_string_array(values: Vec<Option<&str>>) -> ArrayRef {
339        Arc::new(StringArray::from(values))
340    }
341
342    fn make_int32_array(values: Vec<Option<i32>>) -> ArrayRef {
343        Arc::new(Int32Array::from(values))
344    }
345
346    fn make_float32_array(values: Vec<Option<f32>>) -> ArrayRef {
347        Arc::new(Float32Array::from(values))
348    }
349
350    fn make_int8_array(values: Vec<Option<i8>>) -> ArrayRef {
351        Arc::new(Int8Array::from(values))
352    }
353
354    fn make_int16_array(values: Vec<Option<i16>>) -> ArrayRef {
355        Arc::new(Int16Array::from(values))
356    }
357
358    fn make_int64_array(values: Vec<Option<i64>>) -> ArrayRef {
359        Arc::new(Int64Array::from(values))
360    }
361
362    fn make_uint8_array(values: Vec<Option<u8>>) -> ArrayRef {
363        Arc::new(UInt8Array::from(values))
364    }
365
366    fn make_uint16_array(values: Vec<Option<u16>>) -> ArrayRef {
367        Arc::new(UInt16Array::from(values))
368    }
369
370    fn make_uint32_array(values: Vec<Option<u32>>) -> ArrayRef {
371        Arc::new(UInt32Array::from(values))
372    }
373
374    fn make_uint64_array(values: Vec<Option<u64>>) -> ArrayRef {
375        Arc::new(UInt64Array::from(values))
376    }
377
378    fn make_float64_array(values: Vec<Option<f64>>) -> ArrayRef {
379        Arc::new(Float64Array::from(values))
380    }
381
382    fn make_boolean_array(values: Vec<Option<bool>>) -> ArrayRef {
383        Arc::new(BooleanArray::from(values))
384    }
385
386    #[test]
387    fn f_format_utf8_string() {
388        let array = make_string_array(vec![Some("hello"), Some("world")]);
389        let result = format_array_value(array.as_ref(), 0).unwrap();
390        assert_eq!(result, Some("hello".to_string()));
391    }
392
393    #[test]
394    fn f_format_utf8_null() {
395        let array = make_string_array(vec![None, Some("world")]);
396        let result = format_array_value(array.as_ref(), 0).unwrap();
397        assert_eq!(result, Some("NULL".to_string()));
398    }
399
400    #[test]
401    fn f_format_int32() {
402        let array = make_int32_array(vec![Some(42), Some(-100)]);
403        let result = format_array_value(array.as_ref(), 0).unwrap();
404        assert_eq!(result, Some("42".to_string()));
405
406        let result_neg = format_array_value(array.as_ref(), 1).unwrap();
407        assert_eq!(result_neg, Some("-100".to_string()));
408    }
409
410    #[test]
411    fn f_format_float32() {
412        let array = make_float32_array(vec![Some(2.71), Some(2.0)]);
413        let result = format_array_value(array.as_ref(), 0).unwrap();
414        assert_eq!(result, Some("2.71".to_string()));
415    }
416
417    #[test]
418    fn f_format_out_of_bounds() {
419        let array = make_string_array(vec![Some("hello")]);
420        let result = format_array_value(array.as_ref(), 10).unwrap();
421        assert_eq!(result, None);
422    }
423
424    #[test]
425    fn f_truncate_string_short() {
426        let s = "hello";
427        assert_eq!(truncate_string(s, 10), "hello");
428    }
429
430    #[test]
431    fn f_truncate_string_exact() {
432        let s = "hello";
433        assert_eq!(truncate_string(s, 5), "hello");
434    }
435
436    #[test]
437    fn f_truncate_string_long() {
438        let s = "hello world this is a long string";
439        let result = truncate_string(s, 10);
440        assert!(result.ends_with(".."));
441        assert!(result.chars().count() <= 10);
442    }
443
444    #[test]
445    fn f_truncate_string_very_short_max() {
446        let s = "hello";
447        let result = truncate_string(s, 2);
448        assert_eq!(result.chars().count(), 2);
449    }
450
451    #[test]
452    fn f_display_width_ascii() {
453        assert_eq!(display_width("hello"), 5);
454    }
455
456    #[test]
457    fn f_display_width_unicode() {
458        assert_eq!(display_width("日本語"), 3);
459    }
460
461    #[test]
462    fn f_display_width_empty() {
463        assert_eq!(display_width(""), 0);
464    }
465
466    #[test]
467    fn f_hex_encode_empty() {
468        assert_eq!(hex_encode(&[]), "");
469    }
470
471    #[test]
472    fn f_hex_encode_bytes() {
473        assert_eq!(hex_encode(&[0xde, 0xad, 0xbe, 0xef]), "deadbeef");
474    }
475
476    #[test]
477    fn f_format_bytes_preview_short() {
478        let bytes = vec![0x01, 0x02, 0x03];
479        let result = format_bytes_preview(&bytes);
480        assert!(result.starts_with("0x"));
481        assert!(result.contains("010203"));
482    }
483
484    #[test]
485    fn f_format_bytes_preview_long() {
486        let bytes = vec![0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a];
487        let result = format_bytes_preview(&bytes);
488        assert!(result.contains("..."));
489        assert!(result.contains("10 bytes"));
490    }
491
492    #[test]
493    fn f_type_name_string() {
494        assert_eq!(type_name(&DataType::Utf8), "string");
495    }
496
497    #[test]
498    fn f_type_name_int32() {
499        assert_eq!(type_name(&DataType::Int32), "i32");
500    }
501
502    #[test]
503    fn f_type_name_float32() {
504        assert_eq!(type_name(&DataType::Float32), "f32");
505    }
506
507    // Additional tests for comprehensive coverage
508
509    #[test]
510    fn f_format_int8() {
511        let array = make_int8_array(vec![Some(42), Some(-100)]);
512        let result = format_array_value(array.as_ref(), 0).unwrap();
513        assert_eq!(result, Some("42".to_string()));
514    }
515
516    #[test]
517    fn f_format_int16() {
518        let array = make_int16_array(vec![Some(1234), Some(-5678)]);
519        let result = format_array_value(array.as_ref(), 0).unwrap();
520        assert_eq!(result, Some("1234".to_string()));
521    }
522
523    #[test]
524    fn f_format_int64() {
525        let array = make_int64_array(vec![Some(1_000_000_000_000), Some(-1)]);
526        let result = format_array_value(array.as_ref(), 0).unwrap();
527        assert_eq!(result, Some("1000000000000".to_string()));
528    }
529
530    #[test]
531    fn f_format_uint8() {
532        let array = make_uint8_array(vec![Some(255), Some(0)]);
533        let result = format_array_value(array.as_ref(), 0).unwrap();
534        assert_eq!(result, Some("255".to_string()));
535    }
536
537    #[test]
538    fn f_format_uint16() {
539        let array = make_uint16_array(vec![Some(65535), Some(0)]);
540        let result = format_array_value(array.as_ref(), 0).unwrap();
541        assert_eq!(result, Some("65535".to_string()));
542    }
543
544    #[test]
545    fn f_format_uint32() {
546        let array = make_uint32_array(vec![Some(4_000_000_000), Some(0)]);
547        let result = format_array_value(array.as_ref(), 0).unwrap();
548        assert_eq!(result, Some("4000000000".to_string()));
549    }
550
551    #[test]
552    fn f_format_uint64() {
553        let array = make_uint64_array(vec![Some(18_446_744_073_709_551_615), Some(0)]);
554        let result = format_array_value(array.as_ref(), 0).unwrap();
555        assert_eq!(result, Some("18446744073709551615".to_string()));
556    }
557
558    #[test]
559    fn f_format_float64() {
560        let array = make_float64_array(vec![Some(2.718281828), Some(1.0)]);
561        let result = format_array_value(array.as_ref(), 0).unwrap();
562        assert_eq!(result, Some("2.7183".to_string()));
563    }
564
565    #[test]
566    fn f_format_boolean_true() {
567        let array = make_boolean_array(vec![Some(true), Some(false)]);
568        let result = format_array_value(array.as_ref(), 0).unwrap();
569        assert_eq!(result, Some("true".to_string()));
570    }
571
572    #[test]
573    fn f_format_boolean_false() {
574        let array = make_boolean_array(vec![Some(false), Some(true)]);
575        let result = format_array_value(array.as_ref(), 0).unwrap();
576        assert_eq!(result, Some("false".to_string()));
577    }
578
579    #[test]
580    fn f_format_large_string() {
581        let array: ArrayRef = Arc::new(LargeStringArray::from(vec!["large_text", "another"]));
582        let result = format_array_value(array.as_ref(), 0).unwrap();
583        assert_eq!(result, Some("large_text".to_string()));
584    }
585
586    #[test]
587    fn f_format_binary() {
588        let array: ArrayRef = Arc::new(BinaryArray::from_vec(vec![&[0x01, 0x02, 0x03]]));
589        let result = format_array_value(array.as_ref(), 0).unwrap();
590        assert!(result.is_some());
591        assert!(result.unwrap().starts_with("0x"));
592    }
593
594    #[test]
595    fn f_format_large_binary() {
596        let array: ArrayRef = Arc::new(LargeBinaryArray::from_vec(vec![&[0xde, 0xad, 0xbe, 0xef]]));
597        let result = format_array_value(array.as_ref(), 0).unwrap();
598        assert!(result.is_some());
599        assert!(result.unwrap().contains("deadbeef"));
600    }
601
602    #[test]
603    fn f_format_date32() {
604        let array: ArrayRef = Arc::new(Date32Array::from(vec![Some(19000), None]));
605        let result = format_array_value(array.as_ref(), 0).unwrap();
606        assert!(result.is_some());
607        assert!(result.unwrap().contains("date:"));
608    }
609
610    #[test]
611    fn f_format_date64() {
612        let array: ArrayRef = Arc::new(Date64Array::from(vec![Some(1_640_000_000_000), None]));
613        let result = format_array_value(array.as_ref(), 0).unwrap();
614        assert!(result.is_some());
615        assert!(result.unwrap().contains("date64:"));
616    }
617
618    #[test]
619    fn f_format_timestamp_ms() {
620        let array: ArrayRef = Arc::new(TimestampMillisecondArray::from(vec![
621            Some(1_640_000_000_000),
622            None,
623        ]));
624        let result = format_array_value(array.as_ref(), 0).unwrap();
625        assert!(result.is_some());
626        assert!(result.unwrap().contains("ts:"));
627    }
628
629    #[test]
630    fn f_format_null_type() {
631        let array: ArrayRef = Arc::new(NullArray::new(3));
632        let result = format_array_value(array.as_ref(), 0).unwrap();
633        assert_eq!(result, Some("NULL".to_string()));
634    }
635
636    #[test]
637    fn f_type_name_null() {
638        assert_eq!(type_name(&DataType::Null), "null");
639    }
640
641    #[test]
642    fn f_type_name_bool() {
643        assert_eq!(type_name(&DataType::Boolean), "bool");
644    }
645
646    #[test]
647    fn f_type_name_int8() {
648        assert_eq!(type_name(&DataType::Int8), "i8");
649    }
650
651    #[test]
652    fn f_type_name_int16() {
653        assert_eq!(type_name(&DataType::Int16), "i16");
654    }
655
656    #[test]
657    fn f_type_name_int64() {
658        assert_eq!(type_name(&DataType::Int64), "i64");
659    }
660
661    #[test]
662    fn f_type_name_uint8() {
663        assert_eq!(type_name(&DataType::UInt8), "u8");
664    }
665
666    #[test]
667    fn f_type_name_uint16() {
668        assert_eq!(type_name(&DataType::UInt16), "u16");
669    }
670
671    #[test]
672    fn f_type_name_uint32() {
673        assert_eq!(type_name(&DataType::UInt32), "u32");
674    }
675
676    #[test]
677    fn f_type_name_uint64() {
678        assert_eq!(type_name(&DataType::UInt64), "u64");
679    }
680
681    #[test]
682    fn f_type_name_float64() {
683        assert_eq!(type_name(&DataType::Float64), "f64");
684    }
685
686    #[test]
687    fn f_type_name_large_string() {
688        assert_eq!(type_name(&DataType::LargeUtf8), "large_string");
689    }
690
691    #[test]
692    fn f_type_name_binary() {
693        assert_eq!(type_name(&DataType::Binary), "binary");
694    }
695
696    #[test]
697    fn f_type_name_large_binary() {
698        assert_eq!(type_name(&DataType::LargeBinary), "large_binary");
699    }
700
701    #[test]
702    fn f_type_name_date32() {
703        assert_eq!(type_name(&DataType::Date32), "date32");
704    }
705
706    #[test]
707    fn f_type_name_date64() {
708        assert_eq!(type_name(&DataType::Date64), "date64");
709    }
710
711    #[test]
712    fn f_type_name_timestamp() {
713        let ts_type = DataType::Timestamp(arrow::datatypes::TimeUnit::Millisecond, None);
714        assert_eq!(type_name(&ts_type), "timestamp");
715    }
716
717    #[test]
718    fn f_type_name_list() {
719        use arrow::datatypes::Field;
720        let list_type = DataType::List(Arc::new(Field::new("item", DataType::Int32, true)));
721        assert_eq!(type_name(&list_type), "list");
722    }
723
724    #[test]
725    fn f_type_name_large_list() {
726        use arrow::datatypes::Field;
727        let list_type = DataType::LargeList(Arc::new(Field::new("item", DataType::Int32, true)));
728        assert_eq!(type_name(&list_type), "large_list");
729    }
730
731    #[test]
732    fn f_type_name_struct() {
733        use arrow::datatypes::{Field, Fields};
734        let fields = Fields::from(vec![Field::new("a", DataType::Int32, false)]);
735        let struct_type = DataType::Struct(fields);
736        assert_eq!(type_name(&struct_type), "struct");
737    }
738
739    #[test]
740    fn f_type_name_map() {
741        use arrow::datatypes::Field;
742        let entries = Field::new(
743            "entries",
744            DataType::Struct(
745                vec![
746                    Field::new("key", DataType::Utf8, false),
747                    Field::new("value", DataType::Int32, true),
748                ]
749                .into(),
750            ),
751            false,
752        );
753        let map_type = DataType::Map(Arc::new(entries), false);
754        assert_eq!(type_name(&map_type), "map");
755    }
756
757    #[test]
758    fn f_type_name_dict() {
759        let dict_type = DataType::Dictionary(Box::new(DataType::Int32), Box::new(DataType::Utf8));
760        assert_eq!(type_name(&dict_type), "dict");
761    }
762
763    #[test]
764    fn f_type_name_unknown() {
765        // Test a type that falls through to the catch-all
766        let interval_type = DataType::Interval(arrow::datatypes::IntervalUnit::DayTime);
767        assert_eq!(type_name(&interval_type), "unknown");
768    }
769
770    #[test]
771    fn f_format_timestamp_second() {
772        use arrow::array::TimestampSecondArray;
773        let array: ArrayRef = Arc::new(TimestampSecondArray::from(vec![Some(1_640_000_000), None]));
774        let result = format_array_value(array.as_ref(), 0).unwrap();
775        assert!(result.is_some());
776        assert!(result.unwrap().contains("ts:"));
777    }
778
779    #[test]
780    fn f_format_timestamp_microsecond() {
781        use arrow::array::TimestampMicrosecondArray;
782        let array: ArrayRef = Arc::new(TimestampMicrosecondArray::from(vec![
783            Some(1_640_000_000_000_000),
784            None,
785        ]));
786        let result = format_array_value(array.as_ref(), 0).unwrap();
787        assert!(result.is_some());
788        assert!(result.unwrap().contains("ts:"));
789    }
790
791    #[test]
792    fn f_format_timestamp_nanosecond() {
793        use arrow::array::TimestampNanosecondArray;
794        let array: ArrayRef = Arc::new(TimestampNanosecondArray::from(vec![
795            Some(1_640_000_000_000_000_000),
796            None,
797        ]));
798        let result = format_array_value(array.as_ref(), 0).unwrap();
799        assert!(result.is_some());
800        assert!(result.unwrap().contains("ts:"));
801    }
802
803    #[test]
804    fn f_format_unsupported_type() {
805        // Test with an unsupported type that should show placeholder
806        use arrow::{array::ListArray, buffer::OffsetBuffer, datatypes::Field};
807
808        let values = Int32Array::from(vec![1, 2, 3, 4, 5]);
809        let offsets = OffsetBuffer::new(vec![0, 2, 5].into());
810        let field = Arc::new(Field::new("item", DataType::Int32, true));
811        let array: ArrayRef = Arc::new(ListArray::new(field, offsets, Arc::new(values), None));
812
813        let result = format_array_value(array.as_ref(), 0).unwrap();
814        assert!(result.is_some());
815        // Should show <list> placeholder
816        assert!(result.unwrap().contains("list"));
817    }
818}