Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
pola-rs
GitHub Repository: pola-rs/polars
Path: blob/main/crates/polars-core/src/fmt.rs
6939 views
1
#![allow(unsafe_op_in_unsafe_fn)]
2
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
3
use std::borrow::Cow;
4
use std::fmt::{Debug, Display, Formatter, Write};
5
use std::str::FromStr;
6
use std::sync::RwLock;
7
use std::{fmt, str};
8
9
#[cfg(any(
10
feature = "dtype-date",
11
feature = "dtype-datetime",
12
feature = "dtype-time"
13
))]
14
use arrow::temporal_conversions::*;
15
#[cfg(feature = "dtype-datetime")]
16
use chrono::NaiveDateTime;
17
#[cfg(feature = "timezones")]
18
use chrono::TimeZone;
19
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
20
use comfy_table::modifiers::*;
21
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
22
use comfy_table::presets::*;
23
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
24
use comfy_table::*;
25
use num_traits::{Num, NumCast};
26
use polars_error::feature_gated;
27
use polars_utils::relaxed_cell::RelaxedCell;
28
29
use crate::config::*;
30
use crate::prelude::*;
31
32
// Note: see https://github.com/pola-rs/polars/pull/13699 for the rationale
33
// behind choosing 10 as the default value for default number of rows displayed
34
const DEFAULT_ROW_LIMIT: usize = 10;
35
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
36
const DEFAULT_COL_LIMIT: usize = 8;
37
const DEFAULT_STR_LEN_LIMIT: usize = 30;
38
const DEFAULT_LIST_LEN_LIMIT: usize = 3;
39
40
#[derive(Copy, Clone)]
41
#[repr(u8)]
42
pub enum FloatFmt {
43
Mixed,
44
Full,
45
}
46
static FLOAT_PRECISION: RwLock<Option<usize>> = RwLock::new(None);
47
static FLOAT_FMT: RelaxedCell<u8> = RelaxedCell::new_u8(FloatFmt::Mixed as u8);
48
49
static THOUSANDS_SEPARATOR: RelaxedCell<u8> = RelaxedCell::new_u8(b'\0');
50
static DECIMAL_SEPARATOR: RelaxedCell<u8> = RelaxedCell::new_u8(b'.');
51
52
// Numeric formatting getters
53
pub fn get_float_fmt() -> FloatFmt {
54
match FLOAT_FMT.load() {
55
0 => FloatFmt::Mixed,
56
1 => FloatFmt::Full,
57
_ => panic!(),
58
}
59
}
60
pub fn get_float_precision() -> Option<usize> {
61
*FLOAT_PRECISION.read().unwrap()
62
}
63
pub fn get_decimal_separator() -> char {
64
DECIMAL_SEPARATOR.load() as char
65
}
66
pub 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")]
75
pub fn get_trim_decimal_zeros() -> bool {
76
arrow::compute::decimal::get_trim_decimal_zeros()
77
}
78
79
// Numeric formatting setters
80
pub fn set_float_fmt(fmt: FloatFmt) {
81
FLOAT_FMT.store(fmt as u8)
82
}
83
pub fn set_float_precision(precision: Option<usize>) {
84
*FLOAT_PRECISION.write().unwrap() = precision;
85
}
86
pub fn set_decimal_separator(dec: Option<char>) {
87
DECIMAL_SEPARATOR.store(dec.unwrap_or('.') as u8)
88
}
89
pub fn set_thousands_separator(sep: Option<char>) {
90
THOUSANDS_SEPARATOR.store(sep.unwrap_or('\0') as u8)
91
}
92
#[cfg(feature = "dtype-decimal")]
93
pub fn set_trim_decimal_zeros(trim: Option<bool>) {
94
arrow::compute::decimal::set_trim_decimal_zeros(trim)
95
}
96
97
/// Parses an environment variable value.
98
fn parse_env_var<T: FromStr>(name: &str) -> Option<T> {
99
std::env::var(name).ok().and_then(|v| v.parse().ok())
100
}
101
/// Parses an environment variable value as a limit or set a default.
102
///
103
/// Negative values (e.g. -1) are parsed as 'no limit' or [`usize::MAX`].
104
fn 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
113
fn 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"))]
117
fn get_col_limit() -> usize {
118
parse_env_var_limit(FMT_MAX_COLS, DEFAULT_COL_LIMIT)
119
}
120
fn get_str_len_limit() -> usize {
121
parse_env_var_limit(FMT_STR_LEN, DEFAULT_STR_LEN_LIMIT)
122
}
123
fn 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"))]
127
fn 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")))]
134
fn get_ellipsis() -> &'static str {
135
""
136
}
137
138
fn estimate_string_width(s: &str) -> usize {
139
// get a slightly more accurate estimate of a string's screen
140
// width, accounting (very roughly) for multibyte characters
141
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
151
macro_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")]
219
fn 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
246
impl<T> Debug for ChunkedArray<T>
247
where
248
T: PolarsNumericType,
249
{
250
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
251
let dt = format!("{}", T::get_static_dtype());
252
format_array!(f, self, dt, self.name(), "ChunkedArray")
253
}
254
}
255
256
impl 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
262
impl Debug for StringChunked {
263
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
264
format_array!(f, self, "str", self.name(), "ChunkedArray")
265
}
266
}
267
268
impl Debug for BinaryChunked {
269
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
270
format_array!(f, self, "binary", self.name(), "ChunkedArray")
271
}
272
}
273
274
impl 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")]
281
impl 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")]
288
impl<T> Debug for ObjectChunked<T>
289
where
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
329
impl 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(cats, _) => {
406
with_match_categorical_physical_type!(cats.physical(), |$C| {
407
format_array!(f, self.cat::<$C>().unwrap(), "cat", self.name(), "Series")
408
})
409
},
410
411
#[cfg(feature = "dtype-categorical")]
412
DataType::Enum(fcats, _) => {
413
with_match_categorical_physical_type!(fcats.physical(), |$C| {
414
format_array!(f, self.cat::<$C>().unwrap(), "enum", self.name(), "Series")
415
})
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
445
impl Display for Series {
446
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
447
Debug::fmt(self, f)
448
}
449
}
450
451
impl 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"))]
457
fn 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"))]
472
fn 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"))]
519
fn 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"))]
558
fn 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"))]
563
fn fmt_df_shape((shape0, shape1): &(usize, usize)) -> String {
564
// e.g. (1_000_000, 4_000)
565
format!(
566
"({}, {})",
567
fmt_int_string_custom(&shape0.to_string(), 3, "_"),
568
fmt_int_string_custom(&shape1.to_string(), 3, "_")
569
)
570
}
571
572
impl 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; // eg: one char either side of the value
606
607
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
// Truncate the table if we have more rows than the
647
// configured maximum number of rows
648
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; 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
// column width constraints
735
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
// col width is less than min width + table is not truncated
749
constraints.push(col_width_exact(mx));
750
} else if mx <= min_col_width {
751
// col width is less than min width + table is truncated (w/ ellipsis)
752
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
// insert a header row, unless both column names and dtypes are hidden
759
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 tbl_width is explicitly set, use it
766
if let Some(w) = tbl_width {
767
table.set_width(w);
768
} else {
769
// if no tbl_width (it's not tty && width not explicitly set), apply
770
// a default value; this is needed to support non-tty applications
771
#[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
// set alignment of cells, if defined
782
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
// establish 'shape' information (above/below/hidden)
805
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: {shape_str}\n{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
828
fn 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
851
fn fmt_int_string(num: &str) -> String {
852
fmt_int_string_custom(num, 3, &get_thousands_separator())
853
}
854
855
fn fmt_float_string_custom(
856
num: &str,
857
group_size: u8,
858
group_separator: &str,
859
decimal: char,
860
) -> String {
861
// Quick exit if no formatting would be applied
862
if num.len() <= 1 || (group_size == 0 && decimal == '.') {
863
num.to_string()
864
} else {
865
// Take existing numeric string and apply digit grouping & separator/decimal chars
866
// e.g. "1000000" → "1_000_000", "-123456.798" → "-123,456.789", etc
867
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
887
fn fmt_float_string(num: &str) -> String {
888
fmt_float_string_custom(num, 3, &get_thousands_separator(), get_decimal_separator())
889
}
890
891
fn 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
899
const SCIENTIFIC_BOUND: f64 = 999999.0;
900
901
fn 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$}").len() > 19 {
908
return write!(f, "{v:>width$.precision$e}");
909
}
910
let s = format!("{v:>width$.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
// show integers as 0.0, 1.0 ... 101.0
920
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
// large and small floats in scientific notation.
925
// (note: scientific notation does not play well with digit grouping)
926
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
// this makes sure we don't write 12.00000 in case of a long flt that is 12.0000000001
933
// instead we write 12.0
934
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
// 12.0934509341243124
952
// written as
953
// 12.09345
954
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")]
969
fn 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")]
987
const DURATION_PARTS: [&str; 4] = ["d", "h", "m", "s"];
988
#[cfg(feature = "dtype-duration")]
989
const ISO_DURATION_PARTS: [&str; 4] = ["D", "H", "M", "S"];
990
#[cfg(feature = "dtype-duration")]
991
const SIZES_NS: [i64; 4] = [
992
86_400_000_000_000, // per day
993
3_600_000_000_000, // per hour
994
60_000_000_000, // per minute
995
1_000_000_000, // per second
996
];
997
#[cfg(feature = "dtype-duration")]
998
const SIZES_US: [i64; 4] = [86_400_000_000, 3_600_000_000, 60_000_000, 1_000_000];
999
#[cfg(feature = "dtype-duration")]
1000
const SIZES_MS: [i64; 4] = [86_400_000, 3_600_000, 60_000, 1_000];
1001
1002
#[cfg(feature = "dtype-duration")]
1003
pub fn fmt_duration_string<W: Write>(f: &mut W, v: i64, unit: TimeUnit) -> fmt::Result {
1004
// take the physical/integer duration value and return a
1005
// friendly/readable duration string, eg: "3d 22m 55s 1ms"
1006
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
// iterate over dtype-specific sizes to appropriately scale
1014
// and extract 'days', 'hours', 'minutes', and 'seconds' parts.
1015
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
// write fractional seconds as integer nano/micro/milliseconds.
1036
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")]
1056
pub 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
// negative sign before "P" indicates entire ISO duration is negative.
1065
s.push_str("-P");
1066
v = v.abs();
1067
} else {
1068
s.push('P');
1069
}
1070
// iterate over dtype-specific sizes to appropriately scale
1071
// and extract 'days', 'hours', 'minutes', and 'seconds' parts.
1072
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
// days, hours, minutes
1086
s.push_str(buffer.format(whole_num));
1087
s.push_str(ISO_DURATION_PARTS[i]);
1088
} else {
1089
// (index 3 => 'seconds' part): the ISO version writes
1090
// fractional seconds, not integer nano/micro/milliseconds.
1091
// if zero, only write out if no other parts written yet.
1092
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!(".{fractional_part:09}"),
1102
TimeUnit::Microseconds => format!(".{fractional_part:06}"),
1103
TimeUnit::Milliseconds => format!(".{fractional_part:03}"),
1104
};
1105
s.push_str(secs.trim_end_matches('0'));
1106
}
1107
s.push_str(ISO_DURATION_PARTS[i]);
1108
}
1109
}
1110
// (index 0 => 'days' part): after writing days above (if non-zero)
1111
// the ISO duration string requires a `T` before the time part.
1112
if i == 0 {
1113
s.push('T');
1114
}
1115
wrote_part = true;
1116
} else if i == 0 {
1117
// always need to write the `T` separator for ISO
1118
// durations, even if there is no 'days' part.
1119
s.push('T');
1120
}
1121
}
1122
// if there was only a 'days' component, no need for time separator.
1123
if s.ends_with('T') {
1124
s.pop();
1125
}
1126
}
1127
1128
fn 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{b:02x}")?;
1138
}
1139
}
1140
if bytes.len() > width {
1141
write!(f, "\"{ellipsis}")?;
1142
} else {
1143
f.write_str("\"")?;
1144
}
1145
Ok(())
1146
}
1147
1148
impl 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
/// Utility struct to format a timezone aware datetime.
1214
#[allow(dead_code)]
1215
#[cfg(feature = "dtype-datetime")]
1216
pub struct PlTzAware<'a> {
1217
ndt: NaiveDateTime,
1218
tz: &'a str,
1219
}
1220
#[cfg(feature = "dtype-datetime")]
1221
impl<'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")]
1228
impl 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")]
1248
fn 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
// last value has no trailing comma
1255
write!(f, "{}", vals[vals.len() - 1])?;
1256
}
1257
write!(f, "}}")
1258
}
1259
1260
impl Series {
1261
pub fn fmt_list(&self) -> String {
1262
assert!(
1263
!self.dtype().is_object(),
1264
"nested Objects are not allowed\n\nYou probably got here by not setting a `return_dtype` on a UDF on Objects."
1265
);
1266
if self.is_empty() {
1267
return "[]".to_owned();
1268
}
1269
let mut result = "[".to_owned();
1270
let max_items = get_list_len_limit();
1271
let ellipsis = get_ellipsis();
1272
1273
match max_items {
1274
0 => write!(result, "{ellipsis}]").unwrap(),
1275
_ if max_items >= self.len() => {
1276
// this will always leave a trailing ", " after the last item
1277
// but for long lists, this is faster than checking against the length each time
1278
for item in self.rechunk().iter() {
1279
write!(result, "{item}, ").unwrap();
1280
}
1281
// remove trailing ", " and replace with closing brace
1282
result.truncate(result.len() - 2);
1283
result.push(']');
1284
},
1285
_ => {
1286
let s = self.slice(0, max_items).rechunk();
1287
for (i, item) in s.iter().enumerate() {
1288
if i == max_items.saturating_sub(1) {
1289
write!(result, "{ellipsis} {}", self.get(self.len() - 1).unwrap()).unwrap();
1290
break;
1291
} else {
1292
write!(result, "{item}, ").unwrap();
1293
}
1294
}
1295
result.push(']');
1296
},
1297
};
1298
result
1299
}
1300
}
1301
1302
#[inline]
1303
#[cfg(feature = "dtype-decimal")]
1304
fn fmt_decimal(f: &mut Formatter<'_>, v: i128, scale: usize) -> fmt::Result {
1305
let mut fmt_buf = arrow::compute::decimal::DecimalFmtBuffer::new();
1306
let trim_zeros = get_trim_decimal_zeros();
1307
f.write_str(fmt_float_string(fmt_buf.format(v, scale, trim_zeros)).as_str())
1308
}
1309
1310
#[cfg(all(
1311
test,
1312
feature = "temporal",
1313
feature = "dtype-date",
1314
feature = "dtype-datetime"
1315
))]
1316
#[allow(unsafe_op_in_unsafe_fn)]
1317
mod test {
1318
use crate::prelude::*;
1319
1320
#[test]
1321
fn test_fmt_list() {
1322
let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1323
PlSmallStr::from_static("a"),
1324
10,
1325
10,
1326
DataType::Int32,
1327
);
1328
builder.append_opt_slice(Some(&[1, 2, 3, 4, 5, 6]));
1329
builder.append_opt_slice(None);
1330
let list_long = builder.finish().into_series();
1331
1332
assert_eq!(
1333
r#"shape: (2,)
1334
Series: 'a' [list[i32]]
1335
[
1336
[1, 2, … 6]
1337
null
1338
]"#,
1339
format!("{list_long:?}")
1340
);
1341
1342
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "10") };
1343
1344
assert_eq!(
1345
r#"shape: (2,)
1346
Series: 'a' [list[i32]]
1347
[
1348
[1, 2, 3, 4, 5, 6]
1349
null
1350
]"#,
1351
format!("{list_long:?}")
1352
);
1353
1354
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1355
1356
assert_eq!(
1357
r#"shape: (2,)
1358
Series: 'a' [list[i32]]
1359
[
1360
[1, 2, 3, 4, 5, 6]
1361
null
1362
]"#,
1363
format!("{list_long:?}")
1364
);
1365
1366
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1367
1368
assert_eq!(
1369
r#"shape: (2,)
1370
Series: 'a' [list[i32]]
1371
[
1372
[…]
1373
null
1374
]"#,
1375
format!("{list_long:?}")
1376
);
1377
1378
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "1") };
1379
1380
assert_eq!(
1381
r#"shape: (2,)
1382
Series: 'a' [list[i32]]
1383
[
1384
[… 6]
1385
null
1386
]"#,
1387
format!("{list_long:?}")
1388
);
1389
1390
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "4") };
1391
1392
assert_eq!(
1393
r#"shape: (2,)
1394
Series: 'a' [list[i32]]
1395
[
1396
[1, 2, 3, … 6]
1397
null
1398
]"#,
1399
format!("{list_long:?}")
1400
);
1401
1402
let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1403
PlSmallStr::from_static("a"),
1404
10,
1405
10,
1406
DataType::Int32,
1407
);
1408
builder.append_opt_slice(Some(&[1]));
1409
builder.append_opt_slice(None);
1410
let list_short = builder.finish().into_series();
1411
1412
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1413
1414
assert_eq!(
1415
r#"shape: (2,)
1416
Series: 'a' [list[i32]]
1417
[
1418
[1]
1419
null
1420
]"#,
1421
format!("{list_short:?}")
1422
);
1423
1424
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1425
1426
assert_eq!(
1427
r#"shape: (2,)
1428
Series: 'a' [list[i32]]
1429
[
1430
[…]
1431
null
1432
]"#,
1433
format!("{list_short:?}")
1434
);
1435
1436
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1437
1438
assert_eq!(
1439
r#"shape: (2,)
1440
Series: 'a' [list[i32]]
1441
[
1442
[1]
1443
null
1444
]"#,
1445
format!("{list_short:?}")
1446
);
1447
1448
let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1449
PlSmallStr::from_static("a"),
1450
10,
1451
10,
1452
DataType::Int32,
1453
);
1454
builder.append_opt_slice(Some(&[]));
1455
builder.append_opt_slice(None);
1456
let list_empty = builder.finish().into_series();
1457
1458
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1459
1460
assert_eq!(
1461
r#"shape: (2,)
1462
Series: 'a' [list[i32]]
1463
[
1464
[]
1465
null
1466
]"#,
1467
format!("{list_empty:?}")
1468
);
1469
}
1470
1471
#[test]
1472
fn test_fmt_temporal() {
1473
let s = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)])
1474
.into_date();
1475
assert_eq!(
1476
r#"shape: (3,)
1477
Series: 'Date' [date]
1478
[
1479
1970-01-02
1480
null
1481
1970-01-04
1482
]"#,
1483
format!("{:?}", s.into_series())
1484
);
1485
1486
let s = Int64Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(1_000_000_000_000)])
1487
.into_datetime(TimeUnit::Nanoseconds, None);
1488
assert_eq!(
1489
r#"shape: (3,)
1490
Series: '' [datetime[ns]]
1491
[
1492
1970-01-01 00:00:00.000000001
1493
null
1494
1970-01-01 00:16:40
1495
]"#,
1496
format!("{:?}", s.into_series())
1497
);
1498
}
1499
1500
#[test]
1501
fn test_fmt_chunkedarray() {
1502
let ca = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)]);
1503
assert_eq!(
1504
r#"shape: (3,)
1505
ChunkedArray: 'Date' [i32]
1506
[
1507
1
1508
null
1509
3
1510
]"#,
1511
format!("{ca:?}")
1512
);
1513
let ca = StringChunked::new(PlSmallStr::from_static("name"), &["a", "b"]);
1514
assert_eq!(
1515
r#"shape: (2,)
1516
ChunkedArray: 'name' [str]
1517
[
1518
"a"
1519
"b"
1520
]"#,
1521
format!("{ca:?}")
1522
);
1523
}
1524
}
1525
1526