Skip to main content

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::num::IntErrorKind;
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 as a limit or set a default.
98///
99/// Negative values (e.g. -1) are parsed as 'no limit' or [`usize::MAX`].
100fn parse_env_var_limit(name: &str, default: usize) -> usize {
101    let Ok(v) = std::env::var(name) else {
102        return default;
103    };
104
105    let n = match v.parse::<i64>() {
106        Ok(n) => n,
107        Err(e) => match e.kind() {
108            IntErrorKind::PosOverflow | IntErrorKind::NegOverflow => -1,
109            _ => return default,
110        },
111    };
112
113    if n < 0 { usize::MAX } else { n as usize }
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().to_storage() {
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_static_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::UInt128 => {
354                feature_gated!(
355                    "dtype-u128",
356                    format_array!(f, self.u128().unwrap(), "u128", self.name(), "Series")
357                )
358            },
359            DataType::Int8 => {
360                format_array!(f, self.i8().unwrap(), "i8", self.name(), "Series")
361            },
362            DataType::Int16 => {
363                format_array!(f, self.i16().unwrap(), "i16", self.name(), "Series")
364            },
365            DataType::Int32 => {
366                format_array!(f, self.i32().unwrap(), "i32", self.name(), "Series")
367            },
368            DataType::Int64 => {
369                format_array!(f, self.i64().unwrap(), "i64", self.name(), "Series")
370            },
371            DataType::Int128 => {
372                feature_gated!(
373                    "dtype-i128",
374                    format_array!(f, self.i128().unwrap(), "i128", self.name(), "Series")
375                )
376            },
377            #[cfg(feature = "dtype-f16")]
378            DataType::Float16 => {
379                format_array!(f, self.f16().unwrap(), "f16", self.name(), "Series")
380            },
381            DataType::Float32 => {
382                format_array!(f, self.f32().unwrap(), "f32", self.name(), "Series")
383            },
384            DataType::Float64 => {
385                format_array!(f, self.f64().unwrap(), "f64", self.name(), "Series")
386            },
387            #[cfg(feature = "dtype-date")]
388            DataType::Date => format_array!(f, self.date().unwrap(), "date", self.name(), "Series"),
389            #[cfg(feature = "dtype-datetime")]
390            DataType::Datetime(_, _) => {
391                let dt = format!("{}", self.dtype());
392                format_array!(f, self.datetime().unwrap(), &dt, self.name(), "Series")
393            },
394            #[cfg(feature = "dtype-time")]
395            DataType::Time => format_array!(f, self.time().unwrap(), "time", self.name(), "Series"),
396            #[cfg(feature = "dtype-duration")]
397            DataType::Duration(_) => {
398                let dt = format!("{}", self.dtype());
399                format_array!(f, self.duration().unwrap(), &dt, self.name(), "Series")
400            },
401            #[cfg(feature = "dtype-decimal")]
402            DataType::Decimal(_, _) => {
403                let dt = format!("{}", self.dtype());
404                format_array!(f, self.decimal().unwrap(), &dt, self.name(), "Series")
405            },
406            #[cfg(feature = "dtype-array")]
407            DataType::Array(_, _) => {
408                let dt = format!("{}", self.dtype());
409                format_array!(f, self.array().unwrap(), &dt, self.name(), "Series")
410            },
411            DataType::List(_) => {
412                let dt = format!("{}", self.dtype());
413                format_array!(f, self.list().unwrap(), &dt, self.name(), "Series")
414            },
415            #[cfg(feature = "object")]
416            DataType::Object(_) => format_object_array(f, self, self.name(), "Series"),
417            #[cfg(feature = "dtype-categorical")]
418            DataType::Categorical(cats, _) => {
419                with_match_categorical_physical_type!(cats.physical(), |$C| {
420                    format_array!(f, self.cat::<$C>().unwrap(), "cat", self.name(), "Series")
421                })
422            },
423
424            #[cfg(feature = "dtype-categorical")]
425            DataType::Enum(fcats, _) => {
426                with_match_categorical_physical_type!(fcats.physical(), |$C| {
427                    format_array!(f, self.cat::<$C>().unwrap(), "enum", self.name(), "Series")
428                })
429            },
430            #[cfg(feature = "dtype-struct")]
431            dt @ DataType::Struct(_) => format_array!(
432                f,
433                self.struct_().unwrap(),
434                format!("{dt}"),
435                self.name(),
436                "Series"
437            ),
438            DataType::Null => {
439                format_array!(f, self.null().unwrap(), "null", self.name(), "Series")
440            },
441            DataType::Binary => {
442                format_array!(f, self.binary().unwrap(), "binary", self.name(), "Series")
443            },
444            DataType::BinaryOffset => {
445                format_array!(
446                    f,
447                    self.binary_offset().unwrap(),
448                    "binary[offset]",
449                    self.name(),
450                    "Series"
451                )
452            },
453            #[cfg(feature = "dtype-map")]
454            DataType::Map(_, _) => {
455                let dt = format!("{}", self.dtype());
456                format_array!(f, self.map().unwrap(), &dt, self.name(), "Series")
457            },
458            #[cfg(feature = "dtype-extension")]
459            DataType::Extension(_, _) => {
460                let dt = format!("{}", self.dtype());
461                format_array!(f, self.ext().unwrap(), &dt, self.name(), "Series")
462            },
463            dt => panic!("{dt:?} not impl"),
464        }
465    }
466}
467
468impl Display for Series {
469    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
470        Debug::fmt(self, f)
471    }
472}
473
474impl Debug for DataFrame {
475    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
476        Display::fmt(self, f)
477    }
478}
479#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
480fn make_str_val(v: &str, truncate: usize, ellipsis: &String) -> String {
481    let v_trunc = &v[..v
482        .char_indices()
483        .take(truncate)
484        .last()
485        .map(|(i, c)| i + c.len_utf8())
486        .unwrap_or(0)];
487    if v == v_trunc {
488        v.to_string()
489    } else {
490        format!("{v_trunc}{ellipsis}")
491    }
492}
493
494#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
495fn field_to_str(
496    f: &Field,
497    str_truncate: usize,
498    ellipsis: &String,
499    padding: usize,
500) -> (String, usize) {
501    let name = make_str_val(f.name(), str_truncate, ellipsis);
502    let name_length = estimate_string_width(name.as_str());
503    let mut column_name = name;
504    if env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES) {
505        column_name = "".to_string();
506    }
507    let column_dtype = if env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES) {
508        "".to_string()
509    } else if env_is_true(FMT_TABLE_INLINE_COLUMN_DATA_TYPE)
510        | env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
511    {
512        format!("{}", f.dtype())
513    } else {
514        format!("\n{}", f.dtype())
515    };
516    let mut dtype_length = column_dtype.trim_start().len();
517    let mut separator = "\n---";
518    if env_is_true(FMT_TABLE_HIDE_COLUMN_SEPARATOR)
519        | env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
520        | env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES)
521    {
522        separator = ""
523    }
524    let s = if env_is_true(FMT_TABLE_INLINE_COLUMN_DATA_TYPE)
525        & !env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES)
526    {
527        let inline_name_dtype = format!("{column_name} ({column_dtype})");
528        dtype_length = inline_name_dtype.len();
529        inline_name_dtype
530    } else {
531        format!("{column_name}{separator}{column_dtype}")
532    };
533    let mut s_len = std::cmp::max(name_length, dtype_length);
534    let separator_length = estimate_string_width(separator.trim());
535    if s_len < separator_length {
536        s_len = separator_length;
537    }
538    (s, s_len + padding)
539}
540
541#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
542fn prepare_row(
543    row: Vec<Cow<'_, str>>,
544    n_first: usize,
545    n_last: usize,
546    str_truncate: usize,
547    max_elem_lengths: &mut [usize],
548    ellipsis: &String,
549    padding: usize,
550) -> Vec<String> {
551    let reduce_columns = n_first + n_last < row.len();
552    let n_elems = n_first + n_last + reduce_columns as usize;
553    let mut row_strings = Vec::with_capacity(n_elems);
554
555    for (idx, v) in row[0..n_first].iter().enumerate() {
556        let elem_str = make_str_val(v, str_truncate, ellipsis);
557        let elem_len = estimate_string_width(elem_str.as_str()) + padding;
558        if max_elem_lengths[idx] < elem_len {
559            max_elem_lengths[idx] = elem_len;
560        };
561        row_strings.push(elem_str);
562    }
563    if reduce_columns {
564        row_strings.push(ellipsis.to_string());
565        max_elem_lengths[n_first] = ellipsis.chars().count() + padding;
566    }
567    let elem_offset = n_first + reduce_columns as usize;
568    for (idx, v) in row[row.len() - n_last..].iter().enumerate() {
569        let elem_str = make_str_val(v, str_truncate, ellipsis);
570        let elem_len = estimate_string_width(elem_str.as_str()) + padding;
571        let elem_idx = elem_offset + idx;
572        if max_elem_lengths[elem_idx] < elem_len {
573            max_elem_lengths[elem_idx] = elem_len;
574        };
575        row_strings.push(elem_str);
576    }
577    row_strings
578}
579
580#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
581fn env_is_true(varname: &str) -> bool {
582    std::env::var(varname).as_deref().unwrap_or("0") == "1"
583}
584
585#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
586fn fmt_df_shape((shape0, shape1): &(usize, usize)) -> String {
587    // e.g. (1_000_000, 4_000)
588    format!(
589        "({}, {})",
590        fmt_int_string_custom(&shape0.to_string(), 3, "_"),
591        fmt_int_string_custom(&shape1.to_string(), 3, "_")
592    )
593}
594
595impl Display for DataFrame {
596    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
597        #[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
598        {
599            let height = self.height();
600            assert!(
601                self.columns().iter().all(|s| s.len() == height),
602                "The column lengths in the DataFrame are not equal."
603            );
604
605            let table_style = std::env::var(FMT_TABLE_FORMATTING).unwrap_or("DEFAULT".to_string());
606            let is_utf8 = !table_style.starts_with("ASCII");
607            let preset = match table_style.as_str() {
608                "ASCII_FULL" => ASCII_FULL,
609                "ASCII_FULL_CONDENSED" => ASCII_FULL_CONDENSED,
610                "ASCII_NO_BORDERS" => ASCII_NO_BORDERS,
611                "ASCII_BORDERS_ONLY" => ASCII_BORDERS_ONLY,
612                "ASCII_BORDERS_ONLY_CONDENSED" => ASCII_BORDERS_ONLY_CONDENSED,
613                "ASCII_HORIZONTAL_ONLY" => ASCII_HORIZONTAL_ONLY,
614                "ASCII_MARKDOWN" | "MARKDOWN" => ASCII_MARKDOWN,
615                "UTF8_FULL" => UTF8_FULL,
616                "UTF8_FULL_CONDENSED" => UTF8_FULL_CONDENSED,
617                "UTF8_NO_BORDERS" => UTF8_NO_BORDERS,
618                "UTF8_BORDERS_ONLY" => UTF8_BORDERS_ONLY,
619                "UTF8_HORIZONTAL_ONLY" => UTF8_HORIZONTAL_ONLY,
620                "NOTHING" => NOTHING,
621                _ => UTF8_FULL_CONDENSED,
622            };
623            let ellipsis = get_ellipsis().to_string();
624            let ellipsis_len = ellipsis.chars().count();
625            let max_n_cols = get_col_limit();
626            let max_n_rows = get_row_limit();
627            let str_truncate = get_str_len_limit();
628            let padding = 2; // eg: one char either side of the value
629
630            let (n_first, n_last) = if self.width() > max_n_cols {
631                (max_n_cols.div_ceil(2), max_n_cols / 2)
632            } else {
633                (self.width(), 0)
634            };
635            let reduce_columns = n_first + n_last < self.width();
636            let n_tbl_cols = n_first + n_last + reduce_columns as usize;
637            let mut names = Vec::with_capacity(n_tbl_cols);
638            let mut name_lengths = Vec::with_capacity(n_tbl_cols);
639
640            let fields = self.fields();
641            for field in fields[0..n_first].iter() {
642                let (s, l) = field_to_str(field, str_truncate, &ellipsis, padding);
643                names.push(s);
644                name_lengths.push(l);
645            }
646            if reduce_columns {
647                names.push(ellipsis.clone());
648                name_lengths.push(ellipsis_len);
649            }
650            for field in fields[self.width() - n_last..].iter() {
651                let (s, l) = field_to_str(field, str_truncate, &ellipsis, padding);
652                names.push(s);
653                name_lengths.push(l);
654            }
655
656            let mut table = Table::new();
657            table
658                .load_preset(preset)
659                .set_content_arrangement(ContentArrangement::Dynamic);
660
661            if is_utf8 && env_is_true(FMT_TABLE_ROUNDED_CORNERS) {
662                table.apply_modifier(UTF8_ROUND_CORNERS);
663            }
664            let mut constraints = Vec::with_capacity(n_tbl_cols);
665            let mut max_elem_lengths: Vec<usize> = vec![0; n_tbl_cols];
666
667            if max_n_rows > 0 {
668                if height > max_n_rows {
669                    // Truncate the table if we have more rows than the
670                    // configured maximum number of rows
671                    let mut rows = Vec::with_capacity(std::cmp::max(max_n_rows, 2));
672                    let half = max_n_rows / 2;
673                    let rest = max_n_rows % 2;
674
675                    for i in 0..(half + rest) {
676                        let row = self
677                            .columns()
678                            .iter()
679                            .map(|c| c.str_value(i).unwrap())
680                            .collect();
681
682                        let row_strings = prepare_row(
683                            row,
684                            n_first,
685                            n_last,
686                            str_truncate,
687                            &mut max_elem_lengths,
688                            &ellipsis,
689                            padding,
690                        );
691                        rows.push(row_strings);
692                    }
693                    let dots = vec![ellipsis.clone(); rows[0].len()];
694                    rows.push(dots);
695
696                    for i in (height - half)..height {
697                        let row = self
698                            .columns()
699                            .iter()
700                            .map(|c| c.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                        rows.push(row_strings);
713                    }
714                    table.add_rows(rows);
715                } else {
716                    for i in 0..height {
717                        if self.width() > 0 {
718                            let row = self
719                                .materialized_column_iter()
720                                .map(|s| s.str_value(i).unwrap())
721                                .collect();
722
723                            let row_strings = prepare_row(
724                                row,
725                                n_first,
726                                n_last,
727                                str_truncate,
728                                &mut max_elem_lengths,
729                                &ellipsis,
730                                padding,
731                            );
732                            table.add_row(row_strings);
733                        } else {
734                            break;
735                        }
736                    }
737                }
738            } else if height > 0 {
739                let dots: Vec<String> = vec![ellipsis; self.width()];
740                table.add_row(dots);
741            }
742            let tbl_fallback_width = 100;
743            let tbl_width = std::env::var("POLARS_TABLE_WIDTH")
744                .map(|s| {
745                    let n = s
746                        .parse::<i64>()
747                        .expect("could not parse table width argument");
748                    let w = if n < 0 {
749                        u16::MAX
750                    } else {
751                        u16::try_from(n).expect("table width argument does not fit in u16")
752                    };
753                    Some(w)
754                })
755                .unwrap_or(None);
756
757            // column width constraints
758            let col_width_exact =
759                |w: usize| ColumnConstraint::Absolute(comfy_table::Width::Fixed(w as u16));
760            let col_width_bounds = |l: usize, u: usize| ColumnConstraint::Boundaries {
761                lower: Width::Fixed(l as u16),
762                upper: Width::Fixed(u as u16),
763            };
764            let min_col_width = std::cmp::max(5, 3 + padding);
765            for (idx, elem_len) in max_elem_lengths.iter().enumerate() {
766                let mx = std::cmp::min(
767                    str_truncate + ellipsis_len + padding,
768                    std::cmp::max(name_lengths[idx], *elem_len),
769                );
770                if (mx <= min_col_width) && !(max_n_rows > 0 && height > max_n_rows) {
771                    // col width is less than min width + table is not truncated
772                    constraints.push(col_width_exact(mx));
773                } else if mx <= min_col_width {
774                    // col width is less than min width + table is truncated (w/ ellipsis)
775                    constraints.push(col_width_bounds(mx, min_col_width));
776                } else {
777                    constraints.push(col_width_bounds(min_col_width, mx));
778                }
779            }
780
781            // insert a header row, unless both column names and dtypes are hidden
782            if !(env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
783                && env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES))
784            {
785                table.set_header(names).set_constraints(constraints);
786            }
787
788            // if tbl_width is explicitly set, use it
789            if let Some(w) = tbl_width {
790                table.set_width(w);
791            } else {
792                // if no tbl_width (it's not tty && width not explicitly set), apply
793                // a default value; this is needed to support non-tty applications
794                #[cfg(feature = "fmt")]
795                if table.width().is_none() && !table.is_tty() {
796                    table.set_width(tbl_fallback_width);
797                }
798                #[cfg(feature = "fmt_no_tty")]
799                if table.width().is_none() {
800                    table.set_width(tbl_fallback_width);
801                }
802            }
803
804            // set alignment of cells, if defined
805            if std::env::var(FMT_TABLE_CELL_ALIGNMENT).is_ok()
806                | std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT).is_ok()
807            {
808                let str_preset = std::env::var(FMT_TABLE_CELL_ALIGNMENT)
809                    .unwrap_or_else(|_| "DEFAULT".to_string());
810                let num_preset = std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT)
811                    .unwrap_or_else(|_| str_preset.to_string());
812                for (column_index, column) in table.column_iter_mut().enumerate() {
813                    let dtype = fields[column_index].dtype();
814                    let mut preset = str_preset.as_str();
815                    if dtype.is_primitive_numeric() || dtype.is_decimal() {
816                        preset = num_preset.as_str();
817                    }
818                    match preset {
819                        "RIGHT" => column.set_cell_alignment(CellAlignment::Right),
820                        "LEFT" => column.set_cell_alignment(CellAlignment::Left),
821                        "CENTER" => column.set_cell_alignment(CellAlignment::Center),
822                        _ => {},
823                    }
824                }
825            }
826
827            // establish 'shape' information (above/below/hidden)
828            if env_is_true(FMT_TABLE_HIDE_DATAFRAME_SHAPE_INFORMATION) {
829                write!(f, "{table}")?;
830            } else {
831                let shape_str = fmt_df_shape(&self.shape());
832                if env_is_true(FMT_TABLE_DATAFRAME_SHAPE_BELOW) {
833                    write!(f, "{table}\nshape: {shape_str}")?;
834                } else {
835                    write!(f, "shape: {shape_str}\n{table}")?;
836                }
837            }
838        }
839        #[cfg(not(any(feature = "fmt", feature = "fmt_no_tty")))]
840        {
841            write!(
842                f,
843                "shape: {:?}\nto see more, compile with the 'fmt' or 'fmt_no_tty' feature",
844                self.shape()
845            )?;
846        }
847        Ok(())
848    }
849}
850
851fn fmt_int_string_custom(num: &str, group_size: u8, group_separator: &str) -> String {
852    if group_size == 0 || num.len() <= 1 {
853        num.to_string()
854    } else {
855        let mut out = String::new();
856        let sign_offset = if num.starts_with('-') || num.starts_with('+') {
857            out.push(num.chars().next().unwrap());
858            1
859        } else {
860            0
861        };
862        let int_body = &num.as_bytes()[sign_offset..]
863            .rchunks(group_size as usize)
864            .rev()
865            .map(str::from_utf8)
866            .collect::<Result<Vec<&str>, _>>()
867            .unwrap()
868            .join(group_separator);
869        out.push_str(int_body);
870        out
871    }
872}
873
874fn fmt_int_string(num: &str) -> String {
875    fmt_int_string_custom(num, 3, &get_thousands_separator())
876}
877
878fn fmt_float_string_custom(
879    num: &str,
880    group_size: u8,
881    group_separator: &str,
882    decimal: char,
883) -> String {
884    // Quick exit if no formatting would be applied
885    if num.len() <= 1 || (group_size == 0 && decimal == '.') {
886        num.to_string()
887    } else {
888        // Take existing numeric string and apply digit grouping & separator/decimal chars
889        // e.g. "1000000" → "1_000_000", "-123456.798" → "-123,456.789", etc
890        let (idx, has_fractional) = match num.find('.') {
891            Some(i) => (i, true),
892            None => (num.len(), false),
893        };
894        let mut out = String::new();
895        let integer_part = &num[..idx];
896
897        out.push_str(&fmt_int_string_custom(
898            integer_part,
899            group_size,
900            group_separator,
901        ));
902        if has_fractional {
903            out.push(decimal);
904            out.push_str(&num[idx + 1..]);
905        };
906        out
907    }
908}
909
910fn fmt_float_string(num: &str) -> String {
911    fmt_float_string_custom(num, 3, &get_thousands_separator(), get_decimal_separator())
912}
913
914fn fmt_integer<T: Num + NumCast + Display>(
915    f: &mut Formatter<'_>,
916    width: usize,
917    v: T,
918) -> fmt::Result {
919    write!(f, "{:>width$}", fmt_int_string(&v.to_string()))
920}
921
922const SCIENTIFIC_BOUND: f64 = 999999.0;
923
924fn fmt_float<T: Num + NumCast>(f: &mut Formatter<'_>, width: usize, v: T) -> fmt::Result {
925    let v: f64 = NumCast::from(v).unwrap();
926
927    let float_precision = get_float_precision();
928
929    if let Some(precision) = float_precision {
930        if format!("{v:.precision$}").len() > 19 {
931            return write!(f, "{v:>width$.precision$e}");
932        }
933        let s = format!("{v:>width$.precision$}");
934        return write!(f, "{}", fmt_float_string(s.as_str()));
935    }
936
937    if matches!(get_float_fmt(), FloatFmt::Full) {
938        let s = format!("{v:>width$}");
939        return write!(f, "{}", fmt_float_string(s.as_str()));
940    }
941
942    // show integers as 0.0, 1.0 ... 101.0
943    if v.fract() == 0.0 && v.abs() < SCIENTIFIC_BOUND {
944        let s = format!("{v:>width$.1}");
945        write!(f, "{}", fmt_float_string(s.as_str()))
946    } else if format!("{v}").len() > 9 {
947        // large and small floats in scientific notation.
948        // (note: scientific notation does not play well with digit grouping)
949        if (!(0.000001..=SCIENTIFIC_BOUND).contains(&v.abs()) | (v.abs() > SCIENTIFIC_BOUND))
950            && get_thousands_separator().is_empty()
951        {
952            let s = format!("{v:>width$.4e}");
953            write!(f, "{}", fmt_float_string(s.as_str()))
954        } else {
955            // this makes sure we don't write 12.00000 in case of a long flt that is 12.0000000001
956            // instead we write 12.0
957            let s = format!("{v:>width$.6}");
958
959            if s.ends_with('0') {
960                let mut s = s.as_str();
961                let mut len = s.len() - 1;
962
963                while s.ends_with('0') {
964                    s = &s[..len];
965                    len -= 1;
966                }
967                let s = if s.ends_with('.') {
968                    format!("{s}0")
969                } else {
970                    s.to_string()
971                };
972                write!(f, "{}", fmt_float_string(s.as_str()))
973            } else {
974                // 12.0934509341243124
975                // written as
976                // 12.09345
977                let s = format!("{v:>width$.6}");
978                write!(f, "{}", fmt_float_string(s.as_str()))
979            }
980        }
981    } else {
982        let s = if v.fract() == 0.0 {
983            format!("{v:>width$e}")
984        } else {
985            format!("{v:>width$}")
986        };
987        write!(f, "{}", fmt_float_string(s.as_str()))
988    }
989}
990
991#[cfg(feature = "dtype-datetime")]
992fn fmt_datetime(
993    f: &mut Formatter<'_>,
994    v: i64,
995    tu: TimeUnit,
996    tz: Option<&self::datatypes::TimeZone>,
997) -> fmt::Result {
998    let ndt = match tu {
999        TimeUnit::Nanoseconds => timestamp_ns_to_datetime(v),
1000        TimeUnit::Microseconds => timestamp_us_to_datetime(v),
1001        TimeUnit::Milliseconds => timestamp_ms_to_datetime(v),
1002    };
1003    match tz {
1004        None => std::fmt::Display::fmt(&ndt, f),
1005        Some(tz) => PlTzAware::new(ndt, tz).fmt(f),
1006    }
1007}
1008
1009#[cfg(feature = "dtype-duration")]
1010const DURATION_PARTS: [&str; 4] = ["d", "h", "m", "s"];
1011#[cfg(feature = "dtype-duration")]
1012const ISO_DURATION_PARTS: [&str; 4] = ["D", "H", "M", "S"];
1013#[cfg(feature = "dtype-duration")]
1014const SIZES_NS: [i64; 4] = [
1015    86_400_000_000_000, // per day
1016    3_600_000_000_000,  // per hour
1017    60_000_000_000,     // per minute
1018    1_000_000_000,      // per second
1019];
1020#[cfg(feature = "dtype-duration")]
1021const SIZES_US: [i64; 4] = [86_400_000_000, 3_600_000_000, 60_000_000, 1_000_000];
1022#[cfg(feature = "dtype-duration")]
1023const SIZES_MS: [i64; 4] = [86_400_000, 3_600_000, 60_000, 1_000];
1024
1025#[cfg(feature = "dtype-duration")]
1026pub fn fmt_duration_string<W: Write>(f: &mut W, v: i64, unit: TimeUnit) -> fmt::Result {
1027    // take the physical/integer duration value and return a
1028    // friendly/readable duration string, eg: "3d 22m 55s 1ms"
1029    if v == 0 {
1030        return match unit {
1031            TimeUnit::Nanoseconds => f.write_str("0ns"),
1032            TimeUnit::Microseconds => f.write_str("0µs"),
1033            TimeUnit::Milliseconds => f.write_str("0ms"),
1034        };
1035    };
1036    // iterate over dtype-specific sizes to appropriately scale
1037    // and extract 'days', 'hours', 'minutes', and 'seconds' parts.
1038    let sizes = match unit {
1039        TimeUnit::Nanoseconds => SIZES_NS.as_slice(),
1040        TimeUnit::Microseconds => SIZES_US.as_slice(),
1041        TimeUnit::Milliseconds => SIZES_MS.as_slice(),
1042    };
1043    let mut buffer = itoa::Buffer::new();
1044    for (i, &size) in sizes.iter().enumerate() {
1045        let whole_num = if i == 0 {
1046            v / size
1047        } else {
1048            (v % sizes[i - 1]) / size
1049        };
1050        if whole_num != 0 {
1051            f.write_str(buffer.format(whole_num))?;
1052            f.write_str(DURATION_PARTS[i])?;
1053            if v % size != 0 {
1054                f.write_char(' ')?;
1055            }
1056        }
1057    }
1058    // write fractional seconds as integer nano/micro/milliseconds.
1059    let (v, units) = match unit {
1060        TimeUnit::Nanoseconds => (v % 1_000_000_000, ["ns", "µs", "ms"]),
1061        TimeUnit::Microseconds => (v % 1_000_000, ["µs", "ms", ""]),
1062        TimeUnit::Milliseconds => (v % 1_000, ["ms", "", ""]),
1063    };
1064    if v != 0 {
1065        let (value, suffix) = if v % 1_000 != 0 {
1066            (v, units[0])
1067        } else if v % 1_000_000 != 0 {
1068            (v / 1_000, units[1])
1069        } else {
1070            (v / 1_000_000, units[2])
1071        };
1072        f.write_str(buffer.format(value))?;
1073        f.write_str(suffix)?;
1074    }
1075    Ok(())
1076}
1077
1078#[cfg(feature = "dtype-duration")]
1079pub fn iso_duration_string(s: &mut String, mut v: i64, unit: TimeUnit) {
1080    if v == 0 {
1081        s.push_str("PT0S");
1082        return;
1083    }
1084    let mut buffer = itoa::Buffer::new();
1085    let mut wrote_part = false;
1086    if v < 0 {
1087        // negative sign before "P" indicates entire ISO duration is negative.
1088        s.push_str("-P");
1089        v = v.abs();
1090    } else {
1091        s.push('P');
1092    }
1093    // iterate over dtype-specific sizes to appropriately scale
1094    // and extract 'days', 'hours', 'minutes', and 'seconds' parts.
1095    let sizes = match unit {
1096        TimeUnit::Nanoseconds => SIZES_NS.as_slice(),
1097        TimeUnit::Microseconds => SIZES_US.as_slice(),
1098        TimeUnit::Milliseconds => SIZES_MS.as_slice(),
1099    };
1100    for (i, &size) in sizes.iter().enumerate() {
1101        let whole_num = if i == 0 {
1102            v / size
1103        } else {
1104            (v % sizes[i - 1]) / size
1105        };
1106        if whole_num != 0 || i == 3 {
1107            if i != 3 {
1108                // days, hours, minutes
1109                s.push_str(buffer.format(whole_num));
1110                s.push_str(ISO_DURATION_PARTS[i]);
1111            } else {
1112                // (index 3 => 'seconds' part): the ISO version writes
1113                // fractional seconds, not integer nano/micro/milliseconds.
1114                // if zero, only write out if no other parts written yet.
1115                let fractional_part = v % size;
1116                if whole_num == 0 && fractional_part == 0 {
1117                    if !wrote_part {
1118                        s.push_str("0S")
1119                    }
1120                } else {
1121                    s.push_str(buffer.format(whole_num));
1122                    if fractional_part != 0 {
1123                        let secs = match unit {
1124                            TimeUnit::Nanoseconds => format!(".{fractional_part:09}"),
1125                            TimeUnit::Microseconds => format!(".{fractional_part:06}"),
1126                            TimeUnit::Milliseconds => format!(".{fractional_part:03}"),
1127                        };
1128                        s.push_str(secs.trim_end_matches('0'));
1129                    }
1130                    s.push_str(ISO_DURATION_PARTS[i]);
1131                }
1132            }
1133            // (index 0 => 'days' part): after writing days above (if non-zero)
1134            // the ISO duration string requires a `T` before the time part.
1135            if i == 0 {
1136                s.push('T');
1137            }
1138            wrote_part = true;
1139        } else if i == 0 {
1140            // always need to write the `T` separator for ISO
1141            // durations, even if there is no 'days' part.
1142            s.push('T');
1143        }
1144    }
1145    // if there was only a 'days' component, no need for time separator.
1146    if s.ends_with('T') {
1147        s.pop();
1148    }
1149}
1150
1151fn format_blob(f: &mut Formatter<'_>, bytes: &[u8]) -> fmt::Result {
1152    let ellipsis = get_ellipsis();
1153    let width = get_str_len_limit() * 2;
1154    write!(f, "b\"")?;
1155
1156    for b in bytes.iter().take(width) {
1157        if b.is_ascii_alphanumeric() || b.is_ascii_punctuation() {
1158            write!(f, "{}", *b as char)?;
1159        } else {
1160            write!(f, "\\x{b:02x}")?;
1161        }
1162    }
1163    if bytes.len() > width {
1164        write!(f, "\"{ellipsis}")?;
1165    } else {
1166        f.write_str("\"")?;
1167    }
1168    Ok(())
1169}
1170
1171impl Display for AnyValue<'_> {
1172    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1173        let width = 0;
1174        match self {
1175            AnyValue::Null => write!(f, "null"),
1176            AnyValue::UInt8(v) => fmt_integer(f, width, *v),
1177            AnyValue::UInt16(v) => fmt_integer(f, width, *v),
1178            AnyValue::UInt32(v) => fmt_integer(f, width, *v),
1179            AnyValue::UInt64(v) => fmt_integer(f, width, *v),
1180            AnyValue::UInt128(v) => feature_gated!("dtype-u128", fmt_integer(f, width, *v)),
1181            AnyValue::Int8(v) => fmt_integer(f, width, *v),
1182            AnyValue::Int16(v) => fmt_integer(f, width, *v),
1183            AnyValue::Int32(v) => fmt_integer(f, width, *v),
1184            AnyValue::Int64(v) => fmt_integer(f, width, *v),
1185            AnyValue::Int128(v) => feature_gated!("dtype-i128", fmt_integer(f, width, *v)),
1186            AnyValue::Float16(v) => feature_gated!("dtype-f16", fmt_float(f, width, *v)),
1187            AnyValue::Float32(v) => fmt_float(f, width, *v),
1188            AnyValue::Float64(v) => fmt_float(f, width, *v),
1189            AnyValue::Boolean(v) => write!(f, "{}", *v),
1190            AnyValue::String(v) => write!(f, "{}", format_args!("\"{v}\"")),
1191            AnyValue::StringOwned(v) => write!(f, "{}", format_args!("\"{v}\"")),
1192            AnyValue::Binary(d) => format_blob(f, d),
1193            AnyValue::BinaryOwned(d) => format_blob(f, d),
1194            #[cfg(feature = "dtype-date")]
1195            AnyValue::Date(v) => write!(f, "{}", date32_to_date(*v)),
1196            #[cfg(feature = "dtype-datetime")]
1197            AnyValue::Datetime(v, tu, tz) => fmt_datetime(f, *v, *tu, *tz),
1198            #[cfg(feature = "dtype-datetime")]
1199            AnyValue::DatetimeOwned(v, tu, tz) => {
1200                fmt_datetime(f, *v, *tu, tz.as_ref().map(|v| v.as_ref()))
1201            },
1202            #[cfg(feature = "dtype-duration")]
1203            AnyValue::Duration(v, tu) => fmt_duration_string(f, *v, *tu),
1204            #[cfg(feature = "dtype-time")]
1205            AnyValue::Time(_) => {
1206                let nt: chrono::NaiveTime = self.into();
1207                write!(f, "{nt}")
1208            },
1209            #[cfg(feature = "dtype-categorical")]
1210            AnyValue::Categorical(_, _)
1211            | AnyValue::CategoricalOwned(_, _)
1212            | AnyValue::Enum(_, _)
1213            | AnyValue::EnumOwned(_, _) => {
1214                let s = self.get_str().unwrap();
1215                write!(f, "\"{s}\"")
1216            },
1217            #[cfg(feature = "dtype-array")]
1218            AnyValue::Array(s, _size) => write!(f, "{}", s.fmt_list()),
1219            AnyValue::List(s) => write!(f, "{}", s.fmt_list()),
1220            #[cfg(feature = "dtype-map")]
1221            AnyValue::Map(s) => fmt_map(f, s),
1222            #[cfg(feature = "object")]
1223            AnyValue::Object(v) => write!(f, "{v}"),
1224            #[cfg(feature = "object")]
1225            AnyValue::ObjectOwned(v) => write!(f, "{}", v.0.as_ref()),
1226            #[cfg(feature = "dtype-struct")]
1227            av @ AnyValue::Struct(_, _, _) => {
1228                let mut avs = vec![];
1229                av._materialize_struct_av(&mut avs);
1230                fmt_struct(f, &avs)
1231            },
1232            #[cfg(feature = "dtype-struct")]
1233            AnyValue::StructOwned(payload) => fmt_struct(f, &payload.0),
1234            #[cfg(feature = "dtype-decimal")]
1235            AnyValue::Decimal(v, _prec, scale) => fmt_decimal(f, *v, *scale),
1236        }
1237    }
1238}
1239
1240/// Utility struct to format a timezone aware datetime.
1241#[allow(dead_code)]
1242#[cfg(feature = "dtype-datetime")]
1243pub struct PlTzAware<'a> {
1244    ndt: NaiveDateTime,
1245    tz: &'a str,
1246}
1247#[cfg(feature = "dtype-datetime")]
1248impl<'a> PlTzAware<'a> {
1249    pub fn new(ndt: NaiveDateTime, tz: &'a str) -> Self {
1250        Self { ndt, tz }
1251    }
1252}
1253
1254#[cfg(feature = "dtype-datetime")]
1255impl Display for PlTzAware<'_> {
1256    #[allow(unused_variables)]
1257    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1258        #[cfg(feature = "timezones")]
1259        match self.tz.parse::<chrono_tz::Tz>() {
1260            Ok(tz) => {
1261                let dt_utc = chrono::Utc.from_local_datetime(&self.ndt).unwrap();
1262                let dt_tz_aware = dt_utc.with_timezone(&tz);
1263                write!(f, "{dt_tz_aware}")
1264            },
1265            Err(_) => write!(f, "invalid timezone"),
1266        }
1267        #[cfg(not(feature = "timezones"))]
1268        {
1269            panic!("activate 'timezones' feature")
1270        }
1271    }
1272}
1273
1274/// Renders a map row as `{"a": 1, "b": 2}`
1275#[cfg(feature = "dtype-map")]
1276fn fmt_map(f: &mut Formatter<'_>, entries: &Series) -> fmt::Result {
1277    let (keys, values) =
1278        try_unpack_map_entries(entries).expect("Map entries have canonical key and value fields");
1279
1280    // Same truncation as `Series::fmt_list`.
1281    let max_items = get_list_len_limit();
1282    let ellipsis = get_ellipsis();
1283    let shown = max_items.min(entries.len());
1284
1285    write!(f, "{{")?;
1286    for i in 0..shown {
1287        if i > 0 {
1288            write!(f, ", ")?;
1289        }
1290        write!(f, "{}: {}", keys.get(i).unwrap(), values.get(i).unwrap())?;
1291    }
1292    if shown < entries.len() {
1293        if shown > 0 {
1294            write!(f, ", ")?;
1295        }
1296        write!(f, "{ellipsis}")?;
1297    }
1298    write!(f, "}}")
1299}
1300
1301#[cfg(feature = "dtype-struct")]
1302fn fmt_struct(f: &mut Formatter<'_>, vals: &[AnyValue]) -> fmt::Result {
1303    write!(f, "{{")?;
1304    if !vals.is_empty() {
1305        for v in &vals[..vals.len() - 1] {
1306            write!(f, "{v},")?;
1307        }
1308        // last value has no trailing comma
1309        write!(f, "{}", vals[vals.len() - 1])?;
1310    }
1311    write!(f, "}}")
1312}
1313
1314impl Series {
1315    pub fn fmt_list(&self) -> String {
1316        assert!(
1317            !self.dtype().is_object(),
1318            "nested Objects are not allowed\n\nYou probably got here by not setting a `return_dtype` on a UDF on Objects."
1319        );
1320        if self.is_empty() {
1321            return "[]".to_owned();
1322        }
1323        let mut result = "[".to_owned();
1324        let max_items = get_list_len_limit();
1325        let ellipsis = get_ellipsis();
1326
1327        match max_items {
1328            0 => write!(result, "{ellipsis}]").unwrap(),
1329            _ if max_items >= self.len() => {
1330                // this will always leave a trailing ", " after the last item
1331                // but for long lists, this is faster than checking against the length each time
1332                for item in self.rechunk().iter() {
1333                    write!(result, "{item}, ").unwrap();
1334                }
1335                // remove trailing ", " and replace with closing brace
1336                result.truncate(result.len() - 2);
1337                result.push(']');
1338            },
1339            _ => {
1340                let s = self.slice(0, max_items);
1341                for (i, item) in s.iter().enumerate() {
1342                    if i == max_items.saturating_sub(1) {
1343                        write!(result, "{ellipsis} {}", self.get(self.len() - 1).unwrap()).unwrap();
1344                        break;
1345                    } else {
1346                        write!(result, "{item}, ").unwrap();
1347                    }
1348                }
1349                result.push(']');
1350            },
1351        };
1352        result
1353    }
1354}
1355
1356#[inline]
1357#[cfg(feature = "dtype-decimal")]
1358fn fmt_decimal(f: &mut Formatter<'_>, v: i128, scale: usize) -> fmt::Result {
1359    let mut fmt_buf = polars_compute::decimal::DecimalFmtBuffer::new();
1360    let trim_zeros = get_trim_decimal_zeros();
1361    f.write_str(fmt_float_string(fmt_buf.format_dec128(v, scale, trim_zeros, false)).as_str())
1362}
1363
1364#[cfg(all(
1365    test,
1366    feature = "temporal",
1367    feature = "dtype-date",
1368    feature = "dtype-datetime"
1369))]
1370#[allow(unsafe_op_in_unsafe_fn)]
1371mod test {
1372    use crate::prelude::*;
1373
1374    #[test]
1375    fn test_fmt_list() {
1376        let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1377            PlSmallStr::from_static("a"),
1378            10,
1379            10,
1380            DataType::Int32,
1381        );
1382        builder.append_opt_slice(Some(&[1, 2, 3, 4, 5, 6]));
1383        builder.append_opt_slice(None);
1384        let list_long = builder.finish().into_series();
1385
1386        assert_eq!(
1387            r#"shape: (2,)
1388Series: 'a' [list[i32]]
1389[
1390	[1, 2, … 6]
1391	null
1392]"#,
1393            format!("{list_long:?}")
1394        );
1395
1396        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "10") };
1397
1398        assert_eq!(
1399            r#"shape: (2,)
1400Series: 'a' [list[i32]]
1401[
1402	[1, 2, 3, 4, 5, 6]
1403	null
1404]"#,
1405            format!("{list_long:?}")
1406        );
1407
1408        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1409
1410        assert_eq!(
1411            r#"shape: (2,)
1412Series: 'a' [list[i32]]
1413[
1414	[1, 2, 3, 4, 5, 6]
1415	null
1416]"#,
1417            format!("{list_long:?}")
1418        );
1419
1420        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1421
1422        assert_eq!(
1423            r#"shape: (2,)
1424Series: 'a' [list[i32]]
1425[
1426	[…]
1427	null
1428]"#,
1429            format!("{list_long:?}")
1430        );
1431
1432        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "1") };
1433
1434        assert_eq!(
1435            r#"shape: (2,)
1436Series: 'a' [list[i32]]
1437[
1438	[… 6]
1439	null
1440]"#,
1441            format!("{list_long:?}")
1442        );
1443
1444        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "4") };
1445
1446        assert_eq!(
1447            r#"shape: (2,)
1448Series: 'a' [list[i32]]
1449[
1450	[1, 2, 3, … 6]
1451	null
1452]"#,
1453            format!("{list_long:?}")
1454        );
1455
1456        let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1457            PlSmallStr::from_static("a"),
1458            10,
1459            10,
1460            DataType::Int32,
1461        );
1462        builder.append_opt_slice(Some(&[1]));
1463        builder.append_opt_slice(None);
1464        let list_short = builder.finish().into_series();
1465
1466        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1467
1468        assert_eq!(
1469            r#"shape: (2,)
1470Series: 'a' [list[i32]]
1471[
1472	[1]
1473	null
1474]"#,
1475            format!("{list_short:?}")
1476        );
1477
1478        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1479
1480        assert_eq!(
1481            r#"shape: (2,)
1482Series: 'a' [list[i32]]
1483[
1484	[…]
1485	null
1486]"#,
1487            format!("{list_short:?}")
1488        );
1489
1490        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1491
1492        assert_eq!(
1493            r#"shape: (2,)
1494Series: 'a' [list[i32]]
1495[
1496	[1]
1497	null
1498]"#,
1499            format!("{list_short:?}")
1500        );
1501
1502        let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1503            PlSmallStr::from_static("a"),
1504            10,
1505            10,
1506            DataType::Int32,
1507        );
1508        builder.append_opt_slice(Some(&[]));
1509        builder.append_opt_slice(None);
1510        let list_empty = builder.finish().into_series();
1511
1512        unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1513
1514        assert_eq!(
1515            r#"shape: (2,)
1516Series: 'a' [list[i32]]
1517[
1518	[]
1519	null
1520]"#,
1521            format!("{list_empty:?}")
1522        );
1523    }
1524
1525    #[test]
1526    fn test_fmt_temporal() {
1527        let s = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)])
1528            .into_date();
1529        assert_eq!(
1530            r#"shape: (3,)
1531Series: 'Date' [date]
1532[
1533	1970-01-02
1534	null
1535	1970-01-04
1536]"#,
1537            format!("{:?}", s.into_series())
1538        );
1539
1540        let s = Int64Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(1_000_000_000_000)])
1541            .into_datetime(TimeUnit::Nanoseconds, None);
1542        assert_eq!(
1543            r#"shape: (3,)
1544Series: '' [datetime[ns]]
1545[
1546	1970-01-01 00:00:00.000000001
1547	null
1548	1970-01-01 00:16:40
1549]"#,
1550            format!("{:?}", s.into_series())
1551        );
1552    }
1553
1554    #[test]
1555    fn test_fmt_chunkedarray() {
1556        let ca = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)]);
1557        assert_eq!(
1558            r#"shape: (3,)
1559ChunkedArray: 'Date' [i32]
1560[
1561	1
1562	null
1563	3
1564]"#,
1565            format!("{ca:?}")
1566        );
1567        let ca = StringChunked::new(PlSmallStr::from_static("name"), &["a", "b"]);
1568        assert_eq!(
1569            r#"shape: (2,)
1570ChunkedArray: 'name' [str]
1571[
1572	"a"
1573	"b"
1574]"#,
1575            format!("{ca:?}")
1576        );
1577    }
1578}