Skip to main content

tree_table/types/
span.rs

1use alloc::format;
2use alloc::string::String;
3use alloc::string::ToString;
4
5use crate::{SelectionKind, Table, TableGrid};
6
7const SPAN_COL_A_VALUE: usize = 'A' as usize;
8pub const NO_END: usize = usize::MAX; // Represents "to the end of table"
9
10/// Gets an index based on a column letter e.g. A = 0.
11/// Now case-insensitive (accepts both "B" and "b").
12pub fn span_a2i(a: &str) -> usize {
13    a.chars().fold(0, |acc, c| {
14        let cu = c.to_ascii_uppercase();
15        acc * 26 + (cu as usize - SPAN_COL_A_VALUE + 1)
16    }) - 1
17}
18
19/// Converts a 0-based column index back to Excel-style letter(s).
20/// 0 → "A", 25 → "Z", 26 → "AA", etc.
21pub fn col_index_to_letter(mut col: usize) -> String {
22    let mut s = String::new();
23    col += 1; // treat as 1-based for lettering
24    while col > 0 {
25        let rem = (col - 1) % 26;
26        s.push((b'A' + rem as u8) as char);
27        col = (col - 1) / 26;
28    }
29    s.chars().rev().collect()
30}
31
32#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
33#[cfg_attr(feature = "tsify", tsify(from_wasm_abi))]
34#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
35#[derive(Debug, Clone, PartialEq)]
36pub enum SpanKey {
37    Span(Span),
38    None,
39    Un64(usize),
40    Tuple2(Option<usize>, Option<usize>),
41    Tuple4(Option<usize>, Option<usize>, Option<usize>, Option<usize>),
42    TuplePair(
43        (Option<usize>, Option<usize>),
44        (Option<usize>, Option<usize>),
45    ),
46    Str(String),
47}
48
49#[cfg_attr(feature = "tsify", derive(tsify::Tsify))]
50#[cfg_attr(feature = "tsify", tsify(from_wasm_abi))]
51#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
52#[derive(Debug, Copy, Clone, PartialEq)]
53pub struct Span {
54    pub row: usize,
55    pub end_row: usize, // NO_END = to end of table
56    pub col: usize,
57    pub end_col: usize, // NO_END = to end of table
58}
59
60impl Span {
61    pub fn new(row: usize, end_row: usize, col: usize, end_col: usize) -> Self {
62        Span {
63            row,
64            end_row,
65            col,
66            end_col,
67        }
68    }
69
70    /// Returns the `SelectionKind`:
71    /// - `end_row == NO_END` but `end_col != NO_END` → `Rows`
72    /// - `end_col == NO_END` but `end_row != NO_END` → `Cols`
73    /// - anything else → `Cells`
74    pub fn kind(&self) -> SelectionKind {
75        match (self.end_row == NO_END, self.end_col == NO_END) {
76            (true, false) => SelectionKind::Rows,
77            (false, true) => SelectionKind::Cols,
78            _ => SelectionKind::Cells,
79        }
80    }
81
82    /// Iterate over a spans rows
83    pub fn rows(&self, grid: &TableGrid) -> core::ops::Range<usize> {
84        let start = self.row;
85        if self.end_row != NO_END {
86            start..self.end_row + 1
87        } else {
88            start..Table::acr_total_rows(&grid.index.cells)
89        }
90    }
91
92    /// Inclusive start index and end index of the span
93    pub fn rows_start_end(&self, grid: &TableGrid) -> (usize, usize) {
94        let start = self.row;
95        if self.end_row != NO_END {
96            (start, self.end_row)
97        } else {
98            (
99                start,
100                Table::acr_total_rows(&grid.index.cells).saturating_sub(1),
101            )
102        }
103    }
104
105    /// Iterate over a spans cols
106    pub fn cols(&self, grid: &TableGrid) -> core::ops::Range<usize> {
107        let start = self.col;
108        if self.end_col != NO_END {
109            start..self.end_col + 1
110        } else {
111            start..Table::acr_total_cols(&grid.header.cells)
112        }
113    }
114
115    /// Inclusive start index and end index of the span
116    pub fn cols_start_end(&self, grid: &TableGrid) -> (usize, usize) {
117        let start = self.col;
118        if self.end_col != NO_END {
119            (start, self.end_col)
120        } else {
121            (
122                start,
123                Table::acr_total_cols(&grid.header.cells).saturating_sub(1),
124            )
125        }
126    }
127
128    /// The max. row index of the span
129    pub fn max_row(&self, grid: &TableGrid) -> usize {
130        if self.end_row != NO_END {
131            self.end_row
132        } else {
133            Table::acr_total_rows(&grid.index.cells).saturating_sub(1)
134        }
135    }
136
137    /// The max. col index of the span
138    pub fn max_col(&self, grid: &TableGrid) -> usize {
139        if self.end_col != NO_END {
140            self.end_col
141        } else {
142            Table::acr_total_cols(&grid.header.cells).saturating_sub(1)
143        }
144    }
145
146    /// Converts this `Span` back into a string in exactly the formats accepted by
147    /// `parse_string_to_span` (and therefore by `cr_key_to_span` when given a `SpanKey::Str`).
148    pub fn to_string(&self) -> String {
149        if self.row == 0 && self.end_row == NO_END && self.col == 0 && self.end_col == NO_END {
150            return ":".to_string();
151        }
152
153        let col_s = col_index_to_letter(self.col);
154        let col_e = if self.end_col != NO_END {
155            col_index_to_letter(self.end_col)
156        } else {
157            String::new()
158        };
159        let row_s = (self.row + 1).to_string();
160        let row_e = if self.end_row != NO_END {
161            (self.end_row + 1).to_string()
162        } else {
163            String::new()
164        };
165
166        // Only columns (full height)
167        if self.row == 0 && self.end_row == NO_END {
168            if self.col == self.end_col && self.end_col != NO_END {
169                col_s
170            } else if self.end_col == NO_END {
171                format!("{}:", col_s)
172            } else if self.col == 0 {
173                format!(":{}", col_e)
174            } else {
175                format!("{}:{}", col_s, col_e)
176            }
177        }
178        // Only rows (full width)
179        else if self.col == 0 && self.end_col == NO_END {
180            if self.row == self.end_row && self.end_row != NO_END {
181                row_s
182            } else if self.end_row == NO_END {
183                format!("{}:", row_s)
184            } else if self.row == 0 {
185                format!(":{}", row_e)
186            } else {
187                format!("{}:{}", row_s, row_e)
188            }
189        }
190        // Cell / mixed selection
191        else {
192            let start = format!("{}{}", col_s, row_s);
193            if self.row == self.end_row && self.col == self.end_col && self.end_col != NO_END {
194                return start; // single cell
195            }
196
197            let end = if self.end_row == NO_END && self.end_col == NO_END {
198                String::new()
199            } else if self.end_row == NO_END {
200                col_e
201            } else if self.end_col == NO_END {
202                row_e
203            } else {
204                format!("{}{}", col_e, row_e)
205            };
206
207            if end.is_empty() {
208                format!("{}:", start)
209            } else {
210                format!("{}:{}", start, end)
211            }
212        }
213    }
214}
215
216impl core::fmt::Display for Span {
217    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218        write!(f, "{}", self.to_string())
219    }
220}
221
222impl Default for SpanKey {
223    fn default() -> Self {
224        SpanKey::None
225    }
226}
227
228impl From<usize> for SpanKey {
229    fn from(row: usize) -> Self {
230        SpanKey::Un64(row)
231    }
232}
233
234impl From<(Option<usize>, Option<usize>)> for SpanKey {
235    fn from((r, c): (Option<usize>, Option<usize>)) -> Self {
236        SpanKey::Tuple2(r, c)
237    }
238}
239
240impl From<(usize, usize)> for SpanKey {
241    fn from((r, c): (usize, usize)) -> Self {
242        SpanKey::Tuple2(Some(r), Some(c))
243    }
244}
245
246impl From<(Option<usize>, Option<usize>, Option<usize>, Option<usize>)> for SpanKey {
247    fn from(
248        (fr, ur, fc, uc): (Option<usize>, Option<usize>, Option<usize>, Option<usize>),
249    ) -> Self {
250        SpanKey::Tuple4(fr, ur, fc, uc)
251    }
252}
253
254impl
255    From<(
256        (Option<usize>, Option<usize>),
257        (Option<usize>, Option<usize>),
258    )> for SpanKey
259{
260    fn from(
261        ((fr, ur), (fc, uc)): (
262            (Option<usize>, Option<usize>),
263            (Option<usize>, Option<usize>),
264        ),
265    ) -> Self {
266        SpanKey::TuplePair((fr, ur), (fc, uc))
267    }
268}
269
270impl From<&str> for SpanKey {
271    fn from(s: &str) -> Self {
272        SpanKey::Str(s.to_string())
273    }
274}
275
276impl From<String> for SpanKey {
277    fn from(s: String) -> Self {
278        SpanKey::Str(s)
279    }
280}
281
282// ============================================================================
283// NEW: Allocation-free, single-pass parser (replaces all regex + OnceLock)
284// ============================================================================
285
286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
287enum Part {
288    Empty,
289    Row(usize),
290    Col(usize),
291    Cell { col: usize, row: usize },
292}
293
294/// Single linear pass classification (no double iteration, no allocation).
295fn classify_part(p: &str) -> Option<Part> {
296    if p.is_empty() {
297        return Some(Part::Empty);
298    }
299
300    let bytes = p.as_bytes();
301    let first = bytes[0];
302
303    if first.is_ascii_digit() {
304        for &b in bytes {
305            if !b.is_ascii_digit() {
306                return None;
307            }
308        }
309        if let Ok(n) = p.parse::<usize>() {
310            return Some(Part::Row(n.saturating_sub(1)));
311        }
312        return None;
313    }
314
315    if first.is_ascii_alphabetic() {
316        let mut letter_end = 0usize;
317        for (i, &b) in bytes.iter().enumerate() {
318            if b.is_ascii_alphabetic() {
319                letter_end = i + 1;
320            } else if b.is_ascii_digit() {
321                break;
322            } else {
323                return None;
324            }
325        }
326
327        if letter_end == 0 {
328            return None;
329        }
330
331        let letters = &p[..letter_end];
332        let rest = &p[letter_end..];
333
334        if rest.is_empty() {
335            return Some(Part::Col(span_a2i(letters)));
336        }
337
338        for &b in rest.as_bytes() {
339            if !b.is_ascii_digit() {
340                return None;
341            }
342        }
343
344        if let Ok(rn) = rest.parse::<usize>() {
345            return Some(Part::Cell {
346                col: span_a2i(letters),
347                row: rn.saturating_sub(1),
348            });
349        }
350        return None;
351    }
352
353    None
354}
355
356impl Table {
357    pub fn cr_key_to_span(&self, key: SpanKey) -> Result<Span, String> {
358        match key {
359            SpanKey::Span(coords) => Ok(coords),
360            SpanKey::None => Ok(Span {
361                row: 0,
362                end_row: NO_END,
363                col: 0,
364                end_col: NO_END,
365            }),
366            SpanKey::Un64(row) => Ok(Span {
367                row,
368                end_row: row,
369                col: 0,
370                end_col: NO_END,
371            }),
372            SpanKey::Tuple2(r, c) => Ok(Span {
373                row: r.unwrap_or(0),
374                end_row: r.unwrap_or(NO_END),
375                col: c.unwrap_or(0),
376                end_col: c.unwrap_or(NO_END),
377            }),
378            SpanKey::Tuple4(fr, ur, fc, uc) => Ok(Span {
379                row: fr.unwrap_or(0),
380                end_row: ur.unwrap_or(NO_END),
381                col: fc.unwrap_or(0),
382                end_col: uc.unwrap_or(NO_END),
383            }),
384            SpanKey::TuplePair((fr, ur), (fc, uc)) => Ok(Span {
385                row: fr.unwrap_or(0),
386                end_row: ur.unwrap_or(NO_END),
387                col: fc.unwrap_or(0),
388                end_col: uc.unwrap_or(NO_END),
389            }),
390            SpanKey::Str(s) => self.parse_string_to_span(&s),
391        }
392    }
393
394    #[inline]
395    fn parse_string_to_span(&self, s: &str) -> Result<Span, String> {
396        if s.is_empty() {
397            return Ok(Span {
398                row: 0,
399                end_row: NO_END,
400                col: 0,
401                end_col: NO_END,
402            });
403        }
404
405        if s == ":" {
406            return Ok(Span {
407                row: 0,
408                end_row: NO_END,
409                col: 0,
410                end_col: NO_END,
411            });
412        }
413
414        if let Some((left_str, right_str)) = s.split_once(':') {
415            let left = classify_part(left_str)
416                .ok_or_else(|| format!("'{}' could not be converted to span.", s))?;
417            let right = classify_part(right_str)
418                .ok_or_else(|| format!("'{}' could not be converted to span.", s))?;
419
420            match (left, right) {
421                (Part::Row(fr), Part::Row(ur)) => Ok(Span {
422                    row: fr,
423                    end_row: ur,
424                    col: 0,
425                    end_col: NO_END,
426                }),
427                (Part::Col(fc), Part::Col(uc)) => Ok(Span {
428                    row: 0,
429                    end_row: NO_END,
430                    col: fc,
431                    end_col: uc,
432                }),
433                (Part::Cell { col: fc, row: fr }, Part::Cell { col: uc, row: ur }) => Ok(Span {
434                    row: fr,
435                    end_row: ur,
436                    col: fc,
437                    end_col: uc,
438                }),
439                (Part::Row(fr), Part::Empty) => Ok(Span {
440                    row: fr,
441                    end_row: NO_END,
442                    col: 0,
443                    end_col: NO_END,
444                }),
445                (Part::Empty, Part::Row(ur)) => Ok(Span {
446                    row: 0,
447                    end_row: ur,
448                    col: 0,
449                    end_col: NO_END,
450                }),
451                (Part::Col(fc), Part::Empty) => Ok(Span {
452                    row: 0,
453                    end_row: NO_END,
454                    col: fc,
455                    end_col: NO_END,
456                }),
457                (Part::Empty, Part::Col(uc)) => Ok(Span {
458                    row: 0,
459                    end_row: NO_END,
460                    col: 0,
461                    end_col: uc,
462                }),
463                (Part::Cell { col: fc, row: fr }, Part::Empty) => Ok(Span {
464                    row: fr,
465                    end_row: NO_END,
466                    col: fc,
467                    end_col: NO_END,
468                }),
469                (Part::Empty, Part::Cell { col: uc, row: ur }) => Ok(Span {
470                    row: 0,
471                    end_row: ur,
472                    col: 0,
473                    end_col: uc,
474                }),
475                (Part::Cell { col: fc, row: fr }, Part::Col(uc)) => Ok(Span {
476                    row: fr,
477                    end_row: NO_END,
478                    col: fc,
479                    end_col: uc,
480                }),
481                (Part::Cell { col: fc, row: fr }, Part::Row(ur)) => Ok(Span {
482                    row: fr,
483                    end_row: ur,
484                    col: fc,
485                    end_col: NO_END,
486                }),
487                _ => Err(format!("'{}' could not be converted to span.", s)),
488            }
489        } else {
490            match classify_part(s) {
491                Some(Part::Row(r)) => Ok(Span {
492                    row: r,
493                    end_row: r,
494                    col: 0,
495                    end_col: NO_END,
496                }),
497                Some(Part::Col(c)) => Ok(Span {
498                    row: 0,
499                    end_row: NO_END,
500                    col: c,
501                    end_col: c,
502                }),
503                Some(Part::Cell { col: c, row: r }) => Ok(Span {
504                    row: r,
505                    end_row: r,
506                    col: c,
507                    end_col: c,
508                }),
509                _ => Err(format!("'{}' could not be converted to span.", s)),
510            }
511        }
512    }
513
514    /// Convenience wrapper: `table.cr_span_to_string(&my_span)` → same as `my_span.to_string()`.
515    pub fn cr_span_to_string(&self, span: &Span) -> String {
516        span.to_string()
517    }
518}