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