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
8422 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::num::IntErrorKind;
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 as a limit or set a default.
98
///
99
/// Negative values (e.g. -1) are parsed as 'no limit' or [`usize::MAX`].
100
fn 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
116
fn 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"))]
120
fn get_col_limit() -> usize {
121
parse_env_var_limit(FMT_MAX_COLS, DEFAULT_COL_LIMIT)
122
}
123
fn get_str_len_limit() -> usize {
124
parse_env_var_limit(FMT_STR_LEN, DEFAULT_STR_LEN_LIMIT)
125
}
126
fn 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"))]
130
fn 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")))]
137
fn get_ellipsis() -> &'static str {
138
""
139
}
140
141
fn estimate_string_width(s: &str) -> usize {
142
// get a slightly more accurate estimate of a string's screen
143
// width, accounting (very roughly) for multibyte characters
144
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
154
macro_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")]
222
fn 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
249
impl<T> Debug for ChunkedArray<T>
250
where
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
259
impl 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
265
impl Debug for StringChunked {
266
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
267
format_array!(f, self, "str", self.name(), "ChunkedArray")
268
}
269
}
270
271
impl Debug for BinaryChunked {
272
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
273
format_array!(f, self, "binary", self.name(), "ChunkedArray")
274
}
275
}
276
277
impl 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")]
284
impl 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")]
291
impl<T> Debug for ObjectChunked<T>
292
where
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
332
impl 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-extension")]
454
DataType::Extension(_, _) => {
455
let dt = format!("{}", self.dtype());
456
format_array!(f, self.ext().unwrap(), &dt, self.name(), "Series")
457
},
458
dt => panic!("{dt:?} not impl"),
459
}
460
}
461
}
462
463
impl Display for Series {
464
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
465
Debug::fmt(self, f)
466
}
467
}
468
469
impl Debug for DataFrame {
470
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
471
Display::fmt(self, f)
472
}
473
}
474
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
475
fn make_str_val(v: &str, truncate: usize, ellipsis: &String) -> String {
476
let v_trunc = &v[..v
477
.char_indices()
478
.take(truncate)
479
.last()
480
.map(|(i, c)| i + c.len_utf8())
481
.unwrap_or(0)];
482
if v == v_trunc {
483
v.to_string()
484
} else {
485
format!("{v_trunc}{ellipsis}")
486
}
487
}
488
489
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
490
fn field_to_str(
491
f: &Field,
492
str_truncate: usize,
493
ellipsis: &String,
494
padding: usize,
495
) -> (String, usize) {
496
let name = make_str_val(f.name(), str_truncate, ellipsis);
497
let name_length = estimate_string_width(name.as_str());
498
let mut column_name = name;
499
if env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES) {
500
column_name = "".to_string();
501
}
502
let column_dtype = if env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES) {
503
"".to_string()
504
} else if env_is_true(FMT_TABLE_INLINE_COLUMN_DATA_TYPE)
505
| env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
506
{
507
format!("{}", f.dtype())
508
} else {
509
format!("\n{}", f.dtype())
510
};
511
let mut dtype_length = column_dtype.trim_start().len();
512
let mut separator = "\n---";
513
if env_is_true(FMT_TABLE_HIDE_COLUMN_SEPARATOR)
514
| env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
515
| env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES)
516
{
517
separator = ""
518
}
519
let s = if env_is_true(FMT_TABLE_INLINE_COLUMN_DATA_TYPE)
520
& !env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES)
521
{
522
let inline_name_dtype = format!("{column_name} ({column_dtype})");
523
dtype_length = inline_name_dtype.len();
524
inline_name_dtype
525
} else {
526
format!("{column_name}{separator}{column_dtype}")
527
};
528
let mut s_len = std::cmp::max(name_length, dtype_length);
529
let separator_length = estimate_string_width(separator.trim());
530
if s_len < separator_length {
531
s_len = separator_length;
532
}
533
(s, s_len + padding)
534
}
535
536
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
537
fn prepare_row(
538
row: Vec<Cow<'_, str>>,
539
n_first: usize,
540
n_last: usize,
541
str_truncate: usize,
542
max_elem_lengths: &mut [usize],
543
ellipsis: &String,
544
padding: usize,
545
) -> Vec<String> {
546
let reduce_columns = n_first + n_last < row.len();
547
let n_elems = n_first + n_last + reduce_columns as usize;
548
let mut row_strings = Vec::with_capacity(n_elems);
549
550
for (idx, v) in row[0..n_first].iter().enumerate() {
551
let elem_str = make_str_val(v, str_truncate, ellipsis);
552
let elem_len = estimate_string_width(elem_str.as_str()) + padding;
553
if max_elem_lengths[idx] < elem_len {
554
max_elem_lengths[idx] = elem_len;
555
};
556
row_strings.push(elem_str);
557
}
558
if reduce_columns {
559
row_strings.push(ellipsis.to_string());
560
max_elem_lengths[n_first] = ellipsis.chars().count() + padding;
561
}
562
let elem_offset = n_first + reduce_columns as usize;
563
for (idx, v) in row[row.len() - n_last..].iter().enumerate() {
564
let elem_str = make_str_val(v, str_truncate, ellipsis);
565
let elem_len = estimate_string_width(elem_str.as_str()) + padding;
566
let elem_idx = elem_offset + idx;
567
if max_elem_lengths[elem_idx] < elem_len {
568
max_elem_lengths[elem_idx] = elem_len;
569
};
570
row_strings.push(elem_str);
571
}
572
row_strings
573
}
574
575
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
576
fn env_is_true(varname: &str) -> bool {
577
std::env::var(varname).as_deref().unwrap_or("0") == "1"
578
}
579
580
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
581
fn fmt_df_shape((shape0, shape1): &(usize, usize)) -> String {
582
// e.g. (1_000_000, 4_000)
583
format!(
584
"({}, {})",
585
fmt_int_string_custom(&shape0.to_string(), 3, "_"),
586
fmt_int_string_custom(&shape1.to_string(), 3, "_")
587
)
588
}
589
590
impl Display for DataFrame {
591
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
592
#[cfg(any(feature = "fmt", feature = "fmt_no_tty"))]
593
{
594
let height = self.height();
595
assert!(
596
self.columns().iter().all(|s| s.len() == height),
597
"The column lengths in the DataFrame are not equal."
598
);
599
600
let table_style = std::env::var(FMT_TABLE_FORMATTING).unwrap_or("DEFAULT".to_string());
601
let is_utf8 = !table_style.starts_with("ASCII");
602
let preset = match table_style.as_str() {
603
"ASCII_FULL" => ASCII_FULL,
604
"ASCII_FULL_CONDENSED" => ASCII_FULL_CONDENSED,
605
"ASCII_NO_BORDERS" => ASCII_NO_BORDERS,
606
"ASCII_BORDERS_ONLY" => ASCII_BORDERS_ONLY,
607
"ASCII_BORDERS_ONLY_CONDENSED" => ASCII_BORDERS_ONLY_CONDENSED,
608
"ASCII_HORIZONTAL_ONLY" => ASCII_HORIZONTAL_ONLY,
609
"ASCII_MARKDOWN" | "MARKDOWN" => ASCII_MARKDOWN,
610
"UTF8_FULL" => UTF8_FULL,
611
"UTF8_FULL_CONDENSED" => UTF8_FULL_CONDENSED,
612
"UTF8_NO_BORDERS" => UTF8_NO_BORDERS,
613
"UTF8_BORDERS_ONLY" => UTF8_BORDERS_ONLY,
614
"UTF8_HORIZONTAL_ONLY" => UTF8_HORIZONTAL_ONLY,
615
"NOTHING" => NOTHING,
616
_ => UTF8_FULL_CONDENSED,
617
};
618
let ellipsis = get_ellipsis().to_string();
619
let ellipsis_len = ellipsis.chars().count();
620
let max_n_cols = get_col_limit();
621
let max_n_rows = get_row_limit();
622
let str_truncate = get_str_len_limit();
623
let padding = 2; // eg: one char either side of the value
624
625
let (n_first, n_last) = if self.width() > max_n_cols {
626
(max_n_cols.div_ceil(2), max_n_cols / 2)
627
} else {
628
(self.width(), 0)
629
};
630
let reduce_columns = n_first + n_last < self.width();
631
let n_tbl_cols = n_first + n_last + reduce_columns as usize;
632
let mut names = Vec::with_capacity(n_tbl_cols);
633
let mut name_lengths = Vec::with_capacity(n_tbl_cols);
634
635
let fields = self.fields();
636
for field in fields[0..n_first].iter() {
637
let (s, l) = field_to_str(field, str_truncate, &ellipsis, padding);
638
names.push(s);
639
name_lengths.push(l);
640
}
641
if reduce_columns {
642
names.push(ellipsis.clone());
643
name_lengths.push(ellipsis_len);
644
}
645
for field in fields[self.width() - n_last..].iter() {
646
let (s, l) = field_to_str(field, str_truncate, &ellipsis, padding);
647
names.push(s);
648
name_lengths.push(l);
649
}
650
651
let mut table = Table::new();
652
table
653
.load_preset(preset)
654
.set_content_arrangement(ContentArrangement::Dynamic);
655
656
if is_utf8 && env_is_true(FMT_TABLE_ROUNDED_CORNERS) {
657
table.apply_modifier(UTF8_ROUND_CORNERS);
658
}
659
let mut constraints = Vec::with_capacity(n_tbl_cols);
660
let mut max_elem_lengths: Vec<usize> = vec![0; n_tbl_cols];
661
662
if max_n_rows > 0 {
663
if height > max_n_rows {
664
// Truncate the table if we have more rows than the
665
// configured maximum number of rows
666
let mut rows = Vec::with_capacity(std::cmp::max(max_n_rows, 2));
667
let half = max_n_rows / 2;
668
let rest = max_n_rows % 2;
669
670
for i in 0..(half + rest) {
671
let row = self
672
.columns()
673
.iter()
674
.map(|c| c.str_value(i).unwrap())
675
.collect();
676
677
let row_strings = prepare_row(
678
row,
679
n_first,
680
n_last,
681
str_truncate,
682
&mut max_elem_lengths,
683
&ellipsis,
684
padding,
685
);
686
rows.push(row_strings);
687
}
688
let dots = vec![ellipsis.clone(); rows[0].len()];
689
rows.push(dots);
690
691
for i in (height - half)..height {
692
let row = self
693
.columns()
694
.iter()
695
.map(|c| c.str_value(i).unwrap())
696
.collect();
697
698
let row_strings = prepare_row(
699
row,
700
n_first,
701
n_last,
702
str_truncate,
703
&mut max_elem_lengths,
704
&ellipsis,
705
padding,
706
);
707
rows.push(row_strings);
708
}
709
table.add_rows(rows);
710
} else {
711
for i in 0..height {
712
if self.width() > 0 {
713
let row = self
714
.materialized_column_iter()
715
.map(|s| s.str_value(i).unwrap())
716
.collect();
717
718
let row_strings = prepare_row(
719
row,
720
n_first,
721
n_last,
722
str_truncate,
723
&mut max_elem_lengths,
724
&ellipsis,
725
padding,
726
);
727
table.add_row(row_strings);
728
} else {
729
break;
730
}
731
}
732
}
733
} else if height > 0 {
734
let dots: Vec<String> = vec![ellipsis; self.width()];
735
table.add_row(dots);
736
}
737
let tbl_fallback_width = 100;
738
let tbl_width = std::env::var("POLARS_TABLE_WIDTH")
739
.map(|s| {
740
let n = s
741
.parse::<i64>()
742
.expect("could not parse table width argument");
743
let w = if n < 0 {
744
u16::MAX
745
} else {
746
u16::try_from(n).expect("table width argument does not fit in u16")
747
};
748
Some(w)
749
})
750
.unwrap_or(None);
751
752
// column width constraints
753
let col_width_exact =
754
|w: usize| ColumnConstraint::Absolute(comfy_table::Width::Fixed(w as u16));
755
let col_width_bounds = |l: usize, u: usize| ColumnConstraint::Boundaries {
756
lower: Width::Fixed(l as u16),
757
upper: Width::Fixed(u as u16),
758
};
759
let min_col_width = std::cmp::max(5, 3 + padding);
760
for (idx, elem_len) in max_elem_lengths.iter().enumerate() {
761
let mx = std::cmp::min(
762
str_truncate + ellipsis_len + padding,
763
std::cmp::max(name_lengths[idx], *elem_len),
764
);
765
if (mx <= min_col_width) && !(max_n_rows > 0 && height > max_n_rows) {
766
// col width is less than min width + table is not truncated
767
constraints.push(col_width_exact(mx));
768
} else if mx <= min_col_width {
769
// col width is less than min width + table is truncated (w/ ellipsis)
770
constraints.push(col_width_bounds(mx, min_col_width));
771
} else {
772
constraints.push(col_width_bounds(min_col_width, mx));
773
}
774
}
775
776
// insert a header row, unless both column names and dtypes are hidden
777
if !(env_is_true(FMT_TABLE_HIDE_COLUMN_NAMES)
778
&& env_is_true(FMT_TABLE_HIDE_COLUMN_DATA_TYPES))
779
{
780
table.set_header(names).set_constraints(constraints);
781
}
782
783
// if tbl_width is explicitly set, use it
784
if let Some(w) = tbl_width {
785
table.set_width(w);
786
} else {
787
// if no tbl_width (it's not tty && width not explicitly set), apply
788
// a default value; this is needed to support non-tty applications
789
#[cfg(feature = "fmt")]
790
if table.width().is_none() && !table.is_tty() {
791
table.set_width(tbl_fallback_width);
792
}
793
#[cfg(feature = "fmt_no_tty")]
794
if table.width().is_none() {
795
table.set_width(tbl_fallback_width);
796
}
797
}
798
799
// set alignment of cells, if defined
800
if std::env::var(FMT_TABLE_CELL_ALIGNMENT).is_ok()
801
| std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT).is_ok()
802
{
803
let str_preset = std::env::var(FMT_TABLE_CELL_ALIGNMENT)
804
.unwrap_or_else(|_| "DEFAULT".to_string());
805
let num_preset = std::env::var(FMT_TABLE_CELL_NUMERIC_ALIGNMENT)
806
.unwrap_or_else(|_| str_preset.to_string());
807
for (column_index, column) in table.column_iter_mut().enumerate() {
808
let dtype = fields[column_index].dtype();
809
let mut preset = str_preset.as_str();
810
if dtype.is_primitive_numeric() || dtype.is_decimal() {
811
preset = num_preset.as_str();
812
}
813
match preset {
814
"RIGHT" => column.set_cell_alignment(CellAlignment::Right),
815
"LEFT" => column.set_cell_alignment(CellAlignment::Left),
816
"CENTER" => column.set_cell_alignment(CellAlignment::Center),
817
_ => {},
818
}
819
}
820
}
821
822
// establish 'shape' information (above/below/hidden)
823
if env_is_true(FMT_TABLE_HIDE_DATAFRAME_SHAPE_INFORMATION) {
824
write!(f, "{table}")?;
825
} else {
826
let shape_str = fmt_df_shape(&self.shape());
827
if env_is_true(FMT_TABLE_DATAFRAME_SHAPE_BELOW) {
828
write!(f, "{table}\nshape: {shape_str}")?;
829
} else {
830
write!(f, "shape: {shape_str}\n{table}")?;
831
}
832
}
833
}
834
#[cfg(not(any(feature = "fmt", feature = "fmt_no_tty")))]
835
{
836
write!(
837
f,
838
"shape: {:?}\nto see more, compile with the 'fmt' or 'fmt_no_tty' feature",
839
self.shape()
840
)?;
841
}
842
Ok(())
843
}
844
}
845
846
fn fmt_int_string_custom(num: &str, group_size: u8, group_separator: &str) -> String {
847
if group_size == 0 || num.len() <= 1 {
848
num.to_string()
849
} else {
850
let mut out = String::new();
851
let sign_offset = if num.starts_with('-') || num.starts_with('+') {
852
out.push(num.chars().next().unwrap());
853
1
854
} else {
855
0
856
};
857
let int_body = &num.as_bytes()[sign_offset..]
858
.rchunks(group_size as usize)
859
.rev()
860
.map(str::from_utf8)
861
.collect::<Result<Vec<&str>, _>>()
862
.unwrap()
863
.join(group_separator);
864
out.push_str(int_body);
865
out
866
}
867
}
868
869
fn fmt_int_string(num: &str) -> String {
870
fmt_int_string_custom(num, 3, &get_thousands_separator())
871
}
872
873
fn fmt_float_string_custom(
874
num: &str,
875
group_size: u8,
876
group_separator: &str,
877
decimal: char,
878
) -> String {
879
// Quick exit if no formatting would be applied
880
if num.len() <= 1 || (group_size == 0 && decimal == '.') {
881
num.to_string()
882
} else {
883
// Take existing numeric string and apply digit grouping & separator/decimal chars
884
// e.g. "1000000" → "1_000_000", "-123456.798" → "-123,456.789", etc
885
let (idx, has_fractional) = match num.find('.') {
886
Some(i) => (i, true),
887
None => (num.len(), false),
888
};
889
let mut out = String::new();
890
let integer_part = &num[..idx];
891
892
out.push_str(&fmt_int_string_custom(
893
integer_part,
894
group_size,
895
group_separator,
896
));
897
if has_fractional {
898
out.push(decimal);
899
out.push_str(&num[idx + 1..]);
900
};
901
out
902
}
903
}
904
905
fn fmt_float_string(num: &str) -> String {
906
fmt_float_string_custom(num, 3, &get_thousands_separator(), get_decimal_separator())
907
}
908
909
fn fmt_integer<T: Num + NumCast + Display>(
910
f: &mut Formatter<'_>,
911
width: usize,
912
v: T,
913
) -> fmt::Result {
914
write!(f, "{:>width$}", fmt_int_string(&v.to_string()))
915
}
916
917
const SCIENTIFIC_BOUND: f64 = 999999.0;
918
919
fn fmt_float<T: Num + NumCast>(f: &mut Formatter<'_>, width: usize, v: T) -> fmt::Result {
920
let v: f64 = NumCast::from(v).unwrap();
921
922
let float_precision = get_float_precision();
923
924
if let Some(precision) = float_precision {
925
if format!("{v:.precision$}").len() > 19 {
926
return write!(f, "{v:>width$.precision$e}");
927
}
928
let s = format!("{v:>width$.precision$}");
929
return write!(f, "{}", fmt_float_string(s.as_str()));
930
}
931
932
if matches!(get_float_fmt(), FloatFmt::Full) {
933
let s = format!("{v:>width$}");
934
return write!(f, "{}", fmt_float_string(s.as_str()));
935
}
936
937
// show integers as 0.0, 1.0 ... 101.0
938
if v.fract() == 0.0 && v.abs() < SCIENTIFIC_BOUND {
939
let s = format!("{v:>width$.1}");
940
write!(f, "{}", fmt_float_string(s.as_str()))
941
} else if format!("{v}").len() > 9 {
942
// large and small floats in scientific notation.
943
// (note: scientific notation does not play well with digit grouping)
944
if (!(0.000001..=SCIENTIFIC_BOUND).contains(&v.abs()) | (v.abs() > SCIENTIFIC_BOUND))
945
&& get_thousands_separator().is_empty()
946
{
947
let s = format!("{v:>width$.4e}");
948
write!(f, "{}", fmt_float_string(s.as_str()))
949
} else {
950
// this makes sure we don't write 12.00000 in case of a long flt that is 12.0000000001
951
// instead we write 12.0
952
let s = format!("{v:>width$.6}");
953
954
if s.ends_with('0') {
955
let mut s = s.as_str();
956
let mut len = s.len() - 1;
957
958
while s.ends_with('0') {
959
s = &s[..len];
960
len -= 1;
961
}
962
let s = if s.ends_with('.') {
963
format!("{s}0")
964
} else {
965
s.to_string()
966
};
967
write!(f, "{}", fmt_float_string(s.as_str()))
968
} else {
969
// 12.0934509341243124
970
// written as
971
// 12.09345
972
let s = format!("{v:>width$.6}");
973
write!(f, "{}", fmt_float_string(s.as_str()))
974
}
975
}
976
} else {
977
let s = if v.fract() == 0.0 {
978
format!("{v:>width$e}")
979
} else {
980
format!("{v:>width$}")
981
};
982
write!(f, "{}", fmt_float_string(s.as_str()))
983
}
984
}
985
986
#[cfg(feature = "dtype-datetime")]
987
fn fmt_datetime(
988
f: &mut Formatter<'_>,
989
v: i64,
990
tu: TimeUnit,
991
tz: Option<&self::datatypes::TimeZone>,
992
) -> fmt::Result {
993
let ndt = match tu {
994
TimeUnit::Nanoseconds => timestamp_ns_to_datetime(v),
995
TimeUnit::Microseconds => timestamp_us_to_datetime(v),
996
TimeUnit::Milliseconds => timestamp_ms_to_datetime(v),
997
};
998
match tz {
999
None => std::fmt::Display::fmt(&ndt, f),
1000
Some(tz) => PlTzAware::new(ndt, tz).fmt(f),
1001
}
1002
}
1003
1004
#[cfg(feature = "dtype-duration")]
1005
const DURATION_PARTS: [&str; 4] = ["d", "h", "m", "s"];
1006
#[cfg(feature = "dtype-duration")]
1007
const ISO_DURATION_PARTS: [&str; 4] = ["D", "H", "M", "S"];
1008
#[cfg(feature = "dtype-duration")]
1009
const SIZES_NS: [i64; 4] = [
1010
86_400_000_000_000, // per day
1011
3_600_000_000_000, // per hour
1012
60_000_000_000, // per minute
1013
1_000_000_000, // per second
1014
];
1015
#[cfg(feature = "dtype-duration")]
1016
const SIZES_US: [i64; 4] = [86_400_000_000, 3_600_000_000, 60_000_000, 1_000_000];
1017
#[cfg(feature = "dtype-duration")]
1018
const SIZES_MS: [i64; 4] = [86_400_000, 3_600_000, 60_000, 1_000];
1019
1020
#[cfg(feature = "dtype-duration")]
1021
pub fn fmt_duration_string<W: Write>(f: &mut W, v: i64, unit: TimeUnit) -> fmt::Result {
1022
// take the physical/integer duration value and return a
1023
// friendly/readable duration string, eg: "3d 22m 55s 1ms"
1024
if v == 0 {
1025
return match unit {
1026
TimeUnit::Nanoseconds => f.write_str("0ns"),
1027
TimeUnit::Microseconds => f.write_str("0µs"),
1028
TimeUnit::Milliseconds => f.write_str("0ms"),
1029
};
1030
};
1031
// iterate over dtype-specific sizes to appropriately scale
1032
// and extract 'days', 'hours', 'minutes', and 'seconds' parts.
1033
let sizes = match unit {
1034
TimeUnit::Nanoseconds => SIZES_NS.as_slice(),
1035
TimeUnit::Microseconds => SIZES_US.as_slice(),
1036
TimeUnit::Milliseconds => SIZES_MS.as_slice(),
1037
};
1038
let mut buffer = itoa::Buffer::new();
1039
for (i, &size) in sizes.iter().enumerate() {
1040
let whole_num = if i == 0 {
1041
v / size
1042
} else {
1043
(v % sizes[i - 1]) / size
1044
};
1045
if whole_num != 0 {
1046
f.write_str(buffer.format(whole_num))?;
1047
f.write_str(DURATION_PARTS[i])?;
1048
if v % size != 0 {
1049
f.write_char(' ')?;
1050
}
1051
}
1052
}
1053
// write fractional seconds as integer nano/micro/milliseconds.
1054
let (v, units) = match unit {
1055
TimeUnit::Nanoseconds => (v % 1_000_000_000, ["ns", "µs", "ms"]),
1056
TimeUnit::Microseconds => (v % 1_000_000, ["µs", "ms", ""]),
1057
TimeUnit::Milliseconds => (v % 1_000, ["ms", "", ""]),
1058
};
1059
if v != 0 {
1060
let (value, suffix) = if v % 1_000 != 0 {
1061
(v, units[0])
1062
} else if v % 1_000_000 != 0 {
1063
(v / 1_000, units[1])
1064
} else {
1065
(v / 1_000_000, units[2])
1066
};
1067
f.write_str(buffer.format(value))?;
1068
f.write_str(suffix)?;
1069
}
1070
Ok(())
1071
}
1072
1073
#[cfg(feature = "dtype-duration")]
1074
pub fn iso_duration_string(s: &mut String, mut v: i64, unit: TimeUnit) {
1075
if v == 0 {
1076
s.push_str("PT0S");
1077
return;
1078
}
1079
let mut buffer = itoa::Buffer::new();
1080
let mut wrote_part = false;
1081
if v < 0 {
1082
// negative sign before "P" indicates entire ISO duration is negative.
1083
s.push_str("-P");
1084
v = v.abs();
1085
} else {
1086
s.push('P');
1087
}
1088
// iterate over dtype-specific sizes to appropriately scale
1089
// and extract 'days', 'hours', 'minutes', and 'seconds' parts.
1090
let sizes = match unit {
1091
TimeUnit::Nanoseconds => SIZES_NS.as_slice(),
1092
TimeUnit::Microseconds => SIZES_US.as_slice(),
1093
TimeUnit::Milliseconds => SIZES_MS.as_slice(),
1094
};
1095
for (i, &size) in sizes.iter().enumerate() {
1096
let whole_num = if i == 0 {
1097
v / size
1098
} else {
1099
(v % sizes[i - 1]) / size
1100
};
1101
if whole_num != 0 || i == 3 {
1102
if i != 3 {
1103
// days, hours, minutes
1104
s.push_str(buffer.format(whole_num));
1105
s.push_str(ISO_DURATION_PARTS[i]);
1106
} else {
1107
// (index 3 => 'seconds' part): the ISO version writes
1108
// fractional seconds, not integer nano/micro/milliseconds.
1109
// if zero, only write out if no other parts written yet.
1110
let fractional_part = v % size;
1111
if whole_num == 0 && fractional_part == 0 {
1112
if !wrote_part {
1113
s.push_str("0S")
1114
}
1115
} else {
1116
s.push_str(buffer.format(whole_num));
1117
if fractional_part != 0 {
1118
let secs = match unit {
1119
TimeUnit::Nanoseconds => format!(".{fractional_part:09}"),
1120
TimeUnit::Microseconds => format!(".{fractional_part:06}"),
1121
TimeUnit::Milliseconds => format!(".{fractional_part:03}"),
1122
};
1123
s.push_str(secs.trim_end_matches('0'));
1124
}
1125
s.push_str(ISO_DURATION_PARTS[i]);
1126
}
1127
}
1128
// (index 0 => 'days' part): after writing days above (if non-zero)
1129
// the ISO duration string requires a `T` before the time part.
1130
if i == 0 {
1131
s.push('T');
1132
}
1133
wrote_part = true;
1134
} else if i == 0 {
1135
// always need to write the `T` separator for ISO
1136
// durations, even if there is no 'days' part.
1137
s.push('T');
1138
}
1139
}
1140
// if there was only a 'days' component, no need for time separator.
1141
if s.ends_with('T') {
1142
s.pop();
1143
}
1144
}
1145
1146
fn format_blob(f: &mut Formatter<'_>, bytes: &[u8]) -> fmt::Result {
1147
let ellipsis = get_ellipsis();
1148
let width = get_str_len_limit() * 2;
1149
write!(f, "b\"")?;
1150
1151
for b in bytes.iter().take(width) {
1152
if b.is_ascii_alphanumeric() || b.is_ascii_punctuation() {
1153
write!(f, "{}", *b as char)?;
1154
} else {
1155
write!(f, "\\x{b:02x}")?;
1156
}
1157
}
1158
if bytes.len() > width {
1159
write!(f, "\"{ellipsis}")?;
1160
} else {
1161
f.write_str("\"")?;
1162
}
1163
Ok(())
1164
}
1165
1166
impl Display for AnyValue<'_> {
1167
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1168
let width = 0;
1169
match self {
1170
AnyValue::Null => write!(f, "null"),
1171
AnyValue::UInt8(v) => fmt_integer(f, width, *v),
1172
AnyValue::UInt16(v) => fmt_integer(f, width, *v),
1173
AnyValue::UInt32(v) => fmt_integer(f, width, *v),
1174
AnyValue::UInt64(v) => fmt_integer(f, width, *v),
1175
AnyValue::UInt128(v) => feature_gated!("dtype-u128", fmt_integer(f, width, *v)),
1176
AnyValue::Int8(v) => fmt_integer(f, width, *v),
1177
AnyValue::Int16(v) => fmt_integer(f, width, *v),
1178
AnyValue::Int32(v) => fmt_integer(f, width, *v),
1179
AnyValue::Int64(v) => fmt_integer(f, width, *v),
1180
AnyValue::Int128(v) => feature_gated!("dtype-i128", fmt_integer(f, width, *v)),
1181
AnyValue::Float16(v) => feature_gated!("dtype-f16", fmt_float(f, width, *v)),
1182
AnyValue::Float32(v) => fmt_float(f, width, *v),
1183
AnyValue::Float64(v) => fmt_float(f, width, *v),
1184
AnyValue::Boolean(v) => write!(f, "{}", *v),
1185
AnyValue::String(v) => write!(f, "{}", format_args!("\"{v}\"")),
1186
AnyValue::StringOwned(v) => write!(f, "{}", format_args!("\"{v}\"")),
1187
AnyValue::Binary(d) => format_blob(f, d),
1188
AnyValue::BinaryOwned(d) => format_blob(f, d),
1189
#[cfg(feature = "dtype-date")]
1190
AnyValue::Date(v) => write!(f, "{}", date32_to_date(*v)),
1191
#[cfg(feature = "dtype-datetime")]
1192
AnyValue::Datetime(v, tu, tz) => fmt_datetime(f, *v, *tu, *tz),
1193
#[cfg(feature = "dtype-datetime")]
1194
AnyValue::DatetimeOwned(v, tu, tz) => {
1195
fmt_datetime(f, *v, *tu, tz.as_ref().map(|v| v.as_ref()))
1196
},
1197
#[cfg(feature = "dtype-duration")]
1198
AnyValue::Duration(v, tu) => fmt_duration_string(f, *v, *tu),
1199
#[cfg(feature = "dtype-time")]
1200
AnyValue::Time(_) => {
1201
let nt: chrono::NaiveTime = self.into();
1202
write!(f, "{nt}")
1203
},
1204
#[cfg(feature = "dtype-categorical")]
1205
AnyValue::Categorical(_, _)
1206
| AnyValue::CategoricalOwned(_, _)
1207
| AnyValue::Enum(_, _)
1208
| AnyValue::EnumOwned(_, _) => {
1209
let s = self.get_str().unwrap();
1210
write!(f, "\"{s}\"")
1211
},
1212
#[cfg(feature = "dtype-array")]
1213
AnyValue::Array(s, _size) => write!(f, "{}", s.fmt_list()),
1214
AnyValue::List(s) => write!(f, "{}", s.fmt_list()),
1215
#[cfg(feature = "object")]
1216
AnyValue::Object(v) => write!(f, "{v}"),
1217
#[cfg(feature = "object")]
1218
AnyValue::ObjectOwned(v) => write!(f, "{}", v.0.as_ref()),
1219
#[cfg(feature = "dtype-struct")]
1220
av @ AnyValue::Struct(_, _, _) => {
1221
let mut avs = vec![];
1222
av._materialize_struct_av(&mut avs);
1223
fmt_struct(f, &avs)
1224
},
1225
#[cfg(feature = "dtype-struct")]
1226
AnyValue::StructOwned(payload) => fmt_struct(f, &payload.0),
1227
#[cfg(feature = "dtype-decimal")]
1228
AnyValue::Decimal(v, _prec, scale) => fmt_decimal(f, *v, *scale),
1229
}
1230
}
1231
}
1232
1233
/// Utility struct to format a timezone aware datetime.
1234
#[allow(dead_code)]
1235
#[cfg(feature = "dtype-datetime")]
1236
pub struct PlTzAware<'a> {
1237
ndt: NaiveDateTime,
1238
tz: &'a str,
1239
}
1240
#[cfg(feature = "dtype-datetime")]
1241
impl<'a> PlTzAware<'a> {
1242
pub fn new(ndt: NaiveDateTime, tz: &'a str) -> Self {
1243
Self { ndt, tz }
1244
}
1245
}
1246
1247
#[cfg(feature = "dtype-datetime")]
1248
impl Display for PlTzAware<'_> {
1249
#[allow(unused_variables)]
1250
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
1251
#[cfg(feature = "timezones")]
1252
match self.tz.parse::<chrono_tz::Tz>() {
1253
Ok(tz) => {
1254
let dt_utc = chrono::Utc.from_local_datetime(&self.ndt).unwrap();
1255
let dt_tz_aware = dt_utc.with_timezone(&tz);
1256
write!(f, "{dt_tz_aware}")
1257
},
1258
Err(_) => write!(f, "invalid timezone"),
1259
}
1260
#[cfg(not(feature = "timezones"))]
1261
{
1262
panic!("activate 'timezones' feature")
1263
}
1264
}
1265
}
1266
1267
#[cfg(feature = "dtype-struct")]
1268
fn fmt_struct(f: &mut Formatter<'_>, vals: &[AnyValue]) -> fmt::Result {
1269
write!(f, "{{")?;
1270
if !vals.is_empty() {
1271
for v in &vals[..vals.len() - 1] {
1272
write!(f, "{v},")?;
1273
}
1274
// last value has no trailing comma
1275
write!(f, "{}", vals[vals.len() - 1])?;
1276
}
1277
write!(f, "}}")
1278
}
1279
1280
impl Series {
1281
pub fn fmt_list(&self) -> String {
1282
assert!(
1283
!self.dtype().is_object(),
1284
"nested Objects are not allowed\n\nYou probably got here by not setting a `return_dtype` on a UDF on Objects."
1285
);
1286
if self.is_empty() {
1287
return "[]".to_owned();
1288
}
1289
let mut result = "[".to_owned();
1290
let max_items = get_list_len_limit();
1291
let ellipsis = get_ellipsis();
1292
1293
match max_items {
1294
0 => write!(result, "{ellipsis}]").unwrap(),
1295
_ if max_items >= self.len() => {
1296
// this will always leave a trailing ", " after the last item
1297
// but for long lists, this is faster than checking against the length each time
1298
for item in self.rechunk().iter() {
1299
write!(result, "{item}, ").unwrap();
1300
}
1301
// remove trailing ", " and replace with closing brace
1302
result.truncate(result.len() - 2);
1303
result.push(']');
1304
},
1305
_ => {
1306
let s = self.slice(0, max_items);
1307
for (i, item) in s.iter().enumerate() {
1308
if i == max_items.saturating_sub(1) {
1309
write!(result, "{ellipsis} {}", self.get(self.len() - 1).unwrap()).unwrap();
1310
break;
1311
} else {
1312
write!(result, "{item}, ").unwrap();
1313
}
1314
}
1315
result.push(']');
1316
},
1317
};
1318
result
1319
}
1320
}
1321
1322
#[inline]
1323
#[cfg(feature = "dtype-decimal")]
1324
fn fmt_decimal(f: &mut Formatter<'_>, v: i128, scale: usize) -> fmt::Result {
1325
let mut fmt_buf = polars_compute::decimal::DecimalFmtBuffer::new();
1326
let trim_zeros = get_trim_decimal_zeros();
1327
f.write_str(fmt_float_string(fmt_buf.format_dec128(v, scale, trim_zeros, false)).as_str())
1328
}
1329
1330
#[cfg(all(
1331
test,
1332
feature = "temporal",
1333
feature = "dtype-date",
1334
feature = "dtype-datetime"
1335
))]
1336
#[allow(unsafe_op_in_unsafe_fn)]
1337
mod test {
1338
use crate::prelude::*;
1339
1340
#[test]
1341
fn test_fmt_list() {
1342
let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1343
PlSmallStr::from_static("a"),
1344
10,
1345
10,
1346
DataType::Int32,
1347
);
1348
builder.append_opt_slice(Some(&[1, 2, 3, 4, 5, 6]));
1349
builder.append_opt_slice(None);
1350
let list_long = builder.finish().into_series();
1351
1352
assert_eq!(
1353
r#"shape: (2,)
1354
Series: 'a' [list[i32]]
1355
[
1356
[1, 2, … 6]
1357
null
1358
]"#,
1359
format!("{list_long:?}")
1360
);
1361
1362
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "10") };
1363
1364
assert_eq!(
1365
r#"shape: (2,)
1366
Series: 'a' [list[i32]]
1367
[
1368
[1, 2, 3, 4, 5, 6]
1369
null
1370
]"#,
1371
format!("{list_long:?}")
1372
);
1373
1374
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1375
1376
assert_eq!(
1377
r#"shape: (2,)
1378
Series: 'a' [list[i32]]
1379
[
1380
[1, 2, 3, 4, 5, 6]
1381
null
1382
]"#,
1383
format!("{list_long:?}")
1384
);
1385
1386
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1387
1388
assert_eq!(
1389
r#"shape: (2,)
1390
Series: 'a' [list[i32]]
1391
[
1392
[…]
1393
null
1394
]"#,
1395
format!("{list_long:?}")
1396
);
1397
1398
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "1") };
1399
1400
assert_eq!(
1401
r#"shape: (2,)
1402
Series: 'a' [list[i32]]
1403
[
1404
[… 6]
1405
null
1406
]"#,
1407
format!("{list_long:?}")
1408
);
1409
1410
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "4") };
1411
1412
assert_eq!(
1413
r#"shape: (2,)
1414
Series: 'a' [list[i32]]
1415
[
1416
[1, 2, 3, … 6]
1417
null
1418
]"#,
1419
format!("{list_long:?}")
1420
);
1421
1422
let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1423
PlSmallStr::from_static("a"),
1424
10,
1425
10,
1426
DataType::Int32,
1427
);
1428
builder.append_opt_slice(Some(&[1]));
1429
builder.append_opt_slice(None);
1430
let list_short = builder.finish().into_series();
1431
1432
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1433
1434
assert_eq!(
1435
r#"shape: (2,)
1436
Series: 'a' [list[i32]]
1437
[
1438
[1]
1439
null
1440
]"#,
1441
format!("{list_short:?}")
1442
);
1443
1444
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "0") };
1445
1446
assert_eq!(
1447
r#"shape: (2,)
1448
Series: 'a' [list[i32]]
1449
[
1450
[…]
1451
null
1452
]"#,
1453
format!("{list_short:?}")
1454
);
1455
1456
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "-1") };
1457
1458
assert_eq!(
1459
r#"shape: (2,)
1460
Series: 'a' [list[i32]]
1461
[
1462
[1]
1463
null
1464
]"#,
1465
format!("{list_short:?}")
1466
);
1467
1468
let mut builder = ListPrimitiveChunkedBuilder::<Int32Type>::new(
1469
PlSmallStr::from_static("a"),
1470
10,
1471
10,
1472
DataType::Int32,
1473
);
1474
builder.append_opt_slice(Some(&[]));
1475
builder.append_opt_slice(None);
1476
let list_empty = builder.finish().into_series();
1477
1478
unsafe { std::env::set_var("POLARS_FMT_TABLE_CELL_LIST_LEN", "") };
1479
1480
assert_eq!(
1481
r#"shape: (2,)
1482
Series: 'a' [list[i32]]
1483
[
1484
[]
1485
null
1486
]"#,
1487
format!("{list_empty:?}")
1488
);
1489
}
1490
1491
#[test]
1492
fn test_fmt_temporal() {
1493
let s = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)])
1494
.into_date();
1495
assert_eq!(
1496
r#"shape: (3,)
1497
Series: 'Date' [date]
1498
[
1499
1970-01-02
1500
null
1501
1970-01-04
1502
]"#,
1503
format!("{:?}", s.into_series())
1504
);
1505
1506
let s = Int64Chunked::new(PlSmallStr::EMPTY, &[Some(1), None, Some(1_000_000_000_000)])
1507
.into_datetime(TimeUnit::Nanoseconds, None);
1508
assert_eq!(
1509
r#"shape: (3,)
1510
Series: '' [datetime[ns]]
1511
[
1512
1970-01-01 00:00:00.000000001
1513
null
1514
1970-01-01 00:16:40
1515
]"#,
1516
format!("{:?}", s.into_series())
1517
);
1518
}
1519
1520
#[test]
1521
fn test_fmt_chunkedarray() {
1522
let ca = Int32Chunked::new(PlSmallStr::from_static("Date"), &[Some(1), None, Some(3)]);
1523
assert_eq!(
1524
r#"shape: (3,)
1525
ChunkedArray: 'Date' [i32]
1526
[
1527
1
1528
null
1529
3
1530
]"#,
1531
format!("{ca:?}")
1532
);
1533
let ca = StringChunked::new(PlSmallStr::from_static("name"), &["a", "b"]);
1534
assert_eq!(
1535
r#"shape: (2,)
1536
ChunkedArray: 'name' [str]
1537
[
1538
"a"
1539
"b"
1540
]"#,
1541
format!("{ca:?}")
1542
);
1543
}
1544
}
1545
1546