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