1use base64::Engine as _;
10use polars::prelude::*;
11use std::borrow::Cow;
12use std::fmt::Write as _;
13
14pub fn f64_text(v: f64) -> String {
18 if v.is_nan() {
19 return "NaN".to_string();
20 }
21 if v.is_infinite() {
22 return if v > 0.0 { "inf" } else { "-inf" }.to_string();
23 }
24 let a = v.abs();
25 if a == 0.0 || (1e-5..1e16).contains(&a) {
28 whole_reads_as_float(format!("{v}"))
29 } else {
30 format!("{v:e}")
31 }
32}
33
34fn whole_reads_as_float(mut text: String) -> String {
37 if !text.contains('.') {
38 text.push_str(".0");
39 }
40 text
41}
42
43pub fn f32_text(v: f32) -> String {
46 if v.is_nan() {
47 return "NaN".to_string();
48 }
49 if v.is_infinite() {
50 return if v > 0.0 { "inf" } else { "-inf" }.to_string();
51 }
52 let a = v.abs();
53 if a == 0.0 || (1e-5..1e16).contains(&a) {
54 whole_reads_as_float(format!("{v}"))
55 } else {
56 format!("{v:e}")
57 }
58}
59
60pub fn out_of_range(value: &AnyValue) -> Option<String> {
64 use chrono::{DateTime, NaiveDate, TimeDelta};
65 let fits = |dt: Option<DateTime<chrono::Utc>>| {
66 dt.is_some_and(|dt| {
67 dt.checked_add_signed(TimeDelta::days(1)).is_some()
68 && dt.checked_sub_signed(TimeDelta::days(1)).is_some()
69 })
70 };
71 match value {
72 AnyValue::Date(days) => {
73 let fits = days
75 .checked_add(719_163)
76 .and_then(NaiveDate::from_num_days_from_ce_opt)
77 .is_some();
78 (!fits).then(|| format!("{days} days since 1970-01-01"))
79 }
80 AnyValue::Datetime(v, unit, _) | AnyValue::DatetimeOwned(v, unit, _) => {
81 let (dt, unit) = match unit {
82 TimeUnit::Milliseconds => (DateTime::from_timestamp_millis(*v), "ms"),
83 TimeUnit::Microseconds => (DateTime::from_timestamp_micros(*v), "us"),
84 TimeUnit::Nanoseconds => (Some(DateTime::from_timestamp_nanos(*v)), "ns"),
85 };
86 (!fits(dt)).then(|| format!("{v} {unit} since 1970-01-01 UTC"))
87 }
88 AnyValue::Time(ns) => {
89 (!(0..NANOS_PER_DAY).contains(ns)).then(|| format!("{ns} ns since midnight"))
90 }
91 _ => None,
92 }
93}
94
95const NANOS_PER_DAY: i64 = 86_400_000_000_000;
96
97pub fn str_value<'a>(value: &AnyValue<'a>) -> Cow<'a, str> {
101 match past_calendar_text(value) {
102 Some(text) => Cow::Owned(text),
103 None => value.str_value(),
104 }
105}
106
107pub fn list_preview(items: &Series) -> String {
110 const SHOWN: usize = 10;
111 let mut text = String::from("[");
112 for (i, item) in items.iter().take(SHOWN).enumerate() {
113 if i > 0 {
114 text.push_str(", ");
115 }
116 text.push_str(&str_value(&item));
117 }
118 if items.len() > SHOWN {
119 let _ = write!(text, "...] ({} items)", items.len());
120 } else {
121 text.push(']');
122 }
123 text
124}
125
126pub fn past_calendar_text(value: &AnyValue) -> Option<String> {
130 if let Some(text) = out_of_range(value) {
131 return Some(text);
132 }
133 nested_out_of_range(value).then(|| nested_compact(value, CELL_PREVIEW_BYTES).text)
134}
135
136fn nested_out_of_range(value: &AnyValue) -> bool {
139 let holds = |fields: &[Field]| fields.iter().any(|f| holds_calendar(f.dtype()));
140 match value {
141 AnyValue::List(s) | AnyValue::Array(s, _) => series_out_of_range(s),
142 AnyValue::Struct(_, _, fields) if !holds(fields) => false,
143 AnyValue::StructOwned(payload) if !holds(&payload.1) => false,
144 AnyValue::Struct(..) | AnyValue::StructOwned(_) => value
145 ._iter_struct_av()
146 .any(|field| out_of_range(&field).is_some() || nested_out_of_range(&field)),
147 _ => false,
148 }
149}
150
151fn series_out_of_range(s: &Series) -> bool {
154 if !holds_calendar(s.dtype()) {
155 return false;
156 }
157 match s.dtype() {
158 DataType::Date | DataType::Datetime(..) | DataType::Time => {
159 let Ok(stored) = s.to_physical_repr().cast(&DataType::Int64) else {
160 return false;
161 };
162 let Ok(stored) = stored.i64() else {
163 return false;
164 };
165 [stored.min(), stored.max()]
166 .into_iter()
167 .flatten()
168 .any(|v| stored_out_of_range(s.dtype(), v).is_some())
169 }
170 _ => s.iter().any(|item| nested_out_of_range(&item)),
171 }
172}
173
174pub fn stored_out_of_range(dtype: &DataType, stored: i64) -> Option<String> {
176 let value = match dtype {
177 DataType::Date => AnyValue::Date(i32::try_from(stored).ok()?),
178 DataType::Datetime(unit, _) => AnyValue::Datetime(stored, *unit, None),
179 DataType::Time => AnyValue::Time(stored),
180 _ => return None,
181 };
182 out_of_range(&value)
183}
184
185pub fn calendar_without_out_of_range(series: &Series) -> PolarsResult<Option<Series>> {
189 let dtype = series.dtype();
190 if !matches!(
191 dtype,
192 DataType::Date | DataType::Datetime(..) | DataType::Time
193 ) {
194 return Ok(None);
195 }
196 let stored = series.to_physical_repr().cast(&DataType::Int64)?;
197 let stored = stored.i64()?;
198 let past = |v: i64| stored_out_of_range(dtype, v).is_some();
199 if ![stored.min(), stored.max()].into_iter().flatten().any(past) {
200 return Ok(None);
201 }
202 let kept = stored.apply(|v| v.filter(|v| !past(*v)));
203 Ok(Some(
204 kept.into_series()
205 .cast(dtype)?
206 .with_name(series.name().clone()),
207 ))
208}
209
210pub fn holds_calendar(dtype: &DataType) -> bool {
212 match dtype {
213 DataType::Date | DataType::Datetime(..) | DataType::Time => true,
214 DataType::List(inner) | DataType::Array(inner, _) => holds_calendar(inner),
215 DataType::Struct(fields) => fields.iter().any(|f| holds_calendar(f.dtype())),
216 _ => false,
217 }
218}
219
220fn datetime_text(v: i64, unit: TimeUnit, zone: Option<&TimeZone>) -> String {
224 let digits = match unit {
225 TimeUnit::Milliseconds => "%.3f",
226 TimeUnit::Microseconds => "%.6f",
227 TimeUnit::Nanoseconds => "%.9f",
228 };
229 let format = match zone {
230 Some(_) => format!("%Y-%m-%d %H:%M:%S{digits} %:z"),
231 None => format!("%Y-%m-%d %H:%M:%S{digits}"),
232 };
233 let ca = Int64Chunked::from_slice(PlSmallStr::EMPTY, &[v]).into_datetime(unit, zone.cloned());
234 ca.to_string(&format)
235 .ok()
236 .and_then(|s| s.get(0).map(str::to_string))
237 .unwrap_or_else(|| AnyValue::Datetime(v, unit, zone).str_value().into_owned())
238}
239
240pub fn base64_text(bytes: &[u8]) -> String {
242 base64::engine::general_purpose::STANDARD.encode(bytes)
243}
244
245pub fn value_text(value: &AnyValue) -> String {
248 if let Some(text) = out_of_range(value) {
249 return text;
250 }
251 match value {
252 AnyValue::Null => String::new(),
253 AnyValue::Float64(v) => f64_text(*v),
254 AnyValue::Float32(v) => f32_text(*v),
255 AnyValue::Float16(v) => f32_text(f32::from(*v)),
256 AnyValue::Datetime(v, unit, zone) => datetime_text(*v, *unit, *zone),
257 AnyValue::DatetimeOwned(v, unit, zone) => datetime_text(*v, *unit, zone.as_deref()),
258 AnyValue::Duration(v, unit) => {
260 let mut out = String::new();
261 crate::nested_json::duration_iso(*v, *unit, &mut out);
262 out
263 }
264 AnyValue::Binary(bytes) => base64_text(bytes),
265 AnyValue::BinaryOwned(bytes) => base64_text(bytes),
266 AnyValue::List(_)
267 | AnyValue::Array(..)
268 | AnyValue::Struct(..)
269 | AnyValue::StructOwned(_) => {
270 let mut out = String::new();
271 write_nested(value, &mut out, None, 0, usize::MAX);
272 out
273 }
274 v => v.str_value().into_owned(),
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct Bounded {
283 pub text: String,
284 pub cut: bool,
285}
286
287pub fn nested_pretty(value: &AnyValue, budget: usize) -> Bounded {
291 let mut text = String::new();
292 let cut = !write_nested(value, &mut text, Some(0), 0, budget);
293 Bounded { text, cut }
294}
295
296pub fn nested_compact(value: &AnyValue, budget: usize) -> Bounded {
299 let mut text = String::new();
300 let cut = !write_nested(value, &mut text, None, 0, budget);
301 Bounded { text, cut }
302}
303
304pub fn is_nested_value(value: &AnyValue) -> bool {
306 matches!(
307 value,
308 AnyValue::List(_) | AnyValue::Array(..) | AnyValue::Struct(..) | AnyValue::StructOwned(_)
309 )
310}
311
312pub fn nested_len(value: &AnyValue) -> Option<usize> {
314 match value {
315 AnyValue::List(s) | AnyValue::Array(s, _) => Some(s.len()),
316 AnyValue::Struct(_, _, fields) => Some(fields.len()),
317 AnyValue::StructOwned(payload) => Some(payload.1.len()),
318 _ => None,
319 }
320}
321
322fn write_nested(
325 value: &AnyValue,
326 out: &mut String,
327 indent: Option<usize>,
328 depth: usize,
329 budget: usize,
330) -> bool {
331 if out.len() >= budget {
332 return false;
333 }
334 match value {
337 AnyValue::List(s) | AnyValue::Array(s, _) => write_items(
338 s.iter().map(|v| (None, v)),
339 s.len(),
340 ('[', ']'),
341 out,
342 indent,
343 depth,
344 budget,
345 ),
346 AnyValue::Struct(_, _, fields) => write_items(
347 fields
348 .iter()
349 .map(|f| Some(f.name().as_str()))
350 .zip(value._iter_struct_av()),
351 fields.len(),
352 ('{', '}'),
353 out,
354 indent,
355 depth,
356 budget,
357 ),
358 AnyValue::StructOwned(payload) => write_items(
359 payload
360 .1
361 .iter()
362 .map(|f| Some(f.name().as_str()))
363 .zip(payload.0.iter().cloned()),
364 payload.1.len(),
365 ('{', '}'),
366 out,
367 indent,
368 depth,
369 budget,
370 ),
371 AnyValue::String(s) => write_quoted(s, out, budget),
374 AnyValue::StringOwned(s) => write_quoted(s, out, budget),
375 AnyValue::Binary(b) => write_bytes(b, out, budget),
376 AnyValue::BinaryOwned(b) => write_bytes(b, out, budget),
377 v => {
378 out.push_str(&json_scalar(v));
379 true
380 }
381 }
382}
383
384fn write_items<'n, 'v>(
387 items: impl Iterator<Item = (Option<&'n str>, AnyValue<'v>)>,
388 len: usize,
389 (open, close): (char, char),
390 out: &mut String,
391 indent: Option<usize>,
392 depth: usize,
393 budget: usize,
394) -> bool {
395 let pad = |out: &mut String, depth: usize| {
396 if indent.is_some() {
397 out.push('\n');
398 out.extend(std::iter::repeat_n(" ", depth));
399 }
400 };
401 out.push(open);
402 if len == 0 {
403 out.push(close);
404 return true;
405 }
406 for (i, (name, item)) in items.enumerate() {
407 if i > 0 {
408 out.push(',');
409 if indent.is_none() {
410 out.push(' ');
411 }
412 }
413 pad(out, depth + 1);
414 if let Some(name) = name {
415 out.push_str(&json_string(name));
416 out.push_str(": ");
417 }
418 if !write_nested(&item, out, indent, depth + 1, budget) {
419 return false;
420 }
421 if out.len() >= budget && i + 1 < len {
422 return false;
423 }
424 }
425 pad(out, depth);
426 out.push(close);
427 true
428}
429
430fn write_quoted(s: &str, out: &mut String, budget: usize) -> bool {
433 out.push('"');
434 let head = prefix(s, budget.saturating_sub(out.len()));
435 escape_into(head, out);
436 if head.len() < s.len() {
437 return false;
438 }
439 out.push('"');
440 true
441}
442
443fn write_bytes(bytes: &[u8], out: &mut String, budget: usize) -> bool {
445 let room = budget.saturating_sub(out.len() + 1) / 4 * 3;
448 let head = &bytes[..bytes.len().min(room)];
449 out.push('"');
450 out.push_str(&base64_text(head));
451 if head.len() < bytes.len() {
452 return false;
453 }
454 out.push('"');
455 true
456}
457
458fn json_scalar(value: &AnyValue) -> String {
461 match value {
462 AnyValue::Null => "null".to_string(),
463 AnyValue::Boolean(b) => b.to_string(),
464 AnyValue::String(s) => json_string(s),
465 AnyValue::StringOwned(s) => json_string(s),
466 v if v.is_primitive_numeric() || matches!(v, AnyValue::Decimal(..)) => value_text(v),
467 v => json_string(&value_text(v)),
468 }
469}
470
471fn json_string(s: &str) -> String {
474 let mut out = String::with_capacity(s.len() + 2);
475 out.push('"');
476 escape_into(s, &mut out);
477 out.push('"');
478 out
479}
480
481fn invisible(c: char) -> bool {
486 c.is_control()
487 || matches!(
488 c,
489 '\u{a0}'
490 | '\u{ad}'
491 | '\u{34f}'
492 | '\u{61c}'
493 | '\u{115f}'..='\u{1160}'
494 | '\u{1680}'
495 | '\u{17b4}'..='\u{17b5}'
496 | '\u{180b}'..='\u{180f}'
497 | '\u{2000}'..='\u{200f}'
498 | '\u{2028}'..='\u{202f}'
499 | '\u{205f}'..='\u{206f}'
500 | '\u{3000}'
501 | '\u{3164}'
502 | '\u{fe00}'..='\u{fe0f}'
503 | '\u{feff}'
504 | '\u{ffa0}'
505 | '\u{fff0}'..='\u{fffb}'
506 | '\u{1bca0}'..='\u{1bca3}'
507 | '\u{1d173}'..='\u{1d17a}'
508 | '\u{e0000}'..='\u{e0fff}'
509 )
510}
511
512pub fn marked(c: char) -> bool {
516 c.is_control()
517 || matches!(
518 c,
519 '\u{61c}' | '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}'
520 )
521}
522
523fn escape_into(s: &str, out: &mut String) {
524 for c in s.chars() {
525 escape_char(c, out);
526 }
527}
528
529pub fn escape_char(c: char, out: &mut String) {
532 match c {
533 '\\' => out.push_str("\\\\"),
534 '"' => out.push_str("\\\""),
535 '\n' => out.push_str("\\n"),
536 '\r' => out.push_str("\\r"),
537 '\t' => out.push_str("\\t"),
538 '\0' => out.push_str("\\0"),
539 c if invisible(c) => {
540 let _ = write!(out, "\\u{{{:x}}}", c as u32);
541 }
542 c => out.push(c),
543 }
544}
545
546pub fn escaped(s: &str) -> String {
551 json_string(s)
552}
553
554pub fn escaped_bytes(bytes: &[u8]) -> String {
556 let mut out = String::with_capacity(bytes.len() + 3);
557 out.push_str("b\"");
558 for &b in bytes {
559 match b {
560 b'\\' => out.push_str("\\\\"),
561 b'"' => out.push_str("\\\""),
562 b'\n' => out.push_str("\\n"),
563 b'\r' => out.push_str("\\r"),
564 b'\t' => out.push_str("\\t"),
565 0x20..=0x7e => out.push(b as char),
566 b => {
567 let _ = write!(out, "\\x{b:02x}");
568 }
569 }
570 }
571 out.push('"');
572 out
573}
574
575pub fn hex_lines(bytes: &[u8], width: usize) -> Vec<String> {
578 let width = width.max(1);
579 bytes
580 .chunks(width)
581 .enumerate()
582 .map(|(i, chunk)| {
583 let mut line = format!("{:08x} ", i * width);
584 for b in chunk {
585 let _ = write!(line, " {b:02x}");
586 }
587 for _ in chunk.len()..width {
588 line.push_str(" ");
589 }
590 line.push_str(" ");
591 line.extend(chunk.iter().map(|&b| {
592 if (0x20..0x7f).contains(&b) {
593 b as char
594 } else {
595 '.'
596 }
597 }));
598 line
599 })
600 .collect()
601}
602
603fn has_marked(s: &str) -> bool {
607 s.bytes().any(|b| b < 0x20 || b == 0x7f)
608 || (s.bytes().any(|b| matches!(b, 0xc2 | 0xd8 | 0xe2)) && s.chars().any(marked))
609}
610
611pub const CELL_PREVIEW_BYTES: usize = 4096;
613
614pub fn cell_preview(s: &str, g: &crate::glyphs::Glyphs) -> String {
618 let head = prefix(s, CELL_PREVIEW_BYTES);
619 let mut text = preview(head, g).into_owned();
620 if head.len() < s.len() {
621 text.push_str(g.ellipsis);
622 }
623 text
624}
625
626pub fn preview<'a>(s: &'a str, g: &crate::glyphs::Glyphs) -> Cow<'a, str> {
630 if !has_marked(s) {
631 return Cow::Borrowed(s);
632 }
633 let mut out = String::with_capacity(s.len());
634 let mut chars = s.chars().peekable();
635 while let Some(c) = chars.next() {
636 match c {
637 '\r' if chars.peek() == Some(&'\n') => {
638 chars.next();
639 out.push_str(g.newline_mark);
640 }
641 '\n' => out.push_str(g.newline_mark),
642 '\t' => out.push_str(g.tab_mark),
643 c if marked(c) => out.push_str(g.control_mark),
644 c => out.push(c),
645 }
646 }
647 Cow::Owned(out)
648}
649
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
653pub struct TextFacts {
654 pub chars: usize,
655 pub lines: usize,
656 pub leading_spaces: usize,
657 pub trailing_spaces: usize,
658}
659
660pub fn text_facts(s: &str) -> TextFacts {
661 let lines = if s.is_empty() {
662 0
663 } else {
664 s.split('\n').count()
665 };
666 TextFacts {
667 chars: s.chars().count(),
668 lines,
669 leading_spaces: s.chars().take_while(|c| c.is_whitespace()).count(),
670 trailing_spaces: if s.chars().all(char::is_whitespace) {
671 0
672 } else {
673 s.chars().rev().take_while(|c| c.is_whitespace()).count()
674 },
675 }
676}
677
678pub fn prefix(s: &str, budget: usize) -> &str {
681 if s.len() <= budget {
682 return s;
683 }
684 let mut end = budget;
685 while !s.is_char_boundary(end) {
686 end -= 1;
687 }
688 &s[..end]
689}
690
691pub fn copy_text(column: &Column) -> PolarsResult<String> {
694 let value = column.get(0)?;
695 if is_nested_value(&value) {
696 let json = crate::nested_json::column_as_json(column)?;
697 return Ok(match json.get(0)? {
698 AnyValue::Null => String::new(),
699 v => v.str_value().into_owned(),
700 });
701 }
702 Ok(value_text(&value))
703}
704
705pub fn copy_len_floor(value: &AnyValue, stop: usize) -> usize {
711 match value {
712 AnyValue::String(s) => s.len(),
713 AnyValue::StringOwned(s) => s.len(),
714 AnyValue::Binary(b) => crate::clipboard::base64_len(b.len()),
715 AnyValue::BinaryOwned(b) => crate::clipboard::base64_len(b.len()),
716 v if is_nested_value(v) => json_len_floor(v, 0, stop),
717 v => value_text(v).len(),
719 }
720}
721
722fn json_len_floor(value: &AnyValue, so_far: usize, stop: usize) -> usize {
724 if so_far > stop {
725 return so_far;
726 }
727 let punctuation = |len: usize| 2 + len.saturating_sub(1);
729 match value {
730 AnyValue::List(s) | AnyValue::Array(s, _) => {
731 let mut n = so_far + punctuation(s.len());
732 for item in s.iter() {
733 if n > stop {
734 break;
735 }
736 n = json_len_floor(&item, n, stop);
737 }
738 n
739 }
740 AnyValue::Struct(_, _, fields) => {
741 let mut n = so_far + punctuation(fields.len());
742 for (field, item) in fields.iter().zip(value._iter_struct_av()) {
743 if n > stop {
744 break;
745 }
746 n = json_len_floor(&item, n + field.name().len() + 3, stop);
748 }
749 n
750 }
751 AnyValue::StructOwned(payload) => {
752 let mut n = so_far + punctuation(payload.1.len());
753 for (field, item) in payload.1.iter().zip(&payload.0) {
754 if n > stop {
755 break;
756 }
757 n = json_len_floor(item, n + field.name().len() + 3, stop);
758 }
759 n
760 }
761 AnyValue::String(s) => so_far + s.len() + 2,
762 AnyValue::StringOwned(s) => so_far + s.len() + 2,
763 AnyValue::Binary(b) => so_far + crate::clipboard::base64_len(b.len()) + 2,
764 AnyValue::BinaryOwned(b) => so_far + crate::clipboard::base64_len(b.len()) + 2,
765 _ => so_far + 1,
767 }
768}
769
770#[cfg(test)]
771mod tests {
772 use super::*;
773
774 #[test]
775 fn floats_read_back_to_the_same_bits() {
776 for v in [
777 1000000.125,
778 0.1 + 0.2,
779 -0.0,
780 1e300,
781 -2.5e-12,
782 123456789012345.67,
783 f64::MIN_POSITIVE,
784 f64::MAX,
785 1.0,
786 ] {
787 let text = f64_text(v);
788 let back: f64 = text.parse().unwrap();
789 assert_eq!(back.to_bits(), v.to_bits(), "{v} wrote {text}");
790 }
791 assert_eq!(f64_text(1000000.125), "1000000.125");
792 assert_eq!(f64_text(0.1 + 0.2), "0.30000000000000004");
793 assert_eq!(f64_text(1e300), "1e300");
794 assert_eq!(f64_text(1.0), "1.0");
795 }
796
797 #[test]
798 fn negative_zero_nan_and_the_infinities_are_spelled_out() {
799 assert_eq!(f64_text(-0.0), "-0.0");
800 assert_eq!(f64_text(0.0), "0.0");
801 assert_eq!(f64_text(f64::NAN), "NaN");
802 assert_eq!(f64_text(f64::INFINITY), "inf");
803 assert_eq!(f64_text(f64::NEG_INFINITY), "-inf");
804 assert_eq!(f32_text(-0.0), "-0.0");
805 assert_eq!(f32_text(f32::NAN), "NaN");
806 }
807
808 #[test]
809 fn an_f32_is_shortest_at_its_own_precision() {
810 assert_eq!(f32_text(0.1), "0.1");
811 assert_eq!(value_text(&AnyValue::Float32(16777217.0)), "16777216.0");
812 let v = 3.4028235e38f32;
813 assert_eq!(f32_text(v).parse::<f32>().unwrap(), v);
814 }
815
816 #[test]
819 fn exact_is_not_the_compact_display() {
820 let v = AnyValue::Float64(1000000.125);
821 assert_eq!(v.str_value(), "1.0000e6");
822 assert_eq!(value_text(&v), "1000000.125");
823 assert_eq!(value_text(&AnyValue::Int64(-1234567)), "-1234567");
824 }
825
826 #[test]
827 fn datetimes_keep_every_digit_of_their_unit_and_the_offset() {
828 let us = 1_704_164_645_000_120i64;
830 assert_eq!(
831 value_text(&AnyValue::Datetime(us, TimeUnit::Microseconds, None)),
832 "2024-01-02 03:04:05.000120"
833 );
834 assert_eq!(
835 value_text(&AnyValue::Datetime(
836 us * 1000 + 7,
837 TimeUnit::Nanoseconds,
838 None
839 )),
840 "2024-01-02 03:04:05.000120007"
841 );
842 let paris = TimeZone::opt_try_new(Some("Europe/Paris"))
843 .unwrap()
844 .unwrap();
845 assert_eq!(
846 value_text(&AnyValue::Datetime(
847 us,
848 TimeUnit::Microseconds,
849 Some(&paris)
850 )),
851 "2024-01-02 04:04:05.000120 +01:00"
852 );
853 let ms = 1_704_164_645_000i64;
854 assert_eq!(
855 value_text(&AnyValue::Datetime(ms, TimeUnit::Milliseconds, None)),
856 "2024-01-02 03:04:05.000"
857 );
858 }
859
860 #[test]
861 fn dates_times_durations_and_decimals_are_whole() {
862 assert_eq!(value_text(&AnyValue::Date(19724)), "2024-01-02");
863 assert_eq!(
864 value_text(&AnyValue::Time(3_723_000_000_123)),
865 "01:02:03.000000123"
866 );
867 assert_eq!(
868 value_text(&AnyValue::Duration(90_061_000_001, TimeUnit::Microseconds)),
869 "PT90061.000001S"
870 );
871 assert_eq!(value_text(&AnyValue::Decimal(-123450, 10, 4)), "-12.3450");
872 }
873
874 #[test]
875 fn a_null_is_empty_and_bytes_are_base64() {
876 assert_eq!(value_text(&AnyValue::Null), "");
877 assert_eq!(value_text(&AnyValue::Binary(b"hi\x00")), "aGkA");
878 }
879
880 #[test]
881 fn the_escaped_view_tells_a_break_from_a_literal_backslash() {
882 assert_eq!(escaped("line1\nline2"), r#""line1\nline2""#);
883 assert_eq!(escaped(r"line1\nline2"), r#""line1\\nline2""#);
884 assert_eq!(escaped("a\tb\r\n"), r#""a\tb\r\n""#);
885 assert_eq!(escaped(""), r#""""#);
886 assert_eq!(escaped(" pad "), r#"" pad ""#);
887 assert_eq!(escaped("say \"hi\""), r#""say \"hi\"""#);
888 assert_eq!(escaped("\u{1b}[0m"), r#""\u{1b}[0m""#);
889 assert_eq!(
890 escaped("no\u{a0}break\u{200b}"),
891 r#""no\u{a0}break\u{200b}""#
892 );
893 assert_eq!(escaped("東京"), "\"東京\"");
894 }
895
896 #[test]
897 fn bytes_escape_and_dump() {
898 assert_eq!(escaped_bytes(b"ab\x00\xff\""), r#"b"ab\x00\xff\"""#);
899 let lines = hex_lines(b"Hello, world!\x00\x01", 8);
900 assert_eq!(lines.len(), 2);
901 assert_eq!(lines[0], "00000000 48 65 6c 6c 6f 2c 20 77 Hello, w");
902 assert_eq!(lines[1], "00000008 6f 72 6c 64 21 00 01 orld!..");
903 }
904
905 #[test]
906 fn the_preview_marks_breaks_tabs_and_controls() {
907 let g = crate::glyphs::unicode();
908 assert_eq!(preview("line1\nline2", g), "line1¶line2");
909 assert_eq!(preview("tab\tseparated", g), "tab»separated");
910 assert_eq!(preview("crlf\r\nend", g), "crlf¶end");
911 assert_eq!(preview("bell\u{7}", g), "bell¤");
912 assert_eq!(preview("c1\u{85}", g), "c1¤");
913 assert!(matches!(preview("plain é 東京", g), Cow::Borrowed(_)));
914 let a = crate::glyphs::ascii();
915 assert_eq!(preview("a\nb\tc\u{1b}", a), "a$b>c?");
916 }
917
918 #[test]
919 fn text_facts_count_what_the_screen_hides() {
920 let f = text_facts(" two\nlines ");
921 assert_eq!(
922 f,
923 TextFacts {
924 chars: 12,
925 lines: 2,
926 leading_spaces: 2,
927 trailing_spaces: 1
928 }
929 );
930 assert_eq!(text_facts("").lines, 0);
931 assert_eq!(text_facts(" ").trailing_spaces, 0);
932 assert_eq!(text_facts(" ").leading_spaces, 3);
933 }
934
935 #[test]
936 fn prefix_cuts_at_a_character_boundary() {
937 assert_eq!(prefix("東京", 4), "東");
938 assert_eq!(prefix("abc", 10), "abc");
939 }
940
941 fn list(values: &[f64]) -> AnyValue<'static> {
942 AnyValue::List(Series::new("".into(), values))
943 }
944
945 #[test]
946 fn nested_values_lay_out_one_item_per_line_with_exact_scalars() {
947 let pretty = nested_pretty(&list(&[1000000.125, -0.0]), usize::MAX);
948 assert_eq!(pretty.text, "[\n 1000000.125,\n -0.0\n]");
949 assert!(!pretty.cut);
950 assert_eq!(value_text(&list(&[1.5, 2.0])), "[1.5, 2.0]");
951 assert_eq!(nested_pretty(&list(&[]), 100).text, "[]");
952
953 let df = df!("name" => ["a\"b"], "n" => [Some(1i64)]).unwrap();
954 let s = df.into_struct("s".into()).into_series();
955 let value = s.get(0).unwrap();
956 assert_eq!(nested_len(&value), Some(2));
957 assert_eq!(
958 nested_pretty(&value, usize::MAX).text,
959 "{\n \"name\": \"a\\\"b\",\n \"n\": 1\n}"
960 );
961 }
962
963 #[test]
964 fn a_long_nested_value_stops_at_the_budget() {
965 let values: Vec<f64> = (0..10_000).map(f64::from).collect();
966 let pretty = nested_pretty(&list(&values), 200);
967 assert!(pretty.cut);
968 assert!(pretty.text.len() < 260, "{}", pretty.text.len());
969 }
970
971 #[test]
973 fn a_date_past_the_calendar_is_its_stored_number() {
974 let us = AnyValue::Datetime(i64::MIN + 1, TimeUnit::Microseconds, None);
975 assert_eq!(
976 value_text(&us),
977 "-9223372036854775807 us since 1970-01-01 UTC"
978 );
979 let paris = TimeZone::opt_try_new(Some("Europe/Paris"))
980 .unwrap()
981 .unwrap();
982 let ms = AnyValue::Datetime(i64::MAX, TimeUnit::Milliseconds, Some(&paris));
983 assert!(value_text(&ms).starts_with("9223372036854775807 ms"));
984 assert_eq!(
985 value_text(&AnyValue::Date(i32::MAX)),
986 "2147483647 days since 1970-01-01"
987 );
988 assert_eq!(
990 value_text(&AnyValue::Datetime(i64::MAX, TimeUnit::Nanoseconds, None)),
991 "2262-04-11 23:47:16.854775807"
992 );
993 assert_eq!(value_text(&AnyValue::Date(-800_000)), "-0221-09-04");
994 }
995
996 #[test]
1000 fn str_value_never_panics_on_a_date_past_the_calendar() {
1001 let paris = TimeZone::opt_try_new(Some("Europe/Paris"))
1002 .unwrap()
1003 .unwrap();
1004 for (unit, name) in [
1005 (TimeUnit::Milliseconds, "ms"),
1006 (TimeUnit::Microseconds, "us"),
1007 ] {
1008 for zone in [None, Some(&paris)] {
1009 for v in [i64::MIN + 1, i64::MAX] {
1010 assert_eq!(
1011 str_value(&AnyValue::Datetime(v, unit, zone)),
1012 format!("{v} {name} since 1970-01-01 UTC"),
1013 "{unit:?} {zone:?}"
1014 );
1015 }
1016 let epoch = AnyValue::Datetime(0, unit, zone);
1017 assert_eq!(str_value(&epoch), epoch.str_value());
1018 }
1019 }
1020 for zone in [None, Some(&paris)] {
1022 for v in [i64::MIN + 1, i64::MAX] {
1023 let value = AnyValue::Datetime(v, TimeUnit::Nanoseconds, zone);
1024 assert_eq!(str_value(&value), value.str_value());
1025 }
1026 }
1027 assert_eq!(
1028 str_value(&AnyValue::Date(i32::MAX)),
1029 "2147483647 days since 1970-01-01"
1030 );
1031 assert_eq!(
1032 str_value(&AnyValue::Date(i32::MIN)),
1033 "-2147483648 days since 1970-01-01"
1034 );
1035 assert_eq!(str_value(&AnyValue::Date(0)), "1970-01-01");
1036 assert_eq!(str_value(&AnyValue::Time(-1)), "-1 ns since midnight");
1037 assert_eq!(
1038 str_value(&AnyValue::Time(NANOS_PER_DAY)),
1039 "86400000000000 ns since midnight"
1040 );
1041 assert_eq!(
1042 str_value(&AnyValue::Time(NANOS_PER_DAY - 1)),
1043 "23:59:59.999999999"
1044 );
1045 for unit in [
1046 TimeUnit::Milliseconds,
1047 TimeUnit::Microseconds,
1048 TimeUnit::Nanoseconds,
1049 ] {
1050 for v in [i64::MIN, i64::MIN + 1, i64::MAX] {
1051 let value = AnyValue::Duration(v, unit);
1052 assert_eq!(str_value(&value), value.str_value());
1053 }
1054 }
1055
1056 let stamps = |values: &[i64]| {
1057 Series::new("".into(), values)
1058 .cast(&DataType::Datetime(TimeUnit::Microseconds, None))
1059 .unwrap()
1060 };
1061 let past = stamps(&[0, i64::MIN + 1]);
1062 assert_eq!(
1063 str_value(&AnyValue::List(past.clone())),
1064 r#"["1970-01-01 00:00:00.000000", "-9223372036854775807 us since 1970-01-01 UTC"]"#
1065 );
1066 let fine = AnyValue::List(stamps(&[0]));
1067 assert_eq!(str_value(&fine), fine.str_value());
1068 let nested = AnyValue::List(Series::new("".into(), [past.clone()]));
1069 assert!(str_value(&nested).contains("-9223372036854775807 us"));
1070 let row = StructChunked::from_series(
1071 "".into(),
1072 2,
1073 [
1074 Series::new("id".into(), [1i64, 2]),
1075 past.with_name("at".into()),
1076 ]
1077 .iter(),
1078 )
1079 .unwrap()
1080 .into_series();
1081 assert_eq!(
1082 str_value(&row.get(1).unwrap()),
1083 r#"{"id": 2, "at": "-9223372036854775807 us since 1970-01-01 UTC"}"#
1084 );
1085 assert_eq!(
1086 str_value(&row.get(0).unwrap()),
1087 row.get(0).unwrap().str_value()
1088 );
1089 }
1090
1091 #[test]
1094 fn values_past_the_calendar_become_null() {
1095 let dates = Series::new("d".into(), [Some(i32::MIN), Some(0), None, Some(i32::MAX)])
1096 .cast(&DataType::Date)
1097 .unwrap();
1098 let kept = calendar_without_out_of_range(&dates).unwrap().unwrap();
1099 assert_eq!(kept.dtype(), &DataType::Date);
1100 assert_eq!(kept.name().as_str(), "d");
1101 assert_eq!(kept.null_count(), 3);
1102 assert_eq!(kept.get(1).unwrap(), AnyValue::Date(0));
1103 let fine = dates.slice(1, 2);
1104 assert!(calendar_without_out_of_range(&fine).unwrap().is_none());
1105 let numbers = Series::new("n".into(), [i64::MIN, i64::MAX]);
1106 assert!(calendar_without_out_of_range(&numbers).unwrap().is_none());
1107 }
1108
1109 #[test]
1110 fn the_escaped_view_spells_out_every_invisible_character() {
1111 for c in [
1112 '\u{61c}',
1113 '\u{2000}',
1114 '\u{3000}',
1115 '\u{180e}',
1116 '\u{fe0f}',
1117 '\u{e0041}',
1118 '\u{9b}',
1119 '\u{2066}',
1120 '\u{2028}',
1121 ] {
1122 assert_eq!(
1123 escaped(&c.to_string()),
1124 format!("\"\\u{{{:x}}}\"", c as u32)
1125 );
1126 }
1127 }
1128
1129 #[test]
1132 fn the_preview_marks_direction_controls() {
1133 let g = crate::glyphs::unicode();
1134 assert_eq!(preview("a\u{202e}b\u{202c}c\u{61c}", g), "a¤b¤c¤");
1135 assert!(matches!(preview("שלום مرحبا — x", g), Cow::Borrowed(_)));
1136 }
1137
1138 #[test]
1139 fn a_cell_previews_only_the_start_of_a_huge_value() {
1140 let g = crate::glyphs::unicode();
1141 let huge = "x".repeat(CELL_PREVIEW_BYTES * 10);
1142 let cell = cell_preview(&huge, g);
1143 assert_eq!(cell.len(), CELL_PREVIEW_BYTES + g.ellipsis.len());
1144 assert!(cell.ends_with(g.ellipsis));
1145 assert_eq!(cell_preview("a\nb", g), "a¶b");
1146 }
1147
1148 #[test]
1150 fn a_nested_value_is_cut_inside_a_huge_item() {
1151 let huge = "y".repeat(1 << 20);
1152 let s = Series::new("".into(), ["a", huge.as_str()]);
1153 let pretty = nested_pretty(&AnyValue::List(s), 200);
1154 assert!(pretty.cut);
1155 assert!(pretty.text.len() <= 210, "{}", pretty.text.len());
1156 assert!(pretty.text.starts_with("[\n \"a\",\n \"yyy"));
1157 let bytes = Series::new("".into(), [vec![7u8; 1 << 20].as_slice()]);
1158 let compact = nested_compact(&AnyValue::List(bytes), 100);
1159 assert!(compact.cut && compact.text.len() <= 100, "{}", compact.text);
1160 let many = Series::new("".into(), (0..1_000_000i64).collect::<Vec<_>>());
1161 let compact = nested_compact(&AnyValue::List(many), 50);
1162 assert!(compact.cut && compact.text.len() < 60, "{}", compact.text);
1163 }
1164
1165 #[test]
1168 fn copy_len_floor_is_a_floor_and_stops_early() {
1169 let point = StructChunked::from_columns(
1170 "p".into(),
1171 1,
1172 &[
1173 Column::new("x".into(), [Some(1i64)]),
1174 Column::new("label".into(), [None::<&str>]),
1175 ],
1176 )
1177 .unwrap()
1178 .into_column();
1179 let columns = [
1180 Column::new("s".into(), ["tab\there \"quoted\" été"]),
1181 Column::new("b".into(), [b"Hi\x00".as_slice()]),
1182 Column::new("f".into(), [1000000.125f64]),
1183 Column::new("n".into(), [None::<i64>]),
1184 Column::new("l".into(), [list(&[1.5, 2.0])]),
1185 Column::new("t".into(), [Series::new("".into(), ["a\"b", "", "日本"])]),
1186 Column::new("e".into(), [Series::new_empty("".into(), &DataType::Int64)]),
1187 point,
1188 ];
1189 for column in columns {
1190 let value = column.get(0).unwrap();
1191 let text = copy_text(&column).unwrap();
1192 let floor = copy_len_floor(&value, usize::MAX);
1193 assert!(
1194 floor <= text.len(),
1195 "{}: {floor} over {text:?}",
1196 column.name()
1197 );
1198 }
1199 assert_eq!(copy_len_floor(&AnyValue::String("abcdef"), 0), 6);
1201 assert_eq!(copy_len_floor(&AnyValue::Binary(b"abcd"), 0), 8);
1202 let many = Series::new("".into(), (0..1_000_000i64).collect::<Vec<_>>());
1204 assert!(copy_len_floor(&AnyValue::List(many), 1024) > 1024);
1205 }
1206
1207 #[test]
1208 fn copy_text_is_exact_and_nested_is_json() {
1209 let floats = Column::new("f".into(), [1000000.125f64]);
1210 assert_eq!(copy_text(&floats).unwrap(), "1000000.125");
1211 let nulls = Column::new("s".into(), [None::<&str>]);
1212 assert_eq!(copy_text(&nulls).unwrap(), "");
1213 let lists = Column::new("l".into(), [list(&[1.5, 2.0])]);
1214 assert_eq!(copy_text(&lists).unwrap(), "[1.5,2.0]");
1215 }
1216
1217 #[test]
1219 fn a_list_previews_ten_items_and_counts_the_rest() {
1220 let few = Series::new("".into(), &["a", "b"]);
1221 assert_eq!(list_preview(&few), "[a, b]");
1222 let many = Series::new("".into(), (0..12).collect::<Vec<i32>>());
1223 assert_eq!(
1224 list_preview(&many),
1225 "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9...] (12 items)"
1226 );
1227 let ten = Series::new("".into(), (0..10).collect::<Vec<i32>>());
1228 assert_eq!(list_preview(&ten), "[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]");
1229 }
1230}