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)`).
109pub 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
126/// The text [`str_value`] gives a value Polars panics formatting: a date past
127/// the calendar, or a list or struct holding one. `None` for any other value,
128/// which Polars formats as usual.
129pub 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
136/// Whether a list, array or struct holds a value [`out_of_range`] names. Only one
137/// whose type holds a date, datetime or time is looked into.
138fn 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
151/// Whether any value of `s` is one [`out_of_range`] names: a flat column by its
152/// least and greatest stored number, a nested one item by item.
153fn 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
174/// [`out_of_range`] for a value of `dtype` stored as the number `stored`.
175pub 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
185/// A date, datetime or time column with each value [`out_of_range`] names as
186/// null, for the Polars operations that overflow or panic on one. `None` when
187/// it holds none, the common case, which costs a min and a max.
188pub 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
210/// Whether `dtype` is, or holds, a date, datetime or time.
211pub 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
220/// A datetime with every digit its unit stores, and the zone's offset when it
221/// has a zone. Polars' own display drops trailing zeros and names the zone by
222/// abbreviation, which several zones share.
223fn 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
240/// Bytes as base64, the spelling every copy and export of a binary uses.
241pub fn base64_text(bytes: &[u8]) -> String {
242    base64::engine::general_purpose::STANDARD.encode(bytes)
243}
244
245/// A value as exact text. A null is empty, as in an export; a list or struct
246/// is compact JSON-like text over exact scalars; bytes are base64.
247pub 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        // ISO 8601 seconds, as a CSV export and every other copy write it.
259        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        // Integers, booleans, strings, dates, times, decimals and
275        // categories: Polars' text for these is already the whole value.
276        v => v.str_value().into_owned(),
277    }
278}
279
280/// Text cut to a byte budget, and whether anything was cut.
281#[derive(Debug, Clone, PartialEq, Eq)]
282pub struct Bounded {
283    pub text: String,
284    pub cut: bool,
285}
286
287/// A list, array or struct laid out one item per line, indented, stopping once
288/// `budget` bytes are written. Scalars inside are exact; text is quoted and
289/// escaped so an item's edges are visible.
290pub 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
296/// A list, array or struct on one line, stopping once `budget` bytes are
297/// written: for a preview, which shows only the start.
298pub 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
304/// Whether `value` is a list, array or struct.
305pub fn is_nested_value(value: &AnyValue) -> bool {
306    matches!(
307        value,
308        AnyValue::List(_) | AnyValue::Array(..) | AnyValue::Struct(..) | AnyValue::StructOwned(_)
309    )
310}
311
312/// How many items a list or array holds, or fields a struct has.
313pub 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
322/// Write `value` into `out`. `indent` is the current depth when laid out one
323/// item per line, `None` for compact. Returns false once the budget ran out.
324fn 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    // Items are taken one at a time: a million-item list stops at the budget
335    // rather than being turned into a million values first.
336    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        // Text and bytes are cut at the budget too: one huge string in a list is
372        // as costly as a huge cell.
373        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
384/// The items of a list, array or struct between `open` and `close`, each named
385/// when a struct's. False once the budget ran out.
386fn 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
430/// Text quoted and escaped, as much of it as fits the budget. False when cut,
431/// and then with no closing quote, so it does not read as the whole value.
432fn 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
443/// Bytes as quoted base64, as much as fits the budget. False when cut.
444fn write_bytes(bytes: &[u8], out: &mut String, budget: usize) -> bool {
445    // Base64 writes four characters per three bytes; whole groups keep the
446    // part that is shown decodable.
447    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
458/// A scalar as it reads inside a list or struct: text quoted and escaped,
459/// numbers exact, a null `null`.
460fn 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
471/// `s` in double quotes with JSON's escapes, and the invisible characters
472/// [`escaped`] spells out.
473fn 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
481/// Whether `c` draws nothing, or nothing a reader can tell from a space, so the
482/// escaped view spells it out: controls, Unicode's default-ignorable characters
483/// (zero-width and direction marks, the soft hyphen, variation selectors, tags,
484/// the byte-order mark) and the spaces other than U+0020.
485fn 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
512/// Whether a one-line cell draws `c` as a mark rather than as itself: a control
513/// character, which a cell cannot draw, or a direction control, which a terminal
514/// that lays out bidirectional text would apply to the rest of the row.
515pub 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
529/// One character of [`escaped`]'s literal, without the quotes around it: the
530/// inspector escapes a long value a piece at a time.
531pub 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
546/// `s` as a quoted literal: backslash, quote, line breaks, tabs and every
547/// invisible character written as an escape, so a literal `\n` in the data
548/// (`"\\n"`) never reads as a line break, and leading or trailing spaces and
549/// an empty string show by the quotes around them.
550pub fn escaped(s: &str) -> String {
551    json_string(s)
552}
553
554/// Bytes as a quoted literal: printable ASCII as itself, the rest as `\xNN`.
555pub 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
575/// Bytes as hex-dump lines, `width` bytes to a line: offset, hex, then the
576/// printable ASCII with a dot for the rest.
577pub 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
603/// Whether one-line text holds a character [`marked`] in a cell. Bytes first:
604/// C1 controls start with 0xc2, U+061C with 0xd8, the other direction controls
605/// with 0xe2.
606fn 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
611/// Bytes of a value a table cell previews: more than any terminal row draws.
612pub const CELL_PREVIEW_BYTES: usize = 4096;
613
614/// [`preview`] of the start of `s`, ending in the ellipsis when cut: a cell
615/// draws only its start, and measuring a huge value whole every frame costs
616/// what the value costs.
617pub 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
626/// One-line preview of `s`: a line break, a tab or another [`marked`] character
627/// becomes a mark from the glyph set, so `line1\nline2` does not read as
628/// `line1line2`. A `\r\n` pair is one break.
629pub 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/// The facts about text that its look on screen hides: how long it is, how
651/// many lines it runs to, and whitespace at either end.
652#[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
678/// The longest prefix of `s` no longer than `budget` bytes, cut at a
679/// character boundary.
680pub 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
691/// One cell of a one-row column as copy text: exact scalars, nested values as
692/// the JSON an export writes, bytes as base64, a null as empty.
693pub 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
705/// At least how many bytes [`copy_text`] writes for `value`, counted only until
706/// the count passes `stop`: a copy can be refused at a cap before it is
707/// formatted, and a million-item list is not walked to learn that it is over.
708/// Text is its length and bytes their base64; a list or struct is counted from
709/// its punctuation and leaves, each at its shortest JSON.
710pub 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        // Any other scalar is a few bytes: its text is its measure.
718        v => value_text(v).len(),
719    }
720}
721
722/// `so_far` plus a floor on the JSON written for `value`, stopping once past `stop`.
723fn json_len_floor(value: &AnyValue, so_far: usize, stop: usize) -> usize {
724    if so_far > stop {
725        return so_far;
726    }
727    // Brackets, and a comma between items.
728    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                // `"name":` before the value.
747                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        // A number, a flag or a null is at least a character.
766        _ => 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    /// Polars' compact display rounds what the table previews; the exact text
817    /// is the stored value, with no grouping and no settings consulted.
818    #[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        // 2024-01-02 03:04:05.000120 UTC
829        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    /// Polars panics formatting these; the stored number stands in.
972    #[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        // Every nanosecond count is a date; the edges keep their digits.
989        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    /// The table's text for a value is Polars' own, except where Polars panics:
997    /// a date, datetime or time past the calendar is its stored number, alone or
998    /// inside a list or struct. Durations never panic and keep Polars' text.
999    #[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        // Every nanosecond count is a date, with or without a zone.
1021        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    /// Only the values past the calendar are taken out; a column with none is
1092    /// left alone.
1093    #[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    /// A direction control would turn the rest of a row around in a terminal
1130    /// that lays out bidirectional text.
1131    #[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    /// One huge string or a million items in a list stop at the budget too.
1149    #[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    /// The floor a capped copy is checked against is never more than what the
1166    /// copy writes, and it stops counting past its stop.
1167    #[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        // Text and bytes are exact.
1200        assert_eq!(copy_len_floor(&AnyValue::String("abcdef"), 0), 6);
1201        assert_eq!(copy_len_floor(&AnyValue::Binary(b"abcd"), 0), 8);
1202        // A million items are past a 1 KB stop from the brackets and commas alone.
1203        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    /// The table's list preview: ten items, then how many there were.
1218    #[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}