Skip to main content

datui_lib/
exact.rs

1//! A stored value as text, exactly, apart from how the table previews it.
2//!
3//! The table's preview rounds floats, groups digits and cuts long text to fit a
4//! cell; a copy or the inspector gives back what is stored. A float is the
5//! shortest decimal that reads back to the same bits, never Polars' compact
6//! display; a datetime carries every digit of its unit and its zone's offset.
7//! Exact means the stored value: a CSV's `1.50` was stored as `1.5`.
8
9use base64::Engine as _;
10use polars::prelude::*;
11use std::borrow::Cow;
12use std::fmt::Write as _;
13
14/// The shortest decimal that parses back to `v`: plain notation over the
15/// magnitudes people read as plain numbers, exponent notation outside them.
16/// NaN, the infinities and negative zero are spelled out rather than lost.
17pub 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    // Rust's Display and LowerExp are both shortest round-trip; Display alone
26    // would write 1e300 as 301 digits.
27    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
34/// `1.0`, not `1`: a whole float keeps its point, as Polars writes it, so it
35/// does not read as an integer. Parses back the same.
36fn whole_reads_as_float(mut text: String) -> String {
37    if !text.contains('.') {
38        text.push_str(".0");
39    }
40    text
41}
42
43/// [`f64_text`] for an `f32`, shortest at the `f32`'s own precision: widened
44/// to `f64` first, `0.1f32` would read `0.10000000149011612`.
45pub 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
60/// A date, datetime or time outside the calendar's range, as its stored number:
61/// Polars panics formatting one (a sentinel like `i64::MIN` microseconds is real
62/// data). A day of margin either side leaves room for a zone's offset.
63pub 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            // Days from 0001-01-01 to the epoch, as Polars counts them.
74            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
97/// [`AnyValue::str_value`], which panics on a date past the calendar, with such a
98/// value written as its stored number, and a list or struct holding one written
99/// as [`nested_compact`] does.
100pub 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
107/// How the table previews a list cell: its first ten items, and how many there
108/// are when that is not all of them (`[a, b...] (12 items)`). Past
109/// [`CELL_PREVIEW_BYTES`], more than a cell shows, it stops with `...`.
110pub fn list_preview(items: &Series) -> String {
111    const SHOWN: usize = 10;
112    let mut text = String::from("[");
113    for (i, item) in items.iter().take(SHOWN).enumerate() {
114        if text.len() > CELL_PREVIEW_BYTES {
115            text.push_str("...");
116            return text;
117        }
118        if i > 0 {
119            text.push_str(", ");
120        }
121        text.push_str(prefix(&str_value(&item), CELL_PREVIEW_BYTES));
122    }
123    if items.len() > SHOWN {
124        let _ = write!(text, "...] ({} items)", items.len());
125    } else {
126        text.push(']');
127    }
128    text
129}
130
131/// The text [`str_value`] gives a value Polars panics formatting: a date past
132/// the calendar, or a list or struct holding one. `None` for any other value,
133/// which Polars formats as usual.
134pub fn past_calendar_text(value: &AnyValue) -> Option<String> {
135    if let Some(text) = out_of_range(value) {
136        return Some(text);
137    }
138    nested_out_of_range(value).then(|| nested_compact(value, CELL_PREVIEW_BYTES).text)
139}
140
141/// Whether a list, array or struct holds a value [`out_of_range`] names. Only one
142/// whose type holds a date, datetime or time is looked into.
143fn nested_out_of_range(value: &AnyValue) -> bool {
144    let holds = |fields: &[Field]| fields.iter().any(|f| holds_calendar(f.dtype()));
145    match value {
146        AnyValue::List(s) | AnyValue::Array(s, _) => series_out_of_range(s),
147        AnyValue::Struct(_, _, fields) if !holds(fields) => false,
148        AnyValue::StructOwned(payload) if !holds(&payload.1) => false,
149        AnyValue::Struct(..) | AnyValue::StructOwned(_) => value
150            ._iter_struct_av()
151            .any(|field| out_of_range(&field).is_some() || nested_out_of_range(&field)),
152        _ => false,
153    }
154}
155
156/// Whether any value of `s` is one [`out_of_range`] names: a flat column by its
157/// least and greatest stored number, a nested one item by item.
158fn series_out_of_range(s: &Series) -> bool {
159    if !holds_calendar(s.dtype()) {
160        return false;
161    }
162    match s.dtype() {
163        DataType::Date | DataType::Datetime(..) | DataType::Time => {
164            let Ok(stored) = s.to_physical_repr().cast(&DataType::Int64) else {
165                return false;
166            };
167            let Ok(stored) = stored.i64() else {
168                return false;
169            };
170            [stored.min(), stored.max()]
171                .into_iter()
172                .flatten()
173                .any(|v| stored_out_of_range(s.dtype(), v).is_some())
174        }
175        _ => s.iter().any(|item| nested_out_of_range(&item)),
176    }
177}
178
179/// [`out_of_range`] for a value of `dtype` stored as the number `stored`.
180pub fn stored_out_of_range(dtype: &DataType, stored: i64) -> Option<String> {
181    let value = match dtype {
182        DataType::Date => AnyValue::Date(i32::try_from(stored).ok()?),
183        DataType::Datetime(unit, _) => AnyValue::Datetime(stored, *unit, None),
184        DataType::Time => AnyValue::Time(stored),
185        _ => return None,
186    };
187    out_of_range(&value)
188}
189
190/// A date, datetime or time column with each value [`out_of_range`] names as
191/// null, for the Polars operations that overflow or panic on one. `None` when
192/// it holds none, the common case, which costs a min and a max.
193pub fn calendar_without_out_of_range(series: &Series) -> PolarsResult<Option<Series>> {
194    let dtype = series.dtype();
195    if !matches!(
196        dtype,
197        DataType::Date | DataType::Datetime(..) | DataType::Time
198    ) {
199        return Ok(None);
200    }
201    let stored = series.to_physical_repr().cast(&DataType::Int64)?;
202    let stored = stored.i64()?;
203    let past = |v: i64| stored_out_of_range(dtype, v).is_some();
204    if ![stored.min(), stored.max()].into_iter().flatten().any(past) {
205        return Ok(None);
206    }
207    let kept = stored.apply(|v| v.filter(|v| !past(*v)));
208    Ok(Some(
209        kept.into_series()
210            .cast(dtype)?
211            .with_name(series.name().clone()),
212    ))
213}
214
215/// Whether `dtype` is, or holds, a date, datetime or time.
216pub fn holds_calendar(dtype: &DataType) -> bool {
217    match dtype {
218        DataType::Date | DataType::Datetime(..) | DataType::Time => true,
219        DataType::List(inner) | DataType::Array(inner, _) => holds_calendar(inner),
220        DataType::Struct(fields) => fields.iter().any(|f| holds_calendar(f.dtype())),
221        _ => false,
222    }
223}
224
225/// A datetime with every digit its unit stores, and the zone's offset when it
226/// has a zone. Polars' own display drops trailing zeros and names the zone by
227/// abbreviation, which several zones share.
228fn datetime_text(v: i64, unit: TimeUnit, zone: Option<&TimeZone>) -> String {
229    let digits = match unit {
230        TimeUnit::Milliseconds => "%.3f",
231        TimeUnit::Microseconds => "%.6f",
232        TimeUnit::Nanoseconds => "%.9f",
233    };
234    let format = match zone {
235        Some(_) => format!("%Y-%m-%d %H:%M:%S{digits} %:z"),
236        None => format!("%Y-%m-%d %H:%M:%S{digits}"),
237    };
238    let ca = Int64Chunked::from_slice(PlSmallStr::EMPTY, &[v]).into_datetime(unit, zone.cloned());
239    ca.to_string(&format)
240        .ok()
241        .and_then(|s| s.get(0).map(str::to_string))
242        .unwrap_or_else(|| AnyValue::Datetime(v, unit, zone).str_value().into_owned())
243}
244
245/// Bytes as base64, the spelling every copy and export of a binary uses.
246pub fn base64_text(bytes: &[u8]) -> String {
247    base64::engine::general_purpose::STANDARD.encode(bytes)
248}
249
250/// A value as exact text. A null is empty, as in an export; a list or struct
251/// is compact JSON-like text over exact scalars; bytes are base64.
252pub fn value_text(value: &AnyValue) -> String {
253    if let Some(text) = out_of_range(value) {
254        return text;
255    }
256    match value {
257        AnyValue::Null => String::new(),
258        AnyValue::Float64(v) => f64_text(*v),
259        AnyValue::Float32(v) => f32_text(*v),
260        AnyValue::Float16(v) => f32_text(f32::from(*v)),
261        AnyValue::Datetime(v, unit, zone) => datetime_text(*v, *unit, *zone),
262        AnyValue::DatetimeOwned(v, unit, zone) => datetime_text(*v, *unit, zone.as_deref()),
263        // ISO 8601 seconds, as a CSV export and every other copy write it.
264        AnyValue::Duration(v, unit) => {
265            let mut out = String::new();
266            crate::export::nested_json::duration_iso(*v, *unit, &mut out);
267            out
268        }
269        AnyValue::Binary(bytes) => base64_text(bytes),
270        AnyValue::BinaryOwned(bytes) => base64_text(bytes),
271        AnyValue::List(_)
272        | AnyValue::Array(..)
273        | AnyValue::Struct(..)
274        | AnyValue::StructOwned(_) => {
275            let mut out = String::new();
276            write_nested(value, &mut out, None, 0, usize::MAX);
277            out
278        }
279        // Integers, booleans, strings, dates, times, decimals and
280        // categories: Polars' text for these is already the whole value.
281        v => v.str_value().into_owned(),
282    }
283}
284
285/// Text cut to a byte budget, and whether anything was cut.
286#[derive(Debug, Clone, PartialEq, Eq)]
287pub struct Bounded {
288    pub text: String,
289    pub cut: bool,
290}
291
292/// A list, array or struct laid out one item per line, indented, stopping once
293/// `budget` bytes are written. Scalars inside are exact; text is quoted and
294/// escaped so an item's edges are visible.
295pub fn nested_pretty(value: &AnyValue, budget: usize) -> Bounded {
296    let mut text = String::new();
297    let cut = !write_nested(value, &mut text, Some(0), 0, budget);
298    Bounded { text, cut }
299}
300
301/// A list, array or struct on one line, stopping once `budget` bytes are
302/// written: for a preview, which shows only the start.
303pub fn nested_compact(value: &AnyValue, budget: usize) -> Bounded {
304    let mut text = String::new();
305    let cut = !write_nested(value, &mut text, None, 0, budget);
306    Bounded { text, cut }
307}
308
309/// Whether `value` is a list, array or struct.
310pub fn is_nested_value(value: &AnyValue) -> bool {
311    matches!(
312        value,
313        AnyValue::List(_) | AnyValue::Array(..) | AnyValue::Struct(..) | AnyValue::StructOwned(_)
314    )
315}
316
317/// How many items a list or array holds, or fields a struct has.
318pub fn nested_len(value: &AnyValue) -> Option<usize> {
319    match value {
320        AnyValue::List(s) | AnyValue::Array(s, _) => Some(s.len()),
321        AnyValue::Struct(_, _, fields) => Some(fields.len()),
322        AnyValue::StructOwned(payload) => Some(payload.1.len()),
323        _ => None,
324    }
325}
326
327/// Write `value` into `out`. `indent` is the current depth when laid out one
328/// item per line, `None` for compact. Returns false once the budget ran out.
329fn write_nested(
330    value: &AnyValue,
331    out: &mut String,
332    indent: Option<usize>,
333    depth: usize,
334    budget: usize,
335) -> bool {
336    if out.len() >= budget {
337        return false;
338    }
339    // Items are taken one at a time: a million-item list stops at the budget
340    // rather than being turned into a million values first.
341    match value {
342        AnyValue::List(s) | AnyValue::Array(s, _) => write_items(
343            s.iter().map(|v| (None, v)),
344            s.len(),
345            ('[', ']'),
346            out,
347            indent,
348            depth,
349            budget,
350        ),
351        AnyValue::Struct(_, _, fields) => write_items(
352            fields
353                .iter()
354                .map(|f| Some(f.name().as_str()))
355                .zip(value._iter_struct_av()),
356            fields.len(),
357            ('{', '}'),
358            out,
359            indent,
360            depth,
361            budget,
362        ),
363        AnyValue::StructOwned(payload) => write_items(
364            payload
365                .1
366                .iter()
367                .map(|f| Some(f.name().as_str()))
368                .zip(payload.0.iter().cloned()),
369            payload.1.len(),
370            ('{', '}'),
371            out,
372            indent,
373            depth,
374            budget,
375        ),
376        // Text and bytes are cut at the budget too: one huge string in a list is
377        // as costly as a huge cell.
378        AnyValue::String(s) => write_quoted(s, out, budget),
379        AnyValue::StringOwned(s) => write_quoted(s, out, budget),
380        AnyValue::Binary(b) => write_bytes(b, out, budget),
381        AnyValue::BinaryOwned(b) => write_bytes(b, out, budget),
382        v => {
383            out.push_str(&json_scalar(v));
384            true
385        }
386    }
387}
388
389/// The items of a list, array or struct between `open` and `close`, each named
390/// when a struct's. False once the budget ran out.
391fn write_items<'n, 'v>(
392    items: impl Iterator<Item = (Option<&'n str>, AnyValue<'v>)>,
393    len: usize,
394    (open, close): (char, char),
395    out: &mut String,
396    indent: Option<usize>,
397    depth: usize,
398    budget: usize,
399) -> bool {
400    let pad = |out: &mut String, depth: usize| {
401        if indent.is_some() {
402            out.push('\n');
403            out.extend(std::iter::repeat_n("  ", depth));
404        }
405    };
406    out.push(open);
407    if len == 0 {
408        out.push(close);
409        return true;
410    }
411    for (i, (name, item)) in items.enumerate() {
412        if i > 0 {
413            out.push(',');
414            if indent.is_none() {
415                out.push(' ');
416            }
417        }
418        pad(out, depth + 1);
419        if let Some(name) = name {
420            out.push_str(&json_string(name));
421            out.push_str(": ");
422        }
423        if !write_nested(&item, out, indent, depth + 1, budget) {
424            return false;
425        }
426        if out.len() >= budget && i + 1 < len {
427            return false;
428        }
429    }
430    pad(out, depth);
431    out.push(close);
432    true
433}
434
435/// Text quoted and escaped, as much of it as fits the budget. False when cut,
436/// and then with no closing quote, so it does not read as the whole value.
437fn write_quoted(s: &str, out: &mut String, budget: usize) -> bool {
438    out.push('"');
439    let head = prefix(s, budget.saturating_sub(out.len()));
440    escape_into(head, out);
441    if head.len() < s.len() {
442        return false;
443    }
444    out.push('"');
445    true
446}
447
448/// Bytes as quoted base64, as much as fits the budget. False when cut.
449fn write_bytes(bytes: &[u8], out: &mut String, budget: usize) -> bool {
450    // Base64 writes four characters per three bytes; whole groups keep the
451    // part that is shown decodable.
452    let room = budget.saturating_sub(out.len() + 1) / 4 * 3;
453    let head = &bytes[..bytes.len().min(room)];
454    out.push('"');
455    out.push_str(&base64_text(head));
456    if head.len() < bytes.len() {
457        return false;
458    }
459    out.push('"');
460    true
461}
462
463/// A scalar as it reads inside a list or struct: text quoted and escaped,
464/// numbers exact, a null `null`.
465fn json_scalar(value: &AnyValue) -> String {
466    match value {
467        AnyValue::Null => "null".to_string(),
468        AnyValue::Boolean(b) => b.to_string(),
469        AnyValue::String(s) => json_string(s),
470        AnyValue::StringOwned(s) => json_string(s),
471        v if v.is_primitive_numeric() || matches!(v, AnyValue::Decimal(..)) => value_text(v),
472        v => json_string(&value_text(v)),
473    }
474}
475
476/// `s` in double quotes with JSON's escapes, and the invisible characters
477/// [`escaped`] spells out.
478fn json_string(s: &str) -> String {
479    let mut out = String::with_capacity(s.len() + 2);
480    out.push('"');
481    escape_into(s, &mut out);
482    out.push('"');
483    out
484}
485
486/// Whether `c` draws nothing, or nothing a reader can tell from a space, so the
487/// escaped view spells it out: controls, Unicode's default-ignorable characters
488/// (zero-width and direction marks, the soft hyphen, variation selectors, tags,
489/// the byte-order mark) and the spaces other than U+0020.
490fn invisible(c: char) -> bool {
491    c.is_control()
492        || matches!(
493            c,
494            '\u{a0}'
495                | '\u{ad}'
496                | '\u{34f}'
497                | '\u{61c}'
498                | '\u{115f}'..='\u{1160}'
499                | '\u{1680}'
500                | '\u{17b4}'..='\u{17b5}'
501                | '\u{180b}'..='\u{180f}'
502                | '\u{2000}'..='\u{200f}'
503                | '\u{2028}'..='\u{202f}'
504                | '\u{205f}'..='\u{206f}'
505                | '\u{3000}'
506                | '\u{3164}'
507                | '\u{fe00}'..='\u{fe0f}'
508                | '\u{feff}'
509                | '\u{ffa0}'
510                | '\u{fff0}'..='\u{fffb}'
511                | '\u{1bca0}'..='\u{1bca3}'
512                | '\u{1d173}'..='\u{1d17a}'
513                | '\u{e0000}'..='\u{e0fff}'
514        )
515}
516
517/// Whether a one-line cell draws `c` as a mark rather than as itself: a control
518/// character, which a cell cannot draw, or a direction control, which a terminal
519/// that lays out bidirectional text would apply to the rest of the row.
520pub fn marked(c: char) -> bool {
521    c.is_control()
522        || matches!(
523            c,
524            '\u{61c}' | '\u{200e}' | '\u{200f}' | '\u{202a}'..='\u{202e}' | '\u{2066}'..='\u{2069}'
525        )
526}
527
528fn escape_into(s: &str, out: &mut String) {
529    for c in s.chars() {
530        escape_char(c, out);
531    }
532}
533
534/// One character of [`escaped`]'s literal, without the quotes around it: the
535/// inspector escapes a long value a piece at a time.
536pub fn escape_char(c: char, out: &mut String) {
537    match c {
538        '\\' => out.push_str("\\\\"),
539        '"' => out.push_str("\\\""),
540        '\n' => out.push_str("\\n"),
541        '\r' => out.push_str("\\r"),
542        '\t' => out.push_str("\\t"),
543        '\0' => out.push_str("\\0"),
544        c if invisible(c) => {
545            let _ = write!(out, "\\u{{{:x}}}", c as u32);
546        }
547        c => out.push(c),
548    }
549}
550
551/// `s` as a quoted literal: backslash, quote, line breaks, tabs and every
552/// invisible character written as an escape, so a literal `\n` in the data
553/// (`"\\n"`) never reads as a line break, and leading or trailing spaces and
554/// an empty string show by the quotes around them.
555pub fn escaped(s: &str) -> String {
556    json_string(s)
557}
558
559/// Bytes as a quoted literal: printable ASCII as itself, the rest as `\xNN`.
560pub fn escaped_bytes(bytes: &[u8]) -> String {
561    let mut out = String::with_capacity(bytes.len() + 3);
562    out.push_str("b\"");
563    for &b in bytes {
564        match b {
565            b'\\' => out.push_str("\\\\"),
566            b'"' => out.push_str("\\\""),
567            b'\n' => out.push_str("\\n"),
568            b'\r' => out.push_str("\\r"),
569            b'\t' => out.push_str("\\t"),
570            0x20..=0x7e => out.push(b as char),
571            b => {
572                let _ = write!(out, "\\x{b:02x}");
573            }
574        }
575    }
576    out.push('"');
577    out
578}
579
580/// Whether one-line text holds a character [`marked`] in a cell. Bytes first:
581/// C1 controls start with 0xc2, U+061C with 0xd8, the other direction controls
582/// with 0xe2.
583fn has_marked(s: &str) -> bool {
584    s.bytes().any(|b| b < 0x20 || b == 0x7f)
585        || (s.bytes().any(|b| matches!(b, 0xc2 | 0xd8 | 0xe2)) && s.chars().any(marked))
586}
587
588/// Bytes of a nested value a table cell previews: more than any terminal row draws.
589pub const CELL_PREVIEW_BYTES: usize = 4096;
590
591/// The cells a preview drawn in `width` cells (a terminal, or the area it is
592/// drawn in) keeps of a value: past that width and past any width a column can be
593/// set to by hand, so the cut never shows.
594pub fn cell_cut(width: u16) -> usize {
595    usize::from(width.max(crate::widgets::column_widths::MAX_WIDTH))
596}
597
598/// [`cell_text`] of `s`, cut past [`cell_cut`] of `width`.
599pub fn cell_preview(s: &str, g: &crate::glyphs::Glyphs, width: u16) -> String {
600    cell_text(Cow::Borrowed(s), g, cell_cut(width))
601}
602
603/// [`preview`] of the start of `text` that covers more than `cells` cells,
604/// ending in the ellipsis when cut: a cell draws only its start, and measuring
605/// a huge value whole costs what the value costs. Drawn at `cells` or fewer, it
606/// reads and measures as the whole value would. An owned `text` that needs
607/// neither cut nor mark is kept, not copied.
608pub fn cell_text(text: Cow<'_, str>, g: &crate::glyphs::Glyphs, cells: usize) -> String {
609    let head = cell_prefix(&text, cells);
610    if head.len() == text.len() {
611        if !has_marked(&text) {
612            return text.into_owned();
613        }
614        return preview(&text, g).into_owned();
615    }
616    let mut out = preview(head, g).into_owned();
617    out.push_str(g.ellipsis);
618    out
619}
620
621/// The start of `s` that covers more than `cells` cells once [`preview`] marks
622/// it, or all of `s`. Measured by grapheme at ratatui's widths, a control as its
623/// one-cell mark. A value of zero-width graphemes stops at twice `cells`
624/// graphemes, so nothing is walked whole.
625pub fn cell_prefix(s: &str, cells: usize) -> &str {
626    use ratatui::buffer::CellWidth;
627    use unicode_segmentation::UnicodeSegmentation;
628    if s.len() <= cells {
629        return s;
630    }
631    let plain = s.as_bytes()[..=cells]
632        .iter()
633        .take_while(|b| (0x20..0x7f).contains(*b))
634        .count();
635    if plain > cells {
636        return &s[..=cells];
637    }
638    let mut width = 0usize;
639    for (n, (at, grapheme)) in s.grapheme_indices(true).enumerate() {
640        if width > cells || n > 2 * cells {
641            return &s[..at];
642        }
643        width += if grapheme.contains(char::is_control) {
644            1
645        } else {
646            usize::from(grapheme.cell_width())
647        };
648    }
649    s
650}
651
652/// One-line preview of `s`: a line break, a tab or another [`marked`] character
653/// becomes a mark from the glyph set, so `line1\nline2` does not read as
654/// `line1line2`. A `\r\n` pair is one break.
655pub fn preview<'a>(s: &'a str, g: &crate::glyphs::Glyphs) -> Cow<'a, str> {
656    if !has_marked(s) {
657        return Cow::Borrowed(s);
658    }
659    let mut out = String::with_capacity(s.len());
660    let mut chars = s.chars().peekable();
661    while let Some(c) = chars.next() {
662        match c {
663            '\r' if chars.peek() == Some(&'\n') => {
664                chars.next();
665                out.push_str(g.newline_mark);
666            }
667            '\n' => out.push_str(g.newline_mark),
668            '\t' => out.push_str(g.tab_mark),
669            c if marked(c) => out.push_str(g.control_mark),
670            c => out.push(c),
671        }
672    }
673    Cow::Owned(out)
674}
675
676/// The facts about text that its look on screen hides: how long it is, how
677/// many lines it runs to, and whitespace at either end.
678#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
679pub struct TextFacts {
680    pub chars: usize,
681    pub lines: usize,
682    pub leading_spaces: usize,
683    pub trailing_spaces: usize,
684}
685
686pub fn text_facts(s: &str) -> TextFacts {
687    let lines = if s.is_empty() {
688        0
689    } else {
690        s.split('\n').count()
691    };
692    TextFacts {
693        chars: s.chars().count(),
694        lines,
695        leading_spaces: s.chars().take_while(|c| c.is_whitespace()).count(),
696        trailing_spaces: if s.chars().all(char::is_whitespace) {
697            0
698        } else {
699            s.chars().rev().take_while(|c| c.is_whitespace()).count()
700        },
701    }
702}
703
704/// The longest prefix of `s` no longer than `budget` bytes, cut at a
705/// character boundary.
706pub fn prefix(s: &str, budget: usize) -> &str {
707    if s.len() <= budget {
708        return s;
709    }
710    let mut end = budget;
711    while !s.is_char_boundary(end) {
712        end -= 1;
713    }
714    &s[..end]
715}
716
717/// One cell of a one-row column as copy text: exact scalars, nested values as
718/// the JSON an export writes, bytes as base64, a null as empty.
719pub fn copy_text(column: &Column) -> PolarsResult<String> {
720    let value = column.get(0)?;
721    if is_nested_value(&value) {
722        let json = crate::export::nested_json::column_as_json(column)?;
723        return Ok(match json.get(0)? {
724            AnyValue::Null => String::new(),
725            v => v.str_value().into_owned(),
726        });
727    }
728    Ok(value_text(&value))
729}
730
731/// At least how many bytes [`copy_text`] writes for `value`, counted only until
732/// the count passes `stop`: a copy can be refused at a cap before it is
733/// formatted, and a million-item list is not walked to learn that it is over.
734/// Text is its length and bytes their base64; a list or struct is counted from
735/// its punctuation and leaves, each at its shortest JSON.
736pub fn copy_len_floor(value: &AnyValue, stop: usize) -> usize {
737    match value {
738        AnyValue::String(s) => s.len(),
739        AnyValue::StringOwned(s) => s.len(),
740        AnyValue::Binary(b) => crate::clipboard::base64_len(b.len()),
741        AnyValue::BinaryOwned(b) => crate::clipboard::base64_len(b.len()),
742        v if is_nested_value(v) => json_len_floor(v, 0, stop),
743        // Any other scalar is a few bytes: its text is its measure.
744        v => value_text(v).len(),
745    }
746}
747
748/// `so_far` plus a floor on the JSON written for `value`, stopping once past `stop`.
749fn json_len_floor(value: &AnyValue, so_far: usize, stop: usize) -> usize {
750    if so_far > stop {
751        return so_far;
752    }
753    // Brackets, and a comma between items.
754    let punctuation = |len: usize| 2 + len.saturating_sub(1);
755    match value {
756        AnyValue::List(s) | AnyValue::Array(s, _) => {
757            let mut n = so_far + punctuation(s.len());
758            for item in s.iter() {
759                if n > stop {
760                    break;
761                }
762                n = json_len_floor(&item, n, stop);
763            }
764            n
765        }
766        AnyValue::Struct(_, _, fields) => {
767            let mut n = so_far + punctuation(fields.len());
768            for (field, item) in fields.iter().zip(value._iter_struct_av()) {
769                if n > stop {
770                    break;
771                }
772                // `"name":` before the value.
773                n = json_len_floor(&item, n + field.name().len() + 3, stop);
774            }
775            n
776        }
777        AnyValue::StructOwned(payload) => {
778            let mut n = so_far + punctuation(payload.1.len());
779            for (field, item) in payload.1.iter().zip(&payload.0) {
780                if n > stop {
781                    break;
782                }
783                n = json_len_floor(item, n + field.name().len() + 3, stop);
784            }
785            n
786        }
787        AnyValue::String(s) => so_far + s.len() + 2,
788        AnyValue::StringOwned(s) => so_far + s.len() + 2,
789        AnyValue::Binary(b) => so_far + crate::clipboard::base64_len(b.len()) + 2,
790        AnyValue::BinaryOwned(b) => so_far + crate::clipboard::base64_len(b.len()) + 2,
791        // A number, a flag or a null is at least a character.
792        _ => so_far + 1,
793    }
794}
795
796#[cfg(test)]
797mod tests;