polars_core/
fmt.rs

1#![allow(unsafe_op_in_unsafe_fn)]
2#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
3use std::borrow::Cow;
4use std::fmt::{Debug, Display, Formatter, Write};
5use std::str::FromStr;
6use std::sync::RwLock;
7use std::{fmt, str};
8
9#[cfg(any(
10    feature = "dtype-date",
11    feature = "dtype-datetime",
12    feature = "dtype-time"
13))]
14use arrow::temporal_conversions::*;
15#[cfg(feature = "dtype-datetime")]
16use chrono::NaiveDateTime;
17#[cfg(feature = "timezones")]
18use chrono::TimeZone;
19#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
20use comfy_table::modifiers::*;
21#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
22use comfy_table::presets::*;
23#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
24use comfy_table::*;
25use num_traits::{Num, NumCast};
26use polars_error::feature_gated;
27use polars_utils::relaxed_cell::RelaxedCell;
28
29use crate::config::*;
30use crate::prelude::*;
31
32// Note: see https://github.com/pola-rs/polars/pull/13699 for the rationale
33// behind choosing 10 as the default value for default number of rows displayed
34const DEFAULT_ROW_LIMIT: usize = 10;
35#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
36const DEFAULT_COL_LIMIT: usize = 8;
37const DEFAULT_STR_LEN_LIMIT: usize = 30;
38const DEFAULT_LIST_LEN_LIMIT: usize = 3;
39
40#[derive(Copy, Clone)]
41#[repr(u8)]
42pub enum FloatFmt {
43    Mixed,
44    Full,
45}
46static FLOAT_PRECISION: RwLock<Option<usize>> = RwLock::new(None);
47static FLOAT_FMT: RelaxedCell<u8> = RelaxedCell::new_u8(FloatFmt::Mixed as u8);
48
49static THOUSANDS_SEPARATOR: RelaxedCell<u8> = RelaxedCell::new_u8(b'\0');
50static DECIMAL_SEPARATOR: RelaxedCell<u8> = RelaxedCell::new_u8(b'.');
51
52// Numeric formatting getters
53pub fn get_float_fmt() -> FloatFmt {
54    match FLOAT_FMT.load() {
55        0 => FloatFmt::Mixed,
56        1 => FloatFmt::Full,
57        _ => panic!(),
58    }
59}
60pub fn get_float_precision() -> Option<usize> {
61    *FLOAT_PRECISION.read().unwrap()
62}
63pub fn get_decimal_separator() -> char {
64    DECIMAL_SEPARATOR.load() as char
65}
66pub fn get_thousands_separator() -> String {
67    let sep = THOUSANDS_SEPARATOR.load() as char;
68    if sep == '\0' {
69        "".to_string()
70    } else {
71        sep.to_string()
72    }
73}
74#[cfg(feature = "dtype-decimal")]
75pub fn get_trim_decimal_zeros() -> bool {
76    arrow::compute::decimal::get_trim_decimal_zeros()
77}
78
79// Numeric formatting setters
80pub fn set_float_fmt(fmt: FloatFmt) {
81    FLOAT_FMT.store(fmt as u8)
82}
83pub fn set_float_precision(precision: Option<usize>) {
84    *FLOAT_PRECISION.write().unwrap() = precision;
85}
86pub fn set_decimal_separator(dec: Option<char>) {
87    DECIMAL_SEPARATOR.store(dec.unwrap_or('.') as u8)
88}
89pub fn set_thousands_separator(sep: Option<char>) {
90    THOUSANDS_SEPARATOR.store(sep.unwrap_or('\0') as u8)
91}
92#[cfg(feature = "dtype-decimal")]
93pub fn set_trim_decimal_zeros(trim: Option<bool>) {
94    arrow::compute::decimal::set_trim_decimal_zeros(trim)
95}
96
97/// Parses an environment variable value.
98fn parse_env_var<T: FromStr>(name: &str) -> Option<T> {
99    std::env::var(name).ok().and_then(|v| v.parse().ok())
100}
101/// Parses an environment variable value as a limit or set a default.
102///
103/// Negative values (e.g. -1) are parsed as 'no limit' or [`usize::MAX`].
104fn parse_env_var_limit(name: &str, default: usize) -> usize {
105    parse_env_var(name).map_or(
106        default,
107        |n: i64| {
108            if n < 0 { usize::MAX } else { n as usize }
109        },
110    )
111}
112
113fn get_row_limit() -> usize {
114    parse_env_var_limit(FMT_MAX_ROWS, DEFAULT_ROW_LIMIT)
115}
116#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
117fn get_col_limit() -> usize {
118    parse_env_var_limit(FMT_MAX_COLS, DEFAULT_COL_LIMIT)
119}
120fn get_str_len_limit() -> usize {
121    parse_env_var_limit(FMT_STR_LEN, DEFAULT_STR_LEN_LIMIT)
122}
123fn get_list_len_limit() -> usize {
124    parse_env_var_limit(FMT_TABLE_CELL_LIST_LEN, DEFAULT_LIST_LEN_LIMIT)
125}
126#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
127fn get_ellipsis() -> &'static str {
128    match std::env::var(FMT_TABLE_FORMATTING).as_deref().unwrap_or("") {
129        preset if preset.starts_with("ASCII") => "...",
130        _ => "…",
131    }
132}
133#[cfg(not(any(feature = "fmt", feature = "fmt_no_tty")))]
134fn get_ellipsis() -> &'static str {
135    "…"
136}
137
138fn estimate_string_width(s: &str) -> usize {
139    // get a slightly more accurate estimate of a string's screen
140    // width, accounting (very roughly) for multibyte characters
141    let n_chars = s.chars().count();
142    let n_bytes = s.len();
143    if n_bytes == n_chars {
144        n_chars
145    } else {
146        let adjust = n_bytes as f64 / n_chars as f64;
147        std::cmp::min(n_chars * 2, (n_chars as f64 * adjust).ceil() as usize)
148    }
149}
150
151macro_rules! format_array {
152    ($f:ident, $a:expr, $dtype:expr, $name:expr, $array_type:expr) => {{
153        write!(
154            $f,
155            "shape: ({},)\n{}: '{}' [{}]\n[\n",
156            fmt_int_string_custom(&$a.len().to_string(), 3, "_"),
157            $array_type,
158            $name,
159            $dtype
160        )?;
161
162        let ellipsis = get_ellipsis();
163        let truncate = match $a.dtype().to_storage() {
164            DataType::String => true,
165            #[cfg(feature = "dtype-categorical")]
166            DataType::Categorical(_, _) | DataType::Enum(_, _) => true,
167            _ => false,
168        };
169        let truncate_len = if truncate { get_str_len_limit() } else { 0 };
170
171        let write_fn = |v, f: &mut Formatter| -> fmt::Result {
172            if truncate {
173                let v = format!("{}", v);
174                let v_no_quotes = &v[1..v.len() - 1];
175                let v_trunc = &v_no_quotes[..v_no_quotes
176                    .char_indices()
177                    .take(truncate_len)
178                    .last()
179                    .map(|(i, c)| i + c.len_utf8())
180                    .unwrap_or(0)];
181                if v_no_quotes == v_trunc {
182                    write!(f, "\t{}\n", v)?;
183                } else {
184                    write!(f, "\t\"{v_trunc}{ellipsis}\n")?;
185                }
186            } else {
187                write!(f, "\t{v}\n")?;
188            };
189            Ok(())
190        };
191
192        let limit = get_row_limit();
193
194        if $a.len() > limit {
195            let half = limit / 2;
196            let rest = limit % 2;
197
198            for i in 0..(half + rest) {
199                let v = $a.get_any_value(i).unwrap();
200                write_fn(v, $f)?;
201            }
202            write!($f, "\t{ellipsis}\n")?;
203            for i in ($a.len() - half)..$a.len() {
204                let v = $a.get_any_value(i).unwrap();
205                write_fn(v, $f)?;
206            }
207        } else {
208            for i in 0..$a.len() {
209                let v = $a.get_any_value(i).unwrap();
210                write_fn(v, $f)?;
211            }
212        }
213
214        write!($f, "]")
215    }};
216}
217
218#[cfg(feature = "object")]
219fn format_object_array(
220    f: &mut Formatter<'_>,
221    object: &Series,
222    name: &str,
223    array_type: &str,
224) -> fmt::Result {
225    match object.dtype() {
226        DataType::Object(inner_type) => {
227            let limit = std::cmp::min(DEFAULT_ROW_LIMIT, object.len());
228            write!(
229                f,
230                "shape: ({},)\n{}: '{}' [o][{}]\n[\n",
231                fmt_int_string_custom(&object.len().to_string(), 3, "_"),
232                array_type,
233                name,
234                inner_type
235            )?;
236            for i in 0..limit {
237                let v = object.str_value(i);
238                writeln!(f, "\t{}", v.unwrap())?;
239            }
240            write!(f, "]")
241        },
242        _ => unreachable!(),
243    }
244}
245
246impl<T> Debug for ChunkedArray<T>
247where
248    T: PolarsNumericType,
249{
250    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
251        let dt = format!("{}", T::get_static_dtype());
252        format_array!(f, self, dt, self.name(), "ChunkedArray")
253    }
254}
255
256impl Debug for ChunkedArray<BooleanType> {
257    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
258        format_array!(f, self, "bool", self.name(), "ChunkedArray")
259    }
260}
261
262impl Debug for StringChunked {
263    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
264        format_array!(f, self, "str", self.name(), "ChunkedArray")
265    }
266}
267
268impl Debug for BinaryChunked {
269    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
270        format_array!(f, self, "binary", self.name(), "ChunkedArray")
271    }
272}
273
274impl Debug for ListChunked {
275    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
276        format_array!(f, self, "list", self.name(), "ChunkedArray")
277    }
278}
279
280#[cfg(feature = "dtype-array")]
281impl Debug for ArrayChunked {
282    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
283        format_array!(f, self, "fixed size list", self.name(), "ChunkedArray")
284    }
285}
286
287#[cfg(feature = "object")]
288impl<T> Debug for ObjectChunked<T>
289where
290    T: PolarsObject,
291{
292    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
293        let limit = std::cmp::min(DEFAULT_ROW_LIMIT, self.len());
294        let ellipsis = get_ellipsis();
295        let inner_type = T::type_name();
296        write!(
297            f,
298            "ChunkedArray: '{}' [o][{}]\n[\n",
299            self.name(),
300            inner_type
301        )?;
302
303        if limit < self.len() {
304            for i in 0..limit / 2 {
305                match self.get(i) {
306                    None => writeln!(f, "\tnull")?,
307                    Some(val) => writeln!(f, "\t{val}")?,
308                };
309            }
310            writeln!(f, "\t{ellipsis}")?;
311            for i in (0..limit / 2).rev() {
312                match self.get(self.len() - i - 1) {
313                    None => writeln!(f, "\tnull")?,
314                    Some(val) => writeln!(f, "\t{val}")?,
315                };
316            }
317        } else {
318            for i in 0..limit {
319                match self.get(i) {
320                    None => writeln!(f, "\tnull")?,
321                    Some(val) => writeln!(f, "\t{val}")?,
322                };
323            }
324        }
325        Ok(())
326    }
327}
328
329impl Debug for Series {
330    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
331        match self.dtype() {
332            DataType::Boolean => {
333                format_array!(f, self.bool().unwrap(), "bool", self.name(), "Series")
334            },
335            DataType::String => {
336                format_array!(f, self.str().unwrap(), "str", self.name(), "Series")
337            },
338            DataType::UInt8 => {
339                format_array!(f, self.u8().unwrap(), "u8", self.name(), "Series")
340            },
341            DataType::UInt16 => {
342                format_array!(f, self.u16().unwrap(), "u16", self.name(), "Series")
343            },
344            DataType::UInt32 => {
345                format_array!(f, self.u32().unwrap(), "u32", self.name(), "Series")
346            },
347            DataType::UInt64 => {
348                format_array!(f, self.u64().unwrap(), "u64", self.name(), "Series")
349            },
350            DataType::UInt128 => {
351                feature_gated!(
352                    "dtype-u128",
353                    format_array!(f, self.u128().unwrap(), "u128", self.name(), "Series")
354                )
355            },
356            DataType::Int8 => {
357                format_array!(f, self.i8().unwrap(), "i8", self.name(), "Series")
358            },
359            DataType::Int16 => {
360                format_array!(f, self.i16().unwrap(), "i16", self.name(), "Series")
361            },
362            DataType::Int32 => {
363                format_array!(f, self.i32().unwrap(), "i32", self.name(), "Series")
364            },
365            DataType::Int64 => {
366                format_array!(f, self.i64().unwrap(), "i64", self.name(), "Series")
367            },
368            DataType::Int128 => {
369                feature_gated!(
370                    "dtype-i128",
371                    format_array!(f, self.i128().unwrap(), "i128", self.name(), "Series")
372                )
373            },
374            #[cfg(feature = "dtype-f16")]
375            DataType::Float16 => {
376                format_array!(f, self.f16().unwrap(), "f16", self.name(), "Series")
377            },
378            DataType::Float32 => {
379                format_array!(f, self.f32().unwrap(), "f32", self.name(), "Series")
380            },
381            DataType::Float64 => {
382                format_array!(f, self.f64().unwrap(), "f64", self.name(), "Series")
383            },
384            #[cfg(feature = "dtype-date")]
385            DataType::Date => format_array!(f, self.date().unwrap(), "date", self.name(), "Series"),
386            #[cfg(feature = "dtype-datetime")]
387            DataType::Datetime(_, _) => {
388                let dt = format!("{}", self.dtype());
389                format_array!(f, self.datetime().unwrap(), &dt, self.name(), "Series")
390            },
391            #[cfg(feature = "dtype-time")]
392            DataType::Time => format_array!(f, self.time().unwrap(), "time", self.name(), "Series"),
393            #[cfg(feature = "dtype-duration")]
394            DataType::Duration(_) => {
395                let dt = format!("{}", self.dtype());
396                format_array!(f, self.duration().unwrap(), &dt, self.name(), "Series")
397            },
398            #[cfg(feature = "dtype-decimal")]
399            DataType::Decimal(_, _) => {
400                let dt = format!("{}", self.dtype());
401                format_array!(f, self.decimal().unwrap(), &dt, self.name(), "Series")
402            },
403            #[cfg(feature = "dtype-array")]
404            DataType::Array(_, _) => {
405                let dt = format!("{}", self.dtype());
406                format_array!(f, self.array().unwrap(), &dt, self.name(), "Series")
407            },
408            DataType::List(_) => {
409                let dt = format!("{}", self.dtype());
410                format_array!(f, self.list().unwrap(), &dt, self.name(), "Series")
411            },
412            #[cfg(feature = "object")]
413            DataType::Object(_) => format_object_array(f, self, self.name(), "Series"),
414            #[cfg(feature = "dtype-categorical")]
415            DataType::Categorical(cats, _) => {
416                with_match_categorical_physical_type!(cats.physical(), |$C| {
417                    format_array!(f, self.cat::<$C>().unwrap(), "cat", self.name(), "Series")
418                })
419            },
420
421            #[cfg(feature = "dtype-categorical")]
422            DataType::Enum(fcats, _) => {
423                with_match_categorical_physical_type!(fcats.physical(), |$C| {
424                    format_array!(f, self.cat::<$C>().unwrap(), "enum", self.name(), "Series")
425                })
426            },
427            #[cfg(feature = "dtype-struct")]
428            dt @ DataType::Struct(_) => format_array!(
429                f,
430                self.struct_().unwrap(),
431                format!("{dt}"),
432                self.name(),
433                "Series"
434            ),
435            DataType::Null => {
436                format_array!(f, self.null().unwrap(), "null", self.name(), "Series")
437            },
438            DataType::Binary => {
439                format_array!(f, self.binary().unwrap(), "binary", self.name(), "Series")
440            },
441            DataType::BinaryOffset => {
442                format_array!(
443                    f,
444                    self.binary_offset().unwrap(),
445                    "binary[offset]",
446                    self.name(),
447                    "Series"
448                )
449            },
450            #[cfg(feature = "dtype-extension")]
451            DataType::Extension(_, _) => {
452                let dt = format!("{}", self.dtype());
453                format_array!(f, self.ext().unwrap(), &dt, self.name(), "Series")
454            },
455            dt => panic!("{dt:?} not impl"),
456        }
457    }
458}
459
460impl Display for Series {
461    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
462        Debug::fmt(self, f)
463    }
464}
465
466impl Debug for DataFrame {
467    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
468        Display::fmt(self, f)
469    }
470}
471#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
472fn make_str_val(v: &str, truncate: usize, ellipsis: &String) -> String {
473    let v_trunc = &v[..v
474        .char_indices()
475        .take(truncate)
476        .last()
477        .map(|(i, c)| i + c.len_utf8())
478        .unwrap_or(0)];
479    if v == v_trunc {
480        v.to_string()
481    } else {
482        format!("{v_trunc}{ellipsis}")
483    }
484}
485
486#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
487fn field_to_str(
488    f: &Field,
489    str_truncate: usize,
490    ellipsis: &String,
491    padding: usize,
492) -> (String, usize) {
493    let name = make_str_val(f.name(), str_truncate, ellipsis);
494    let name_length = estimate_string_width(name.as_str());
495    let mut column_name = name;
496    if env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES) {
497        column_name = "".to_string();
498    }
499    let column_dtype = if env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES) {
500        "".to_string()
501    } else if env_is_true(FMT_TABLE_INLINE_COLUMN_DATA_TYPE)
502        | env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
503    {
504        format!("{}", f.dtype())
505    } else {
506        format!("\n{}", f.dtype())
507    };
508    let mut dtype_length = column_dtype.trim_start().len();
509    let mut separator = "\n---";
510    if env_is_true(FMT_TABLE_HIDE_COLUMN_SEPARATOR)
511        | env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
512        | env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES)
513    {
514        separator = ""
515    }
516    let s = if env_is_true(FMT_TABLE_INLINE_COLUMN_DATA_TYPE)
517        & !env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES)
518    {
519        let inline_name_dtype = format!("{column_name} ({column_dtype})");
520        dtype_length = inline_name_dtype.len();
521        inline_name_dtype
522    } else {
523        format!("{column_name}{separator}{column_dtype}")
524    };
525    let mut s_len = std::cmp::max(name_length, dtype_length);
526    let separator_length = estimate_string_width(separator.trim());
527    if s_len < separator_length {
528        s_len = separator_length;
529    }
530    (s, s_len + padding)
531}
532
533#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
534fn prepare_row(
535    row: Vec<Cow<'_, str>>,
536    n_first: usize,
537    n_last: usize,
538    str_truncate: usize,
539    max_elem_lengths: &mut [usize],
540    ellipsis: &String,
541    padding: usize,
542) -> Vec<String> {
543    let reduce_columns = n_first + n_last < row.len();
544    let n_elems = n_first + n_last + reduce_columns as usize;
545    let mut row_strings = Vec::with_capacity(n_elems);
546
547    for (idx, v) in row[0..n_first].iter().enumerate() {
548        let elem_str = make_str_val(v, str_truncate, ellipsis);
549        let elem_len = estimate_string_width(elem_str.as_str()) + padding;
550        if max_elem_lengths[idx] < elem_len {
551            max_elem_lengths[idx] = elem_len;
552        };
553        row_strings.push(elem_str);
554    }
555    if reduce_columns {
556        row_strings.push(ellipsis.to_string());
557        max_elem_lengths[n_first] = ellipsis.chars().count() + padding;
558    }
559    let elem_offset = n_first + reduce_columns as usize;
560    for (idx, v) in row[row.len() - n_last..].iter().enumerate() {
561        let elem_str = make_str_val(v, str_truncate, ellipsis);
562        let elem_len = estimate_string_width(elem_str.as_str()) + padding;
563        let elem_idx = elem_offset + idx;
564        if max_elem_lengths[elem_idx] < elem_len {
565            max_elem_lengths[elem_idx] = elem_len;
566        };
567        row_strings.push(elem_str);
568    }
569    row_strings
570}
571
572#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
573fn env_is_true(varname: &str) -> bool {
574    std::env::var(varname).as_deref().unwrap_or("0") == "1"
575}
576
577#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
578fn fmt_df_shape((shape0, shape1): &(usize, usize)) -> String {
579    // e.g. (1_000_000, 4_000)
580    format!(
581        "({}, {})",
582        fmt_int_string_custom(&shape0.to_string(), 3, "_"),
583        fmt_int_string_custom(&shape1.to_string(), 3, "_")
584    )
585}
586
587impl Display for DataFrame {
588    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
589        #[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
590        {
591            let height = self.height();
592            assert!(
593                self.columns.iter().all(|s| s.len() == height),
594                "The column lengths in the DataFrame are not equal."
595            );
596
597            let table_style = std::env::var(FMT_TABLE_FORMATTING).unwrap_or("DEFAULT".to_string());
598            let is_utf8 = !table_style.starts_with("ASCII");
599            let preset = match table_style.as_str() {
600                "ASCII_FULL" => ASCII_FULL,
601                "ASCII_FULL_CONDENSED" => ASCII_FULL_CONDENSED,
602                "ASCII_NO_BORDERS" => ASCII_NO_BORDERS,
603                "ASCII_BORDERS_ONLY" => ASCII_BORDERS_ONLY,
604                "ASCII_BORDERS_ONLY_CONDENSED" => ASCII_BORDERS_ONLY_CONDENSED,
605                "ASCII_HORIZONTAL_ONLY" => ASCII_HORIZONTAL_ONLY,
606                "ASCII_MARKDOWN" | "MARKDOWN" => ASCII_MARKDOWN,
607                "UTF8_FULL" => UTF8_FULL,
608                "UTF8_FULL_CONDENSED" => UTF8_FULL_CONDENSED,
609                "UTF8_NO_BORDERS" => UTF8_NO_BORDERS,
610                "UTF8_BORDERS_ONLY" => UTF8_BORDERS_ONLY,
611                "UTF8_HORIZONTAL_ONLY" => UTF8_HORIZONTAL_ONLY,
612                "NOTHING" => NOTHING,
613                _ => UTF8_FULL_CONDENSED,
614            };
615            let ellipsis = get_ellipsis().to_string();
616            let ellipsis_len = ellipsis.chars().count();
617            let max_n_cols = get_col_limit();
618            let max_n_rows = get_row_limit();
619            let str_truncate = get_str_len_limit();
620            let padding = 2; // eg: one char either side of the value
621
622            let (n_first, n_last) = if self.width() > max_n_cols {
623                (max_n_cols.div_ceil(2), max_n_cols / 2)
624            } else {
625                (self.width(), 0)
626            };
627            let reduce_columns = n_first + n_last < self.width();
628            let n_tbl_cols = n_first + n_last + reduce_columns as usize;
629            let mut names = Vec::with_capacity(n_tbl_cols);
630            let mut name_lengths = Vec::with_capacity(n_tbl_cols);
631
632            let fields = self.fields();
633            for field in fields[0..n_first].iter() {
634                let (s, l) = field_to_str(field, str_truncate, &ellipsis, padding);
635                names.push(s);
636                name_lengths.push(l);
637            }
638            if reduce_columns {
639                names.push(ellipsis.clone());
640                name_lengths.push(ellipsis_len);
641            }
642            for field in fields[self.width() - n_last..].iter() {
643                let (s, l) = field_to_str(field, str_truncate, &ellipsis, padding);
644                names.push(s);
645                name_lengths.push(l);
646            }
647
648            let mut table = Table::new();
649            table
650                .load_preset(preset)
651                .set_content_arrangement(ContentArrangement::Dynamic);
652
653            if is_utf8 && env_is_true(FMT_TABLE_ROUNDED_CORNERS) {
654                table.apply_modifier(UTF8_ROUND_CORNERS);
655            }
656            let mut constraints = Vec::with_capacity(n_tbl_cols);
657            let mut max_elem_lengths: Vec<usize> = vec![0; n_tbl_cols];
658
659            if max_n_rows > 0 {
660                if height > max_n_rows {
661                    // Truncate the table if we have more rows than the
662                    // configured maximum number of rows
663                    let mut rows = Vec::with_capacity(std::cmp::max(max_n_rows, 2));
664                    let half = max_n_rows / 2;
665                    let rest = max_n_rows % 2;
666
667                    for i in 0..(half + rest) {
668                        let row = self
669                            .get_columns()
670                            .iter()
671                            .map(|c| c.str_value(i).unwrap())
672                            .collect();
673
674                        let row_strings = prepare_row(
675                            row,
676                            n_first,
677                            n_last,
678                            str_truncate,
679                            &mut max_elem_lengths,
680                            &ellipsis,
681                            padding,
682                        );
683                        rows.push(row_strings);
684                    }
685                    let dots = vec![ellipsis.clone(); rows[0].len()];
686                    rows.push(dots);
687
688                    for i in (height - half)..height {
689                        let row = self
690                            .get_columns()
691                            .iter()
692                            .map(|c| c.str_value(i).unwrap())
693                            .collect();
694
695                        let row_strings = prepare_row(
696                            row,
697                            n_first,
698                            n_last,
699                            str_truncate,
700                            &mut max_elem_lengths,
701                            &ellipsis,
702                            padding,
703                        );
704                        rows.push(row_strings);
705                    }
706                    table.add_rows(rows);
707                } else {
708                    for i in 0..height {
709                        if self.width() > 0 {
710                            let row = self
711                                .materialized_column_iter()
712                                .map(|s| s.str_value(i).unwrap())
713                                .collect();
714
715                            let row_strings = prepare_row(
716                                row,
717                                n_first,
718                                n_last,
719                                str_truncate,
720                                &mut max_elem_lengths,
721                                &ellipsis,
722                                padding,
723                            );
724                            table.add_row(row_strings);
725                        } else {
726                            break;
727                        }
728                    }
729                }
730            } else if height > 0 {
731                let dots: Vec<String> = vec![ellipsis; self.columns.len()];
732                table.add_row(dots);
733            }
734            let tbl_fallback_width = 100;
735            let tbl_width = std::env::var("POLARS_TABLE_WIDTH")
736                .map(|s| {
737                    let n = s
738                        .parse::<i64>()
739                        .expect("could not parse table width argument");
740                    let w = if n < 0 {
741                        u16::MAX
742                    } else {
743                        u16::try_from(n).expect("table width argument does not fit in u16")
744                    };
745                    Some(w)
746                })
747                .unwrap_or(None);
748
749            // column width constraints
750            let col_width_exact =
751                |w: usize| ColumnConstraint::Absolute(comfy_table::Width::Fixed(w as u16));
752            let col_width_bounds = |l: usize, u: usize| ColumnConstraint::Boundaries {
753                lower: Width::Fixed(l as u16),
754                upper: Width::Fixed(u as u16),
755            };
756            let min_col_width = std::cmp::max(5, 3 + padding);
757            for (idx, elem_len) in max_elem_lengths.iter().enumerate() {
758                let mx = std::cmp::min(
759                    str_truncate + ellipsis_len + padding,
760                    std::cmp::max(name_lengths[idx], *elem_len),
761                );
762                if (mx <= min_col_width) && !(max_n_rows > 0 && height > max_n_rows) {
763                    // col width is less than min width + table is not truncated
764                    constraints.push(col_width_exact(mx));
765                } else if mx <= min_col_width {
766                    // col width is less than min width + table is truncated (w/ ellipsis)
767                    constraints.push(col_width_bounds(mx, min_col_width));
768                } else {
769                    constraints.push(col_width_bounds(min_col_width, mx));
770                }
771            }
772
773            // insert a header row, unless both column names and dtypes are hidden
774            if !(env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
775                && env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES))
776            {
777                table.set_header(names).set_constraints(constraints);
778            }
779
780            // if tbl_width is explicitly set, use it
781            if let Some(w) = tbl_width {
782                table.set_width(w);
783            } else {
784                // if no tbl_width (it's not tty && width not explicitly set), apply
785                // a default value; this is needed to support non-tty applications
786                #[cfg(feature = "fmt")]
787                if table.width().is_none() && !table.is_tty() {
788                    table.set_width(tbl_fallback_width);
789                }
790                #[cfg(feature = "fmt_no_tty")]
791                if table.width().is_none() {
792                    table.set_width(tbl_fallback_width);
793                }
794            }
795
796            // set alignment of cells, if defined
797            if std::env::var(FMT_TABLE_CELL_ALIGNMENT).is_ok()
798                | std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT).is_ok()
799            {
800                let str_preset = std::env::var(FMT_TABLE_CELL_ALIGNMENT)
801                    .unwrap_or_else(|_| "DEFAULT".to_string());
802                let num_preset = std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT)
803                    .unwrap_or_else(|_| str_preset.to_string());
804                for (column_index, column) in table.column_iter_mut().enumerate() {
805                    let dtype = fields[column_index].dtype();
806                    let mut preset = str_preset.as_str();
807                    if dtype.is_primitive_numeric() || dtype.is_decimal() {
808                        preset = num_preset.as_str();
809                    }
810                    match preset {
811                        "RIGHT" => column.set_cell_alignment(CellAlignment::Right),
812                        "LEFT" => column.set_cell_alignment(CellAlignment::Left),
813                        "CENTER" => column.set_cell_alignment(CellAlignment::Center),
814                        _ => {},
815                    }
816                }
817            }
818
819            // establish 'shape' information (above/below/hidden)
820            if env_is_true(FMT_TABLE_HIDE_DATAFRAME_SHAPE_INFORMATION) {
821                write!(f, "{table}")?;
822            } else {
823                let shape_str = fmt_df_shape(&self.shape());
824                if env_is_true(FMT_TABLE_DATAFRAME_SHAPE_BELOW) {
825                    write!(f, "{table}\nshape: {shape_str}")?;
826                } else {
827                    write!(f, "shape: {shape_str}\n{table}")?;
828                }
829            }
830        }
831        #[cfg(not(any(feature = "fmt", feature = "fmt_no_tty")))]
832        {
833            write!(
834                f,
835                "shape: {:?}\nto see more, compile with the 'fmt' or 'fmt_no_tty' feature",
836                self.shape()
837            )?;
838        }
839        Ok(())
840    }
841}
842
843fn fmt_int_string_custom(num: &str, group_size: u8, group_separator: &str) -> String {
844    if group_size == 0 || num.len() <= 1 {
845        num.to_string()
846    } else {
847        let mut out = String::new();
848        let sign_offset = if num.starts_with('-') || num.starts_with('+') {
849            out.push(num.chars().next().unwrap());
850            1
851        } else {
852            0
853        };
854        let int_body = &num.as_bytes()[sign_offset..]
855            .rchunks(group_size as usize)
856            .rev()
857            .map(str::from_utf8)
858            .collect::<Result<Vec<&str>, _>>()
859            .unwrap()
860            .join(group_separator);
861        out.push_str(int_body);
862        out
863    }
864}
865
866fn fmt_int_string(num: &str) -> String {
867    fmt_int_string_custom(num, 3, &get_thousands_separator())
868}
869
870fn fmt_float_string_custom(
871    num: &str,
872    group_size: u8,
873    group_separator: &str,
874    decimal: char,
875) -> String {
876    // Quick exit if no formatting would be applied
877    if num.len() <= 1 || (group_size == 0 && decimal == '.') {
878        num.to_string()
879    } else {
880        // Take existing numeric string and apply digit grouping & separator/decimal chars
881        // e.g. "1000000" → "1_000_000", "-123456.798" → "-123,456.789", etc
882        let (idx, has_fractional) = match num.find('.') {
883            Some(i) => (i, true),
884            None => (num.len(), false),
885        };
886        let mut out = String::new();
887        let integer_part = &num[..idx];
888
889        out.push_str(&fmt_int_string_custom(
890            integer_part,
891            group_size,
892            group_separator,
893        ));
894        if has_fractional {
895            out.push(decimal);
896            out.push_str(&num[idx + 1..]);
897        };
898        out
899    }
900}
901
902fn fmt_float_string(num: &str) -> String {
903    fmt_float_string_custom(num, 3, &get_thousands_separator(), get_decimal_separator())
904}
905
906fn fmt_integer<T: Num + NumCast + Display>(
907    f: &mut Formatter<'_>,
908    width: usize,
909    v: T,
910) -> fmt::Result {
911    write!(f, "{:>width$}", fmt_int_string(&v.to_string()))
912}
913
914const SCIENTIFIC_BOUND: f64 = 999999.0;
915
916fn fmt_float<T: Num + NumCast>(f: &mut Formatter<'_>, width: usize, v: T) -> fmt::Result {
917    let v: f64 = NumCast::from(v).unwrap();
918
919    let float_precision = get_float_precision();
920
921    if let Some(precision) = float_precision {
922        if format!("{v:.precision$}").len() > 19 {
923            return write!(f, "{v:>width$.precision$e}");
924        }
925        let s = format!("{v:>width$.precision$}");
926        return write!(f, "{}", fmt_float_string(s.as_str()));
927    }
928
929    if matches!(get_float_fmt(), FloatFmt::Full) {
930        let s = format!("{v:>width$}");
931        return write!(f, "{}", fmt_float_string(s.as_str()));
932    }
933
934    // show integers as 0.0, 1.0 ... 101.0
935    if v.fract() == 0.0 && v.abs() < SCIENTIFIC_BOUND {
936        let s = format!("{v:>width$.1}");
937        write!(f, "{}", fmt_float_string(s.as_str()))
938    } else if format!("{v}").len() > 9 {
939        // large and small floats in scientific notation.
940        // (note: scientific notation does not play well with digit grouping)
941        if (!(0.000001..=SCIENTIFIC_BOUND).contains(&v.abs()) | (v.abs() > SCIENTIFIC_BOUND))
942            && get_thousands_separator().is_empty()
943        {
944            let s = format!("{v:>width$.4e}");
945            write!(f, "{}", fmt_float_string(s.as_str()))
946        } else {
947            // this makes sure we don't write 12.00000 in case of a long flt that is 12.0000000001
948            // instead we write 12.0
949            let s = format!("{v:>width$.6}");
950
951            if s.ends_with('0') {
952                let mut s = s.as_str();
953                let mut len = s.len() - 1;
954
955                while s.ends_with('0') {
956                    s = &s[..len];
957                    len -= 1;
958                }
959                let s = if s.ends_with('.') {
960                    format!("{s}0")
961                } else {
962                    s.to_string()
963                };
964                write!(f, "{}", fmt_float_string(s.as_str()))
965            } else {
966                // 12.0934509341243124
967                // written as
968                // 12.09345
969                let s = format!("{v:>width$.6}");
970                write!(f, "{}", fmt_float_string(s.as_str()))
971            }
972        }
973    } else {
974        let s = if v.fract() == 0.0 {
975            format!("{v:>width$e}")
976        } else {
977            format!("{v:>width$}")
978        };
979        write!(f, "{}", fmt_float_string(s.as_str()))
980    }
981}
982
983#[cfg(feature = "dtype-datetime")]
984fn fmt_datetime(
985    f: &mut Formatter<'_>,
986    v: i64,
987    tu: TimeUnit,
988    tz: Option<&self::datatypes::TimeZone>,
989) -> fmt::Result {
990    let ndt = match tu {
991        TimeUnit::Nanoseconds => timestamp_ns_to_datetime(v),
992        TimeUnit::Microseconds => timestamp_us_to_datetime(v),
993        TimeUnit::Milliseconds => timestamp_ms_to_datetime(v),
994    };
995    match tz {
996        None => std::fmt::Display::fmt(&ndt, f),
997        Some(tz) => PlTzAware::new(ndt, tz).fmt(f),
998    }
999}
1000
1001#[cfg(feature = "dtype-duration")]
1002const DURATION_PARTS: [&str; 4] = ["d", "h", "m", "s"];
1003#[cfg(feature = "dtype-duration")]
1004const ISO_DURATION_PARTS: [&str; 4] = ["D", "H", "M", "S"];
1005#[cfg(feature = "dtype-duration")]
1006const SIZES_NS: [i64; 4] = [
1007    86_400_000_000_000, // per day
1008    3_600_000_000_000,  // per hour
1009    60_000_000_000,     // per minute
1010    1_000_000_000,      // per second
1011];
1012#[cfg(feature = "dtype-duration")]
1013const SIZES_US: [i64; 4] = [86_400_000_000, 3_600_000_000, 60_000_000, 1_000_000];
1014#[cfg(feature = "dtype-duration")]
1015const SIZES_MS: [i64; 4] = [86_400_000, 3_600_000, 60_000, 1_000];
1016
1017#[cfg(feature = "dtype-duration")]
1018pub fn fmt_duration_string<W: Write>(f: &mut W, v: i64, unit: TimeUnit) -> fmt::Result {
1019    // take the physical/integer duration value and return a
1020    // friendly/readable duration string, eg: "3d 22m 55s 1ms"
1021    if v == 0 {
1022        return match unit {
1023            TimeUnit::Nanoseconds => f.write_str("0ns"),
1024            TimeUnit::Microseconds => f.write_str("0µs"),
1025            TimeUnit::Milliseconds => f.write_str("0ms"),
1026        };
1027    };
1028    // iterate over dtype-specific sizes to appropriately scale
1029    // and extract 'days', 'hours', 'minutes', and 'seconds' parts.
1030    let sizes = match unit {
1031        TimeUnit::Nanoseconds => SIZES_NS.as_slice(),
1032        TimeUnit::Microseconds => SIZES_US.as_slice(),
1033        TimeUnit::Milliseconds => SIZES_MS.as_slice(),
1034    };
1035    let mut buffer = itoa::Buffer::new();
1036    for (i, &size) in sizes.iter().enumerate() {
1037        let whole_num = if i == 0 {
1038            v / size
1039        } else {
1040            (v % sizes[i - 1]) / size
1041        };
1042        if whole_num != 0 {
1043            f.write_str(buffer.format(whole_num))?;
1044            f.write_str(DURATION_PARTS[i])?;
1045            if v % size != 0 {
1046                f.write_char(' ')?;
1047            }
1048        }
1049    }
1050    // write fractional seconds as integer nano/micro/milliseconds.
1051    let (v, units) = match unit {
1052        TimeUnit::Nanoseconds => (v % 1_000_000_000, ["ns", "µs", "ms"]),
1053        TimeUnit::Microseconds => (v % 1_000_000, ["µs", "ms", ""]),
1054        TimeUnit::Milliseconds => (v % 1_000, ["ms", "", ""]),
1055    };
1056    if v != 0 {
1057        let (value, suffix) = if v % 1_000 != 0 {
1058            (v, units[0])
1059        } else if v % 1_000_000 != 0 {
1060            (v / 1_000, units[1])
1061        } else {
1062            (v / 1_000_000, units[2])
1063        };
1064        f.write_str(buffer.format(value))?;
1065        f.write_str(suffix)?;
1066    }
1067    Ok(())
1068}
1069
1070#[cfg(feature = "dtype-duration")]
1071pub fn iso_duration_string(s: &mut String, mut v: i64, unit: TimeUnit) {
1072    if v == 0 {
1073        s.push_str("PT0S");
1074        return;
1075    }
1076    let mut buffer = itoa::Buffer::new();
1077    let mut wrote_part = false;
1078    if v < 0 {
1079        // negative sign before "P" indicates entire ISO duration is negative.
1080        s.push_str("-P");
1081        v = v.abs();
1082    } else {
1083        s.push('P');
1084    }
1085    // iterate over dtype-specific sizes to appropriately scale
1086    // and extract 'days', 'hours', 'minutes', and 'seconds' parts.
1087    let sizes = match unit {
1088        TimeUnit::Nanoseconds => SIZES_NS.as_slice(),
1089        TimeUnit::Microseconds => SIZES_US.as_slice(),
1090        TimeUnit::Milliseconds => SIZES_MS.as_slice(),
1091    };
1092    for (i, &size) in sizes.iter().enumerate() {
1093        let whole_num = if i == 0 {
1094            v / size
1095        } else {
1096            (v % sizes[i - 1]) / size
1097        };
1098        if whole_num != 0 || i == 3 {
1099            if i != 3 {
1100                // days, hours, minutes
1101                s.push_str(buffer.format(whole_num));
1102                s.push_str(ISO_DURATION_PARTS[i]);
1103            } else {
1104                // (index 3 => 'seconds' part): the ISO version writes
1105                // fractional seconds, not integer nano/micro/milliseconds.
1106                // if zero, only write out if no other parts written yet.
1107                let fractional_part = v % size;
1108                if whole_num == 0 && fractional_part == 0 {
1109                    if !wrote_part {
1110                        s.push_str("0S")
1111                    }
1112                } else {
1113                    s.push_str(buffer.format(whole_num));
1114                    if fractional_part != 0 {
1115                        let secs = match unit {
1116                            TimeUnit::Nanoseconds => format!(".{fractional_part:09}"),
1117                            TimeUnit::Microseconds => format!(".{fractional_part:06}"),
1118                            TimeUnit::Milliseconds => format!(".{fractional_part:03}"),
1119                        };
1120                        s.push_str(secs.trim_end_matches('0'));
1121                    }
1122                    s.push_str(ISO_DURATION_PARTS[i]);
1123                }
1124            }
1125            // (index 0 => 'days' part): after writing days above (if non-zero)
1126            // the ISO duration string requires a `T` before the time part.
1127            if i == 0 {
1128                s.push('T');
1129            }
1130            wrote_part = true;
1131        } else if i == 0 {
1132            // always need to write the `T` separator for ISO
1133            // durations, even if there is no 'days' part.
1134            s.push('T');
1135        }
1136    }
1137    // if there was only a 'days' component, no need for time separator.
1138    if s.ends_with('T') {
1139        s.pop();
1140    }
1141}
1142
1143fn format_blob(f: &mut Formatter<'_>, bytes: &[u8]) -> fmt::Result {
1144    let ellipsis = get_ellipsis();
1145    let width = get_str_len_limit() * 2;
1146    write!(f, "b\"")?;
1147
1148    for b in bytes.iter().take(width) {
1149        if b.is_ascii_alphanumeric() || b.is_ascii_punctuation() {
1150            write!(f, "{}", *b as char)?;
1151        } else {
1152            write!(f, "\\x{b:02x}")?;
1153        }
1154    }
1155    if bytes.len() > width {
1156        write!(f, "\"{ellipsis}")?;
1157    } else {
1158        f.write_str("\"")?;
1159    }
1160    Ok(())
1161}
1162
1163impl Display for AnyValue<'_> {
1164    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1165        let width = 0;
1166        match self {
1167            AnyValue::Null => write!(f, "null"),
1168            AnyValue::UInt8(v) => fmt_integer(f, width, *v),
1169            AnyValue::UInt16(v) => fmt_integer(f, width, *v),
1170            AnyValue::UInt32(v) => fmt_integer(f, width, *v),
1171            AnyValue::UInt64(v) => fmt_integer(f, width, *v),
1172            AnyValue::UInt128(v) => feature_gated!("dtype-u128", fmt_integer(f, width, *v)),
1173            AnyValue::Int8(v) => fmt_integer(f, width, *v),
1174            AnyValue::Int16(v) => fmt_integer(f, width, *v),
1175            AnyValue::Int32(v) => fmt_integer(f, width, *v),
1176            AnyValue::Int64(v) => fmt_integer(f, width, *v),
1177            AnyValue::Int128(v) => feature_gated!("dtype-i128", fmt_integer(f, width, *v)),
1178            AnyValue::Float16(v) => feature_gated!("dtype-f16", fmt_float(f, width, *v)),
1179            AnyValue::Float32(v) => fmt_float(f, width, *v),
1180            AnyValue::Float64(v) => fmt_float(f, width, *v),
1181            AnyValue::Boolean(v) => write!(f, "{}", *v),
1182            AnyValue::String(v) => write!(f, "{}", format_args!("\"{v}\"")),
1183            AnyValue::StringOwned(v) => write!(f, "{}", format_args!("\"{v}\"")),
1184            AnyValue::Binary(d) => format_blob(f, d),
1185            AnyValue::BinaryOwned(d) => format_blob(f, d),
1186            #[cfg(feature = "dtype-date")]
1187            AnyValue::Date(v) => write!(f, "{}", date32_to_date(*v)),
1188            #[cfg(feature = "dtype-datetime")]
1189            AnyValue::Datetime(v, tu, tz) => fmt_datetime(f, *v, *tu, *tz),
1190            #[cfg(feature = "dtype-datetime")]
1191            AnyValue::DatetimeOwned(v, tu, tz) => {
1192                fmt_datetime(f, *v, *tu, tz.as_ref().map(|v| v.as_ref()))
1193            },
1194            #[cfg(feature = "dtype-duration")]
1195            AnyValue::Duration(v, tu) => fmt_duration_string(f, *v, *tu),
1196            #[cfg(feature = "dtype-time")]
1197            AnyValue::Time(_) => {
1198                let nt: chrono::NaiveTime = self.into();
1199                write!(f, "{nt}")
1200            },
1201            #[cfg(feature = "dtype-categorical")]
1202            AnyValue::Categorical(_, _)
1203            | AnyValue::CategoricalOwned(_, _)
1204            | AnyValue::Enum(_, _)
1205            | AnyValue::EnumOwned(_, _) => {
1206                let s = self.get_str().unwrap();
1207                write!(f, "\"{s}\"")
1208            },
1209            #[cfg(feature = "dtype-array")]
1210            AnyValue::Array(s, _size) => write!(f, "{}", s.fmt_list()),
1211            AnyValue::List(s) => write!(f, "{}", s.fmt_list()),
1212            #[cfg(feature = "object")]
1213            AnyValue::Object(v) => write!(f, "{v}"),
1214            #[cfg(feature = "object")]
1215            AnyValue::ObjectOwned(v) => write!(f, "{}", v.0.as_ref()),
1216            #[cfg(feature = "dtype-struct")]
1217            av @ AnyValue::Struct(_, _, _) => {
1218                let mut avs = vec![];
1219                av._materialize_struct_av(&mut avs);
1220                fmt_struct(f, &avs)
1221            },
1222            #[cfg(feature = "dtype-struct")]
1223            AnyValue::StructOwned(payload) => fmt_struct(f, &payload.0),
1224            #[cfg(feature = "dtype-decimal")]
1225            AnyValue::Decimal(v, _prec, scale) => fmt_decimal(f, *v, *scale),
1226        }
1227    }
1228}
1229
1230/// Utility struct to format a timezone aware datetime.
1231#[allow(dead_code)]
1232#[cfg(feature = "dtype-datetime")]
1233pub struct PlTzAware<'a> {
1234    ndt: NaiveDateTime,
1235    tz: &'a str,
1236}
1237#[cfg(feature = "dtype-datetime")]
1238impl<'a> PlTzAware<'a> {
1239    pub fn new(ndt: NaiveDateTime, tz: &'a str) -> Self {
1240        Self { ndt, tz }
1241    }
1242}
1243
1244#[cfg(feature = "dtype-datetime")]
1245impl Display for PlTzAware<'_> {
1246    #[allow(unused_variables)]
1247    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1248        #[cfg(feature = "timezones")]
1249        match self.tz.parse::<chrono_tz::Tz>() {
1250            Ok(tz) => {
1251                let dt_utc = chrono::Utc.from_local_datetime(&self.ndt).unwrap();
1252                let dt_tz_aware = dt_utc.with_timezone(&tz);
1253                write!(f, "{dt_tz_aware}")
1254            },
1255            Err(_) => write!(f, "invalid timezone"),
1256        }
1257        #[cfg(not(feature = "timezones"))]
1258        {
1259            panic!("activate 'timezones' feature")
1260        }
1261    }
1262}
1263
1264#[cfg(feature = "dtype-struct")]
1265fn fmt_struct(f: &mut Formatter<'_>, vals: &[AnyValue]) -> fmt::Result {
1266    write!(f, "{{")?;
1267    if !vals.is_empty() {
1268        for v in &vals[..vals.len() - 1] {
1269            write!(f, "{v},")?;
1270        }
1271        // last value has no trailing comma
1272        write!(f, "{}", vals[vals.len() - 1])?;
1273    }
1274    write!(f, "}}")
1275}
1276
1277impl Series {
1278    pub fn fmt_list(&self) -> String {
1279        assert!(
1280            !self.dtype().is_object(),
1281            "nested Objects are not allowed\n\nYou probably got here by not setting a `return_dtype` on a UDF on Objects."
1282        );
1283        if self.is_empty() {
1284            return "[]".to_owned();
1285        }
1286        let mut result = "[".to_owned();
1287        let max_items = get_list_len_limit();
1288        let ellipsis = get_ellipsis();
1289
1290        match max_items {
1291            0 => write!(result, "{ellipsis}]").unwrap(),
1292            _ if max_items >= self.len() => {
1293                // this will always leave a trailing ", " after the last item
1294                // but for long lists, this is faster than checking against the length each time
1295                for item in self.rechunk().iter() {
1296                    write!(result, "{item}, ").unwrap();
1297                }
1298                // remove trailing ", " and replace with closing brace
1299                result.truncate(result.len() - 2);
1300                result.push(']');
1301            },
1302            _ => {
1303                let s = self.slice(0, max_items);
1304                for (i, item) in s.iter().enumerate() {
1305                    if i == max_items.saturating_sub(1) {
1306                        write!(result, "{ellipsis} {}", self.get(self.len() - 1).unwrap()).unwrap();
1307                        break;
1308                    } else {
1309                        write!(result, "{item}, ").unwrap();
1310                    }
1311                }
1312                result.push(']');
1313            },
1314        };
1315        result
1316    }
1317}
1318
1319#[inline]
1320#[cfg(feature = "dtype-decimal")]
1321fn fmt_decimal(f: &mut Formatter<'_>, v: i128, scale: usize) -> fmt::Result {
1322    let mut fmt_buf = polars_compute::decimal::DecimalFmtBuffer::new();
1323    let trim_zeros = get_trim_decimal_zeros();
1324    f.write_str(fmt_float_string(fmt_buf.format_dec128(v, scale, trim_zeros, false)).as_str())
1325}
1326
1327#[cfg(all(
1328    test,
1329    feature = "temporal",
1330    feature = "dtype-date",
1331    feature = "dtype-datetime"
1332))]
1333#[allow(unsafe_op_in_unsafe_fn)]
1334mod test {
1335    use crate::prelude::*;
1336
1337    #[test]
1338    fn test_fmt_list() {
1339        let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1340            PlSmallStr::from_static("a"),
1341            10,
1342            10,
1343            DataType::Int32,
1344        );
1345        builder.append_opt_slice(Some(&[1, 2, 3, 4, 5, 6]));
1346        builder.append_opt_slice(None);
1347        let list_long = builder.finish().into_series();
1348
1349        assert_eq!(
1350            r#"shape: (2,)
1351Series: 'a' [list[i32]]
1352[
1353	[1, 2, … 6]
1354	null
1355]"#,
1356            format!("{list_long:?}")
1357        );
1358
1359        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "10") };
1360
1361        assert_eq!(
1362            r#"shape: (2,)
1363Series: 'a' [list[i32]]
1364[
1365	[1, 2, 3, 4, 5, 6]
1366	null
1367]"#,
1368            format!("{list_long:?}")
1369        );
1370
1371        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1372
1373        assert_eq!(
1374            r#"shape: (2,)
1375Series: 'a' [list[i32]]
1376[
1377	[1, 2, 3, 4, 5, 6]
1378	null
1379]"#,
1380            format!("{list_long:?}")
1381        );
1382
1383        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1384
1385        assert_eq!(
1386            r#"shape: (2,)
1387Series: 'a' [list[i32]]
1388[
1389	[…]
1390	null
1391]"#,
1392            format!("{list_long:?}")
1393        );
1394
1395        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "1") };
1396
1397        assert_eq!(
1398            r#"shape: (2,)
1399Series: 'a' [list[i32]]
1400[
1401	[… 6]
1402	null
1403]"#,
1404            format!("{list_long:?}")
1405        );
1406
1407        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "4") };
1408
1409        assert_eq!(
1410            r#"shape: (2,)
1411Series: 'a' [list[i32]]
1412[
1413	[1, 2, 3, … 6]
1414	null
1415]"#,
1416            format!("{list_long:?}")
1417        );
1418
1419        let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1420            PlSmallStr::from_static("a"),
1421            10,
1422            10,
1423            DataType::Int32,
1424        );
1425        builder.append_opt_slice(Some(&[1]));
1426        builder.append_opt_slice(None);
1427        let list_short = builder.finish().into_series();
1428
1429        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1430
1431        assert_eq!(
1432            r#"shape: (2,)
1433Series: 'a' [list[i32]]
1434[
1435	[1]
1436	null
1437]"#,
1438            format!("{list_short:?}")
1439        );
1440
1441        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1442
1443        assert_eq!(
1444            r#"shape: (2,)
1445Series: 'a' [list[i32]]
1446[
1447	[…]
1448	null
1449]"#,
1450            format!("{list_short:?}")
1451        );
1452
1453        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1454
1455        assert_eq!(
1456            r#"shape: (2,)
1457Series: 'a' [list[i32]]
1458[
1459	[1]
1460	null
1461]"#,
1462            format!("{list_short:?}")
1463        );
1464
1465        let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1466            PlSmallStr::from_static("a"),
1467            10,
1468            10,
1469            DataType::Int32,
1470        );
1471        builder.append_opt_slice(Some(&[]));
1472        builder.append_opt_slice(None);
1473        let list_empty = builder.finish().into_series();
1474
1475        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1476
1477        assert_eq!(
1478            r#"shape: (2,)
1479Series: 'a' [list[i32]]
1480[
1481	[]
1482	null
1483]"#,
1484            format!("{list_empty:?}")
1485        );
1486    }
1487
1488    #[test]
1489    fn test_fmt_temporal() {
1490        let s = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)])
1491            .into_date();
1492        assert_eq!(
1493            r#"shape: (3,)
1494Series: 'Date' [date]
1495[
1496	1970-01-02
1497	null
1498	1970-01-04
1499]"#,
1500            format!("{:?}", s.into_series())
1501        );
1502
1503        let s = Int64Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(1_000_000_000_000)])
1504            .into_datetime(TimeUnit::Nanoseconds, None);
1505        assert_eq!(
1506            r#"shape: (3,)
1507Series: '' [datetime[ns]]
1508[
1509	1970-01-01 00:00:00.000000001
1510	null
1511	1970-01-01 00:16:40
1512]"#,
1513            format!("{:?}", s.into_series())
1514        );
1515    }
1516
1517    #[test]
1518    fn test_fmt_chunkedarray() {
1519        let ca = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)]);
1520        assert_eq!(
1521            r#"shape: (3,)
1522ChunkedArray: 'Date' [i32]
1523[
1524	1
1525	null
1526	3
1527]"#,
1528            format!("{ca:?}")
1529        );
1530        let ca = StringChunked::new(PlSmallStr::from_static("name"), &["a", "b"]);
1531        assert_eq!(
1532            r#"shape: (2,)
1533ChunkedArray: 'name' [str]
1534[
1535	"a"
1536	"b"
1537]"#,
1538            format!("{ca:?}")
1539        );
1540    }
1541}