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::sync::atomic::{AtomicU8, Ordering};
8use std::{fmt, str};
9
10#[cfg(any(
11 feature = "dtype-date",
12 feature = "dtype-datetime",
13 feature = "dtype-time"
14))]
15use arrow::temporal_conversions::*;
16#[cfg(feature = "dtype-datetime")]
17use chrono::NaiveDateTime;
18#[cfg(feature = "timezones")]
19use chrono::TimeZone;
20#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
21use comfy_table::modifiers::*;
22#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
23use comfy_table::presets::*;
24#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
25use comfy_table::*;
26use num_traits::{Num, NumCast};
27use polars_error::feature_gated;
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: AtomicU8 = AtomicU8::new(FloatFmt::Mixed as u8);
48
49static THOUSANDS_SEPARATOR: AtomicU8 = AtomicU8::new(b'\0');
50static DECIMAL_SEPARATOR: AtomicU8 = AtomicU8::new(b'.');
51
52pub fn get_float_fmt() -> FloatFmt {
54 match FLOAT_FMT.load(Ordering::Relaxed) {
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(Ordering::Relaxed) as char
65}
66pub fn get_thousands_separator() -> String {
67 let sep = THOUSANDS_SEPARATOR.load(Ordering::Relaxed) 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, Ordering::Relaxed)
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, Ordering::Relaxed)
88}
89pub fn set_thousands_separator(sep: Option<char>) {
90 THOUSANDS_SEPARATOR.store(sep.unwrap_or('\0') as u8, Ordering::Relaxed)
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() {
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_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::Int8 => {
351 format_array!(f, self.i8().unwrap(), "i8", self.name(), "Series")
352 },
353 DataType::Int16 => {
354 format_array!(f, self.i16().unwrap(), "i16", self.name(), "Series")
355 },
356 DataType::Int32 => {
357 format_array!(f, self.i32().unwrap(), "i32", self.name(), "Series")
358 },
359 DataType::Int64 => {
360 format_array!(f, self.i64().unwrap(), "i64", self.name(), "Series")
361 },
362 DataType::Int128 => {
363 feature_gated!(
364 "dtype-i128",
365 format_array!(f, self.i128().unwrap(), "i128", self.name(), "Series")
366 )
367 },
368 DataType::Float32 => {
369 format_array!(f, self.f32().unwrap(), "f32", self.name(), "Series")
370 },
371 DataType::Float64 => {
372 format_array!(f, self.f64().unwrap(), "f64", self.name(), "Series")
373 },
374 #[cfg(feature = "dtype-date")]
375 DataType::Date => format_array!(f, self.date().unwrap(), "date", self.name(), "Series"),
376 #[cfg(feature = "dtype-datetime")]
377 DataType::Datetime(_, _) => {
378 let dt = format!("{}", self.dtype());
379 format_array!(f, self.datetime().unwrap(), &dt, self.name(), "Series")
380 },
381 #[cfg(feature = "dtype-time")]
382 DataType::Time => format_array!(f, self.time().unwrap(), "time", self.name(), "Series"),
383 #[cfg(feature = "dtype-duration")]
384 DataType::Duration(_) => {
385 let dt = format!("{}", self.dtype());
386 format_array!(f, self.duration().unwrap(), &dt, self.name(), "Series")
387 },
388 #[cfg(feature = "dtype-decimal")]
389 DataType::Decimal(_, _) => {
390 let dt = format!("{}", self.dtype());
391 format_array!(f, self.decimal().unwrap(), &dt, self.name(), "Series")
392 },
393 #[cfg(feature = "dtype-array")]
394 DataType::Array(_, _) => {
395 let dt = format!("{}", self.dtype());
396 format_array!(f, self.array().unwrap(), &dt, self.name(), "Series")
397 },
398 DataType::List(_) => {
399 let dt = format!("{}", self.dtype());
400 format_array!(f, self.list().unwrap(), &dt, self.name(), "Series")
401 },
402 #[cfg(feature = "object")]
403 DataType::Object(_) => format_object_array(f, self, self.name(), "Series"),
404 #[cfg(feature = "dtype-categorical")]
405 DataType::Categorical(_, _) => {
406 format_array!(f, self.categorical().unwrap(), "cat", self.name(), "Series")
407 },
408
409 #[cfg(feature = "dtype-categorical")]
410 DataType::Enum(_, _) => format_array!(
411 f,
412 self.categorical().unwrap(),
413 "enum",
414 self.name(),
415 "Series"
416 ),
417 #[cfg(feature = "dtype-struct")]
418 dt @ DataType::Struct(_) => format_array!(
419 f,
420 self.struct_().unwrap(),
421 format!("{dt}"),
422 self.name(),
423 "Series"
424 ),
425 DataType::Null => {
426 format_array!(f, self.null().unwrap(), "null", self.name(), "Series")
427 },
428 DataType::Binary => {
429 format_array!(f, self.binary().unwrap(), "binary", self.name(), "Series")
430 },
431 DataType::BinaryOffset => {
432 format_array!(
433 f,
434 self.binary_offset().unwrap(),
435 "binary[offset]",
436 self.name(),
437 "Series"
438 )
439 },
440 dt => panic!("{dt:?} not impl"),
441 }
442 }
443}
444
445impl Display for Series {
446 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
447 Debug::fmt(self, f)
448 }
449}
450
451impl Debug for DataFrame {
452 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
453 Display::fmt(self, f)
454 }
455}
456#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
457fn make_str_val(v: &str, truncate: usize, ellipsis: &String) -> String {
458 let v_trunc = &v[..v
459 .char_indices()
460 .take(truncate)
461 .last()
462 .map(|(i, c)| i + c.len_utf8())
463 .unwrap_or(0)];
464 if v == v_trunc {
465 v.to_string()
466 } else {
467 format!("{v_trunc}{ellipsis}")
468 }
469}
470
471#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
472fn field_to_str(
473 f: &Field,
474 str_truncate: usize,
475 ellipsis: &String,
476 padding: usize,
477) -> (String, usize) {
478 let name = make_str_val(f.name(), str_truncate, ellipsis);
479 let name_length = estimate_string_width(name.as_str());
480 let mut column_name = name;
481 if env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES) {
482 column_name = "".to_string();
483 }
484 let column_dtype = if env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES) {
485 "".to_string()
486 } else if env_is_true(FMT_TABLE_INLINE_COLUMN_DATA_TYPE)
487 | env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
488 {
489 format!("{}", f.dtype())
490 } else {
491 format!("\n{}", f.dtype())
492 };
493 let mut dtype_length = column_dtype.trim_start().len();
494 let mut separator = "\n---";
495 if env_is_true(FMT_TABLE_HIDE_COLUMN_SEPARATOR)
496 | env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
497 | env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES)
498 {
499 separator = ""
500 }
501 let s = if env_is_true(FMT_TABLE_INLINE_COLUMN_DATA_TYPE)
502 & !env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES)
503 {
504 let inline_name_dtype = format!("{column_name} ({column_dtype})");
505 dtype_length = inline_name_dtype.len();
506 inline_name_dtype
507 } else {
508 format!("{column_name}{separator}{column_dtype}")
509 };
510 let mut s_len = std::cmp::max(name_length, dtype_length);
511 let separator_length = estimate_string_width(separator.trim());
512 if s_len < separator_length {
513 s_len = separator_length;
514 }
515 (s, s_len + padding)
516}
517
518#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
519fn prepare_row(
520 row: Vec<Cow<'_, str>>,
521 n_first: usize,
522 n_last: usize,
523 str_truncate: usize,
524 max_elem_lengths: &mut [usize],
525 ellipsis: &String,
526 padding: usize,
527) -> Vec<String> {
528 let reduce_columns = n_first + n_last < row.len();
529 let n_elems = n_first + n_last + reduce_columns as usize;
530 let mut row_strings = Vec::with_capacity(n_elems);
531
532 for (idx, v) in row[0..n_first].iter().enumerate() {
533 let elem_str = make_str_val(v, str_truncate, ellipsis);
534 let elem_len = estimate_string_width(elem_str.as_str()) + padding;
535 if max_elem_lengths[idx] < elem_len {
536 max_elem_lengths[idx] = elem_len;
537 };
538 row_strings.push(elem_str);
539 }
540 if reduce_columns {
541 row_strings.push(ellipsis.to_string());
542 max_elem_lengths[n_first] = ellipsis.chars().count() + padding;
543 }
544 let elem_offset = n_first + reduce_columns as usize;
545 for (idx, v) in row[row.len() - n_last..].iter().enumerate() {
546 let elem_str = make_str_val(v, str_truncate, ellipsis);
547 let elem_len = estimate_string_width(elem_str.as_str()) + padding;
548 let elem_idx = elem_offset + idx;
549 if max_elem_lengths[elem_idx] < elem_len {
550 max_elem_lengths[elem_idx] = elem_len;
551 };
552 row_strings.push(elem_str);
553 }
554 row_strings
555}
556
557#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
558fn env_is_true(varname: &str) -> bool {
559 std::env::var(varname).as_deref().unwrap_or("0") == "1"
560}
561
562#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
563fn fmt_df_shape((shape0, shape1): &(usize, usize)) -> String {
564 format!(
566 "({}, {})",
567 fmt_int_string_custom(&shape0.to_string(), 3, "_"),
568 fmt_int_string_custom(&shape1.to_string(), 3, "_")
569 )
570}
571
572impl Display for DataFrame {
573 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
574 #[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
575 {
576 let height = self.height();
577 assert!(
578 self.columns.iter().all(|s| s.len() == height),
579 "The column lengths in the DataFrame are not equal."
580 );
581
582 let table_style = std::env::var(FMT_TABLE_FORMATTING).unwrap_or("DEFAULT".to_string());
583 let is_utf8 = !table_style.starts_with("ASCII");
584 let preset = match table_style.as_str() {
585 "ASCII_FULL" => ASCII_FULL,
586 "ASCII_FULL_CONDENSED" => ASCII_FULL_CONDENSED,
587 "ASCII_NO_BORDERS" => ASCII_NO_BORDERS,
588 "ASCII_BORDERS_ONLY" => ASCII_BORDERS_ONLY,
589 "ASCII_BORDERS_ONLY_CONDENSED" => ASCII_BORDERS_ONLY_CONDENSED,
590 "ASCII_HORIZONTAL_ONLY" => ASCII_HORIZONTAL_ONLY,
591 "ASCII_MARKDOWN" | "MARKDOWN" => ASCII_MARKDOWN,
592 "UTF8_FULL" => UTF8_FULL,
593 "UTF8_FULL_CONDENSED" => UTF8_FULL_CONDENSED,
594 "UTF8_NO_BORDERS" => UTF8_NO_BORDERS,
595 "UTF8_BORDERS_ONLY" => UTF8_BORDERS_ONLY,
596 "UTF8_HORIZONTAL_ONLY" => UTF8_HORIZONTAL_ONLY,
597 "NOTHING" => NOTHING,
598 _ => UTF8_FULL_CONDENSED,
599 };
600 let ellipsis = get_ellipsis().to_string();
601 let ellipsis_len = ellipsis.chars().count();
602 let max_n_cols = get_col_limit();
603 let max_n_rows = get_row_limit();
604 let str_truncate = get_str_len_limit();
605 let padding = 2; let (n_first, n_last) = if self.width() > max_n_cols {
608 (max_n_cols.div_ceil(2), max_n_cols / 2)
609 } else {
610 (self.width(), 0)
611 };
612 let reduce_columns = n_first + n_last < self.width();
613 let n_tbl_cols = n_first + n_last + reduce_columns as usize;
614 let mut names = Vec::with_capacity(n_tbl_cols);
615 let mut name_lengths = Vec::with_capacity(n_tbl_cols);
616
617 let fields = self.fields();
618 for field in fields[0..n_first].iter() {
619 let (s, l) = field_to_str(field, str_truncate, &ellipsis, padding);
620 names.push(s);
621 name_lengths.push(l);
622 }
623 if reduce_columns {
624 names.push(ellipsis.clone());
625 name_lengths.push(ellipsis_len);
626 }
627 for field in fields[self.width() - n_last..].iter() {
628 let (s, l) = field_to_str(field, str_truncate, &ellipsis, padding);
629 names.push(s);
630 name_lengths.push(l);
631 }
632
633 let mut table = Table::new();
634 table
635 .load_preset(preset)
636 .set_content_arrangement(ContentArrangement::Dynamic);
637
638 if is_utf8 && env_is_true(FMT_TABLE_ROUNDED_CORNERS) {
639 table.apply_modifier(UTF8_ROUND_CORNERS);
640 }
641 let mut constraints = Vec::with_capacity(n_tbl_cols);
642 let mut max_elem_lengths: Vec<usize> = vec![0; n_tbl_cols];
643
644 if max_n_rows > 0 {
645 if height > max_n_rows {
646 let mut rows = Vec::with_capacity(std::cmp::max(max_n_rows, 2));
649 let half = max_n_rows / 2;
650 let rest = max_n_rows % 2;
651
652 for i in 0..(half + rest) {
653 let row = self
654 .get_columns()
655 .iter()
656 .map(|c| c.str_value(i).unwrap())
657 .collect();
658
659 let row_strings = prepare_row(
660 row,
661 n_first,
662 n_last,
663 str_truncate,
664 &mut max_elem_lengths,
665 &ellipsis,
666 padding,
667 );
668 rows.push(row_strings);
669 }
670 let dots = vec![ellipsis.clone(); rows[0].len()];
671 rows.push(dots);
672
673 for i in (height - half)..height {
674 let row = self
675 .get_columns()
676 .iter()
677 .map(|c| c.str_value(i).unwrap())
678 .collect();
679
680 let row_strings = prepare_row(
681 row,
682 n_first,
683 n_last,
684 str_truncate,
685 &mut max_elem_lengths,
686 &ellipsis,
687 padding,
688 );
689 rows.push(row_strings);
690 }
691 table.add_rows(rows);
692 } else {
693 for i in 0..height {
694 if self.width() > 0 {
695 let row = self
696 .materialized_column_iter()
697 .map(|s| s.str_value(i).unwrap())
698 .collect();
699
700 let row_strings = prepare_row(
701 row,
702 n_first,
703 n_last,
704 str_truncate,
705 &mut max_elem_lengths,
706 &ellipsis,
707 padding,
708 );
709 table.add_row(row_strings);
710 } else {
711 break;
712 }
713 }
714 }
715 } else if height > 0 {
716 let dots: Vec<String> = vec![ellipsis.clone(); self.columns.len()];
717 table.add_row(dots);
718 }
719 let tbl_fallback_width = 100;
720 let tbl_width = std::env::var("POLARS_TABLE_WIDTH")
721 .map(|s| {
722 let n = s
723 .parse::<i64>()
724 .expect("could not parse table width argument");
725 let w = if n < 0 {
726 u16::MAX
727 } else {
728 u16::try_from(n).expect("table width argument does not fit in u16")
729 };
730 Some(w)
731 })
732 .unwrap_or(None);
733
734 let col_width_exact =
736 |w: usize| ColumnConstraint::Absolute(comfy_table::Width::Fixed(w as u16));
737 let col_width_bounds = |l: usize, u: usize| ColumnConstraint::Boundaries {
738 lower: Width::Fixed(l as u16),
739 upper: Width::Fixed(u as u16),
740 };
741 let min_col_width = std::cmp::max(5, 3 + padding);
742 for (idx, elem_len) in max_elem_lengths.iter().enumerate() {
743 let mx = std::cmp::min(
744 str_truncate + ellipsis_len + padding,
745 std::cmp::max(name_lengths[idx], *elem_len),
746 );
747 if (mx <= min_col_width) && !(max_n_rows > 0 && height > max_n_rows) {
748 constraints.push(col_width_exact(mx));
750 } else if mx <= min_col_width {
751 constraints.push(col_width_bounds(mx, min_col_width));
753 } else {
754 constraints.push(col_width_bounds(min_col_width, mx));
755 }
756 }
757
758 if !(env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
760 && env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES))
761 {
762 table.set_header(names).set_constraints(constraints);
763 }
764
765 if let Some(w) = tbl_width {
767 table.set_width(w);
768 } else {
769 #[cfg(feature = "fmt")]
772 if table.width().is_none() && !table.is_tty() {
773 table.set_width(tbl_fallback_width);
774 }
775 #[cfg(feature = "fmt_no_tty")]
776 if table.width().is_none() {
777 table.set_width(tbl_fallback_width);
778 }
779 }
780
781 if std::env::var(FMT_TABLE_CELL_ALIGNMENT).is_ok()
783 | std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT).is_ok()
784 {
785 let str_preset = std::env::var(FMT_TABLE_CELL_ALIGNMENT)
786 .unwrap_or_else(|_| "DEFAULT".to_string());
787 let num_preset = std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT)
788 .unwrap_or_else(|_| str_preset.to_string());
789 for (column_index, column) in table.column_iter_mut().enumerate() {
790 let dtype = fields[column_index].dtype();
791 let mut preset = str_preset.as_str();
792 if dtype.is_primitive_numeric() || dtype.is_decimal() {
793 preset = num_preset.as_str();
794 }
795 match preset {
796 "RIGHT" => column.set_cell_alignment(CellAlignment::Right),
797 "LEFT" => column.set_cell_alignment(CellAlignment::Left),
798 "CENTER" => column.set_cell_alignment(CellAlignment::Center),
799 _ => {},
800 }
801 }
802 }
803
804 if env_is_true(FMT_TABLE_HIDE_DATAFRAME_SHAPE_INFORMATION) {
806 write!(f, "{table}")?;
807 } else {
808 let shape_str = fmt_df_shape(&self.shape());
809 if env_is_true(FMT_TABLE_DATAFRAME_SHAPE_BELOW) {
810 write!(f, "{table}\nshape: {}", shape_str)?;
811 } else {
812 write!(f, "shape: {}\n{}", shape_str, table)?;
813 }
814 }
815 }
816 #[cfg(not(any(feature = "fmt", feature = "fmt_no_tty")))]
817 {
818 write!(
819 f,
820 "shape: {:?}\nto see more, compile with the 'fmt' or 'fmt_no_tty' feature",
821 self.shape()
822 )?;
823 }
824 Ok(())
825 }
826}
827
828fn fmt_int_string_custom(num: &str, group_size: u8, group_separator: &str) -> String {
829 if group_size == 0 || num.len() <= 1 {
830 num.to_string()
831 } else {
832 let mut out = String::new();
833 let sign_offset = if num.starts_with('-') || num.starts_with('+') {
834 out.push(num.chars().next().unwrap());
835 1
836 } else {
837 0
838 };
839 let int_body = &num.as_bytes()[sign_offset..]
840 .rchunks(group_size as usize)
841 .rev()
842 .map(str::from_utf8)
843 .collect::<Result<Vec<&str>, _>>()
844 .unwrap()
845 .join(group_separator);
846 out.push_str(int_body);
847 out
848 }
849}
850
851fn fmt_int_string(num: &str) -> String {
852 fmt_int_string_custom(num, 3, &get_thousands_separator())
853}
854
855fn fmt_float_string_custom(
856 num: &str,
857 group_size: u8,
858 group_separator: &str,
859 decimal: char,
860) -> String {
861 if num.len() <= 1 || (group_size == 0 && decimal == '.') {
863 num.to_string()
864 } else {
865 let (idx, has_fractional) = match num.find('.') {
868 Some(i) => (i, true),
869 None => (num.len(), false),
870 };
871 let mut out = String::new();
872 let integer_part = &num[..idx];
873
874 out.push_str(&fmt_int_string_custom(
875 integer_part,
876 group_size,
877 group_separator,
878 ));
879 if has_fractional {
880 out.push(decimal);
881 out.push_str(&num[idx + 1..]);
882 };
883 out
884 }
885}
886
887fn fmt_float_string(num: &str) -> String {
888 fmt_float_string_custom(num, 3, &get_thousands_separator(), get_decimal_separator())
889}
890
891fn fmt_integer<T: Num + NumCast + Display>(
892 f: &mut Formatter<'_>,
893 width: usize,
894 v: T,
895) -> fmt::Result {
896 write!(f, "{:>width$}", fmt_int_string(&v.to_string()))
897}
898
899const SCIENTIFIC_BOUND: f64 = 999999.0;
900
901fn fmt_float<T: Num + NumCast>(f: &mut Formatter<'_>, width: usize, v: T) -> fmt::Result {
902 let v: f64 = NumCast::from(v).unwrap();
903
904 let float_precision = get_float_precision();
905
906 if let Some(precision) = float_precision {
907 if format!("{v:.precision$}", precision = precision).len() > 19 {
908 return write!(f, "{v:>width$.precision$e}", precision = precision);
909 }
910 let s = format!("{v:>width$.precision$}", precision = precision);
911 return write!(f, "{}", fmt_float_string(s.as_str()));
912 }
913
914 if matches!(get_float_fmt(), FloatFmt::Full) {
915 let s = format!("{v:>width$}");
916 return write!(f, "{}", fmt_float_string(s.as_str()));
917 }
918
919 if v.fract() == 0.0 && v.abs() < SCIENTIFIC_BOUND {
921 let s = format!("{v:>width$.1}");
922 write!(f, "{}", fmt_float_string(s.as_str()))
923 } else if format!("{v}").len() > 9 {
924 if (!(0.000001..=SCIENTIFIC_BOUND).contains(&v.abs()) | (v.abs() > SCIENTIFIC_BOUND))
927 && get_thousands_separator().is_empty()
928 {
929 let s = format!("{v:>width$.4e}");
930 write!(f, "{}", fmt_float_string(s.as_str()))
931 } else {
932 let s = format!("{v:>width$.6}");
935
936 if s.ends_with('0') {
937 let mut s = s.as_str();
938 let mut len = s.len() - 1;
939
940 while s.ends_with('0') {
941 s = &s[..len];
942 len -= 1;
943 }
944 let s = if s.ends_with('.') {
945 format!("{s}0")
946 } else {
947 s.to_string()
948 };
949 write!(f, "{}", fmt_float_string(s.as_str()))
950 } else {
951 let s = format!("{v:>width$.6}");
955 write!(f, "{}", fmt_float_string(s.as_str()))
956 }
957 }
958 } else {
959 let s = if v.fract() == 0.0 {
960 format!("{v:>width$e}")
961 } else {
962 format!("{v:>width$}")
963 };
964 write!(f, "{}", fmt_float_string(s.as_str()))
965 }
966}
967
968#[cfg(feature = "dtype-datetime")]
969fn fmt_datetime(
970 f: &mut Formatter<'_>,
971 v: i64,
972 tu: TimeUnit,
973 tz: Option<&self::datatypes::TimeZone>,
974) -> fmt::Result {
975 let ndt = match tu {
976 TimeUnit::Nanoseconds => timestamp_ns_to_datetime(v),
977 TimeUnit::Microseconds => timestamp_us_to_datetime(v),
978 TimeUnit::Milliseconds => timestamp_ms_to_datetime(v),
979 };
980 match tz {
981 None => std::fmt::Display::fmt(&ndt, f),
982 Some(tz) => PlTzAware::new(ndt, tz).fmt(f),
983 }
984}
985
986#[cfg(feature = "dtype-duration")]
987const DURATION_PARTS: [&str; 4] = ["d", "h", "m", "s"];
988#[cfg(feature = "dtype-duration")]
989const ISO_DURATION_PARTS: [&str; 4] = ["D", "H", "M", "S"];
990#[cfg(feature = "dtype-duration")]
991const SIZES_NS: [i64; 4] = [
992 86_400_000_000_000, 3_600_000_000_000, 60_000_000_000, 1_000_000_000, ];
997#[cfg(feature = "dtype-duration")]
998const SIZES_US: [i64; 4] = [86_400_000_000, 3_600_000_000, 60_000_000, 1_000_000];
999#[cfg(feature = "dtype-duration")]
1000const SIZES_MS: [i64; 4] = [86_400_000, 3_600_000, 60_000, 1_000];
1001
1002#[cfg(feature = "dtype-duration")]
1003pub fn fmt_duration_string<W: Write>(f: &mut W, v: i64, unit: TimeUnit) -> fmt::Result {
1004 if v == 0 {
1007 return match unit {
1008 TimeUnit::Nanoseconds => f.write_str("0ns"),
1009 TimeUnit::Microseconds => f.write_str("0µs"),
1010 TimeUnit::Milliseconds => f.write_str("0ms"),
1011 };
1012 };
1013 let sizes = match unit {
1016 TimeUnit::Nanoseconds => SIZES_NS.as_slice(),
1017 TimeUnit::Microseconds => SIZES_US.as_slice(),
1018 TimeUnit::Milliseconds => SIZES_MS.as_slice(),
1019 };
1020 let mut buffer = itoa::Buffer::new();
1021 for (i, &size) in sizes.iter().enumerate() {
1022 let whole_num = if i == 0 {
1023 v / size
1024 } else {
1025 (v % sizes[i - 1]) / size
1026 };
1027 if whole_num != 0 {
1028 f.write_str(buffer.format(whole_num))?;
1029 f.write_str(DURATION_PARTS[i])?;
1030 if v % size != 0 {
1031 f.write_char(' ')?;
1032 }
1033 }
1034 }
1035 let (v, units) = match unit {
1037 TimeUnit::Nanoseconds => (v % 1_000_000_000, ["ns", "µs", "ms"]),
1038 TimeUnit::Microseconds => (v % 1_000_000, ["µs", "ms", ""]),
1039 TimeUnit::Milliseconds => (v % 1_000, ["ms", "", ""]),
1040 };
1041 if v != 0 {
1042 let (value, suffix) = if v % 1_000 != 0 {
1043 (v, units[0])
1044 } else if v % 1_000_000 != 0 {
1045 (v / 1_000, units[1])
1046 } else {
1047 (v / 1_000_000, units[2])
1048 };
1049 f.write_str(buffer.format(value))?;
1050 f.write_str(suffix)?;
1051 }
1052 Ok(())
1053}
1054
1055#[cfg(feature = "dtype-duration")]
1056pub fn iso_duration_string(s: &mut String, mut v: i64, unit: TimeUnit) {
1057 if v == 0 {
1058 s.push_str("PT0S");
1059 return;
1060 }
1061 let mut buffer = itoa::Buffer::new();
1062 let mut wrote_part = false;
1063 if v < 0 {
1064 s.push_str("-P");
1066 v = v.abs();
1067 } else {
1068 s.push('P');
1069 }
1070 let sizes = match unit {
1073 TimeUnit::Nanoseconds => SIZES_NS.as_slice(),
1074 TimeUnit::Microseconds => SIZES_US.as_slice(),
1075 TimeUnit::Milliseconds => SIZES_MS.as_slice(),
1076 };
1077 for (i, &size) in sizes.iter().enumerate() {
1078 let whole_num = if i == 0 {
1079 v / size
1080 } else {
1081 (v % sizes[i - 1]) / size
1082 };
1083 if whole_num != 0 || i == 3 {
1084 if i != 3 {
1085 s.push_str(buffer.format(whole_num));
1087 s.push_str(ISO_DURATION_PARTS[i]);
1088 } else {
1089 let fractional_part = v % size;
1093 if whole_num == 0 && fractional_part == 0 {
1094 if !wrote_part {
1095 s.push_str("0S")
1096 }
1097 } else {
1098 s.push_str(buffer.format(whole_num));
1099 if fractional_part != 0 {
1100 let secs = match unit {
1101 TimeUnit::Nanoseconds => format!(".{:09}", fractional_part),
1102 TimeUnit::Microseconds => format!(".{:06}", fractional_part),
1103 TimeUnit::Milliseconds => format!(".{:03}", fractional_part),
1104 };
1105 s.push_str(secs.trim_end_matches('0'));
1106 }
1107 s.push_str(ISO_DURATION_PARTS[i]);
1108 }
1109 }
1110 if i == 0 {
1113 s.push('T');
1114 }
1115 wrote_part = true;
1116 } else if i == 0 {
1117 s.push('T');
1120 }
1121 }
1122 if s.ends_with('T') {
1124 s.pop();
1125 }
1126}
1127
1128fn format_blob(f: &mut Formatter<'_>, bytes: &[u8]) -> fmt::Result {
1129 let ellipsis = get_ellipsis();
1130 let width = get_str_len_limit() * 2;
1131 write!(f, "b\"")?;
1132
1133 for b in bytes.iter().take(width) {
1134 if b.is_ascii_alphanumeric() || b.is_ascii_punctuation() {
1135 write!(f, "{}", *b as char)?;
1136 } else {
1137 write!(f, "\\x{:02x}", b)?;
1138 }
1139 }
1140 if bytes.len() > width {
1141 write!(f, "\"{ellipsis}")?;
1142 } else {
1143 f.write_str("\"")?;
1144 }
1145 Ok(())
1146}
1147
1148impl Display for AnyValue<'_> {
1149 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1150 let width = 0;
1151 match self {
1152 AnyValue::Null => write!(f, "null"),
1153 AnyValue::UInt8(v) => fmt_integer(f, width, *v),
1154 AnyValue::UInt16(v) => fmt_integer(f, width, *v),
1155 AnyValue::UInt32(v) => fmt_integer(f, width, *v),
1156 AnyValue::UInt64(v) => fmt_integer(f, width, *v),
1157 AnyValue::Int8(v) => fmt_integer(f, width, *v),
1158 AnyValue::Int16(v) => fmt_integer(f, width, *v),
1159 AnyValue::Int32(v) => fmt_integer(f, width, *v),
1160 AnyValue::Int64(v) => fmt_integer(f, width, *v),
1161 AnyValue::Int128(v) => feature_gated!("dtype-i128", fmt_integer(f, width, *v)),
1162 AnyValue::Float32(v) => fmt_float(f, width, *v),
1163 AnyValue::Float64(v) => fmt_float(f, width, *v),
1164 AnyValue::Boolean(v) => write!(f, "{}", *v),
1165 AnyValue::String(v) => write!(f, "{}", format_args!("\"{v}\"")),
1166 AnyValue::StringOwned(v) => write!(f, "{}", format_args!("\"{v}\"")),
1167 AnyValue::Binary(d) => format_blob(f, d),
1168 AnyValue::BinaryOwned(d) => format_blob(f, d),
1169 #[cfg(feature = "dtype-date")]
1170 AnyValue::Date(v) => write!(f, "{}", date32_to_date(*v)),
1171 #[cfg(feature = "dtype-datetime")]
1172 AnyValue::Datetime(v, tu, tz) => fmt_datetime(f, *v, *tu, *tz),
1173 #[cfg(feature = "dtype-datetime")]
1174 AnyValue::DatetimeOwned(v, tu, tz) => {
1175 fmt_datetime(f, *v, *tu, tz.as_ref().map(|v| v.as_ref()))
1176 },
1177 #[cfg(feature = "dtype-duration")]
1178 AnyValue::Duration(v, tu) => fmt_duration_string(f, *v, *tu),
1179 #[cfg(feature = "dtype-time")]
1180 AnyValue::Time(_) => {
1181 let nt: chrono::NaiveTime = self.into();
1182 write!(f, "{nt}")
1183 },
1184 #[cfg(feature = "dtype-categorical")]
1185 AnyValue::Categorical(_, _, _)
1186 | AnyValue::CategoricalOwned(_, _, _)
1187 | AnyValue::Enum(_, _, _)
1188 | AnyValue::EnumOwned(_, _, _) => {
1189 let s = self.get_str().unwrap();
1190 write!(f, "\"{s}\"")
1191 },
1192 #[cfg(feature = "dtype-array")]
1193 AnyValue::Array(s, _size) => write!(f, "{}", s.fmt_list()),
1194 AnyValue::List(s) => write!(f, "{}", s.fmt_list()),
1195 #[cfg(feature = "object")]
1196 AnyValue::Object(v) => write!(f, "{v}"),
1197 #[cfg(feature = "object")]
1198 AnyValue::ObjectOwned(v) => write!(f, "{}", v.0.as_ref()),
1199 #[cfg(feature = "dtype-struct")]
1200 av @ AnyValue::Struct(_, _, _) => {
1201 let mut avs = vec![];
1202 av._materialize_struct_av(&mut avs);
1203 fmt_struct(f, &avs)
1204 },
1205 #[cfg(feature = "dtype-struct")]
1206 AnyValue::StructOwned(payload) => fmt_struct(f, &payload.0),
1207 #[cfg(feature = "dtype-decimal")]
1208 AnyValue::Decimal(v, scale) => fmt_decimal(f, *v, *scale),
1209 }
1210 }
1211}
1212
1213#[allow(dead_code)]
1215#[cfg(feature = "dtype-datetime")]
1216pub struct PlTzAware<'a> {
1217 ndt: NaiveDateTime,
1218 tz: &'a str,
1219}
1220#[cfg(feature = "dtype-datetime")]
1221impl<'a> PlTzAware<'a> {
1222 pub fn new(ndt: NaiveDateTime, tz: &'a str) -> Self {
1223 Self { ndt, tz }
1224 }
1225}
1226
1227#[cfg(feature = "dtype-datetime")]
1228impl Display for PlTzAware<'_> {
1229 #[allow(unused_variables)]
1230 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1231 #[cfg(feature = "timezones")]
1232 match self.tz.parse::<chrono_tz::Tz>() {
1233 Ok(tz) => {
1234 let dt_utc = chrono::Utc.from_local_datetime(&self.ndt).unwrap();
1235 let dt_tz_aware = dt_utc.with_timezone(&tz);
1236 write!(f, "{dt_tz_aware}")
1237 },
1238 Err(_) => write!(f, "invalid timezone"),
1239 }
1240 #[cfg(not(feature = "timezones"))]
1241 {
1242 panic!("activate 'timezones' feature")
1243 }
1244 }
1245}
1246
1247#[cfg(feature = "dtype-struct")]
1248fn fmt_struct(f: &mut Formatter<'_>, vals: &[AnyValue]) -> fmt::Result {
1249 write!(f, "{{")?;
1250 if !vals.is_empty() {
1251 for v in &vals[..vals.len() - 1] {
1252 write!(f, "{v},")?;
1253 }
1254 write!(f, "{}", vals[vals.len() - 1])?;
1256 }
1257 write!(f, "}}")
1258}
1259
1260impl Series {
1261 pub fn fmt_list(&self) -> String {
1262 if self.is_empty() {
1263 return "[]".to_owned();
1264 }
1265 let mut result = "[".to_owned();
1266 let max_items = get_list_len_limit();
1267 let ellipsis = get_ellipsis();
1268
1269 match max_items {
1270 0 => write!(result, "{ellipsis}]").unwrap(),
1271 _ if max_items >= self.len() => {
1272 for item in self.rechunk().iter() {
1275 write!(result, "{item}, ").unwrap();
1276 }
1277 result.truncate(result.len() - 2);
1279 result.push(']');
1280 },
1281 _ => {
1282 let s = self.slice(0, max_items).rechunk();
1283 for (i, item) in s.iter().enumerate() {
1284 if i == max_items.saturating_sub(1) {
1285 write!(result, "{ellipsis} {}", self.get(self.len() - 1).unwrap()).unwrap();
1286 break;
1287 } else {
1288 write!(result, "{item}, ").unwrap();
1289 }
1290 }
1291 result.push(']');
1292 },
1293 };
1294 result
1295 }
1296}
1297
1298#[inline]
1299#[cfg(feature = "dtype-decimal")]
1300fn fmt_decimal(f: &mut Formatter<'_>, v: i128, scale: usize) -> fmt::Result {
1301 let mut fmt_buf = arrow::compute::decimal::DecimalFmtBuffer::new();
1302 let trim_zeros = get_trim_decimal_zeros();
1303 f.write_str(fmt_float_string(fmt_buf.format(v, scale, trim_zeros)).as_str())
1304}
1305
1306#[cfg(all(
1307 test,
1308 feature = "temporal",
1309 feature = "dtype-date",
1310 feature = "dtype-datetime"
1311))]
1312#[allow(unsafe_op_in_unsafe_fn)]
1313mod test {
1314 use crate::prelude::*;
1315
1316 #[test]
1317 fn test_fmt_list() {
1318 let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1319 PlSmallStr::from_static("a"),
1320 10,
1321 10,
1322 DataType::Int32,
1323 );
1324 builder.append_opt_slice(Some(&[1, 2, 3, 4, 5, 6]));
1325 builder.append_opt_slice(None);
1326 let list_long = builder.finish().into_series();
1327
1328 assert_eq!(
1329 r#"shape: (2,)
1330Series: 'a' [list[i32]]
1331[
1332 [1, 2, … 6]
1333 null
1334]"#,
1335 format!("{:?}", list_long)
1336 );
1337
1338 unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "10") };
1339
1340 assert_eq!(
1341 r#"shape: (2,)
1342Series: 'a' [list[i32]]
1343[
1344 [1, 2, 3, 4, 5, 6]
1345 null
1346]"#,
1347 format!("{:?}", list_long)
1348 );
1349
1350 unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1351
1352 assert_eq!(
1353 r#"shape: (2,)
1354Series: 'a' [list[i32]]
1355[
1356 [1, 2, 3, 4, 5, 6]
1357 null
1358]"#,
1359 format!("{:?}", list_long)
1360 );
1361
1362 unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1363
1364 assert_eq!(
1365 r#"shape: (2,)
1366Series: 'a' [list[i32]]
1367[
1368 […]
1369 null
1370]"#,
1371 format!("{:?}", list_long)
1372 );
1373
1374 unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "1") };
1375
1376 assert_eq!(
1377 r#"shape: (2,)
1378Series: 'a' [list[i32]]
1379[
1380 [… 6]
1381 null
1382]"#,
1383 format!("{:?}", list_long)
1384 );
1385
1386 unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "4") };
1387
1388 assert_eq!(
1389 r#"shape: (2,)
1390Series: 'a' [list[i32]]
1391[
1392 [1, 2, 3, … 6]
1393 null
1394]"#,
1395 format!("{:?}", list_long)
1396 );
1397
1398 let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1399 PlSmallStr::from_static("a"),
1400 10,
1401 10,
1402 DataType::Int32,
1403 );
1404 builder.append_opt_slice(Some(&[1]));
1405 builder.append_opt_slice(None);
1406 let list_short = builder.finish().into_series();
1407
1408 unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1409
1410 assert_eq!(
1411 r#"shape: (2,)
1412Series: 'a' [list[i32]]
1413[
1414 [1]
1415 null
1416]"#,
1417 format!("{:?}", list_short)
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_short)
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 [1]
1439 null
1440]"#,
1441 format!("{:?}", list_short)
1442 );
1443
1444 let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1445 PlSmallStr::from_static("a"),
1446 10,
1447 10,
1448 DataType::Int32,
1449 );
1450 builder.append_opt_slice(Some(&[]));
1451 builder.append_opt_slice(None);
1452 let list_empty = builder.finish().into_series();
1453
1454 unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1455
1456 assert_eq!(
1457 r#"shape: (2,)
1458Series: 'a' [list[i32]]
1459[
1460 []
1461 null
1462]"#,
1463 format!("{:?}", list_empty)
1464 );
1465 }
1466
1467 #[test]
1468 fn test_fmt_temporal() {
1469 let s = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)])
1470 .into_date();
1471 assert_eq!(
1472 r#"shape: (3,)
1473Series: 'Date' [date]
1474[
1475 1970-01-02
1476 null
1477 1970-01-04
1478]"#,
1479 format!("{:?}", s.into_series())
1480 );
1481
1482 let s = Int64Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(1_000_000_000_000)])
1483 .into_datetime(TimeUnit::Nanoseconds, None);
1484 assert_eq!(
1485 r#"shape: (3,)
1486Series: '' [datetime[ns]]
1487[
1488 1970-01-01 00:00:00.000000001
1489 null
1490 1970-01-01 00:16:40
1491]"#,
1492 format!("{:?}", s.into_series())
1493 );
1494 }
1495
1496 #[test]
1497 fn test_fmt_chunkedarray() {
1498 let ca = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)]);
1499 assert_eq!(
1500 r#"shape: (3,)
1501ChunkedArray: 'Date' [i32]
1502[
1503 1
1504 null
1505 3
1506]"#,
1507 format!("{:?}", ca)
1508 );
1509 let ca = StringChunked::new(PlSmallStr::from_static("name"), &["a", "b"]);
1510 assert_eq!(
1511 r#"shape: (2,)
1512ChunkedArray: 'name' [str]
1513[
1514 "a"
1515 "b"
1516]"#,
1517 format!("{:?}", ca)
1518 );
1519 }
1520}