polars_core/
fmt.rs

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