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