Skip to main content

truecalc_workbook/
address.rs

1//! A1 cell addressing: parsing, bounds checking, and the A1↔`(row, column)`
2//! conversion utilities of the runtime grid (plan item 3.1).
3//!
4//! A serialized cell key MUST match `^[A-Z]{1,3}[1-9][0-9]{0,7}$` **and** lie
5//! within the address bounds of the limits ADR (rows `1..=10_000_000`,
6//! columns `1..=18_278`, i.e. `A..=ZZZ`). [`Workbook::from_json`] rejects every
7//! other key — no `$`, no sheet qualifier, no lowercase, no leading zero.
8//!
9//! An [`Address`] is the parsed, bounds-validated form used as the in-memory
10//! grid key (plan item 3.1). It can only be constructed in bounds, so every
11//! address held by a [`Worksheet`] is guaranteed valid; [`Address::to_a1`]
12//! re-emits the exact plain-uppercase key the canonical serializer writes.
13//!
14//! [`Workbook::from_json`]: crate::Workbook::from_json
15//! [`Worksheet`]: crate::Worksheet
16
17use crate::limits::{MAX_COLUMN, MAX_ROW};
18
19/// A parsed, in-bounds A1 address: 1-based `(row, column)`.
20///
21/// The grid key of a [`Worksheet`](crate::Worksheet). Every constructor is
22/// bounds-checked (rows `1..=10_000_000`, columns `1..=18_278`), so an
23/// `Address` value is always serializable to a valid A1 key.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
25pub struct Address {
26    /// 1-based row.
27    pub row: u32,
28    /// 1-based column (`A` = 1).
29    pub column: u32,
30}
31
32impl Address {
33    /// Builds an address from 1-based `(row, column)`, enforcing the address
34    /// bounds (rows `1..=10_000_000`, columns `1..=18_278`). Returns `None`
35    /// when either coordinate is `0` or out of bounds.
36    pub fn new(row: u32, column: u32) -> Option<Self> {
37        if row == 0 || row > MAX_ROW || column == 0 || column > MAX_COLUMN {
38            return None;
39        }
40        Some(Self { row, column })
41    }
42
43    /// Parses a plain uppercase A1 address (e.g. `A1`, `BC42`), enforcing the
44    /// normative key syntax (`^[A-Z]{1,3}[1-9][0-9]{0,7}$`) and the address
45    /// bounds (schema spec §3). Returns `None` on any malformed or
46    /// out-of-bounds key.
47    pub fn from_a1(key: &str) -> Option<Self> {
48        parse_a1(key)
49    }
50
51    /// Re-emits the plain-uppercase A1 key (`A1`, `BC42`) — the inverse of
52    /// [`Address::from_a1`] and the exact key the canonical serializer writes.
53    ///
54    /// # Panics
55    ///
56    /// Panics if `row` or `column` is outside the bounds this type is
57    /// documented to hold (rows `1..=10_000_000`, columns `1..=18_278`).
58    /// Every constructor enforces these bounds, so this is only reachable by
59    /// setting the public `row`/`column` fields directly rather than going
60    /// through [`Address::new`]. The column-axis panic (an out-of-bounds
61    /// `column`) is pre-existing; the row-axis panic is new here — before,
62    /// an out-of-bounds `row` silently produced a key that was well-formed
63    /// but meaningless (and so never matched a real grid entry) instead of
64    /// panicking.
65    pub fn to_a1(&self) -> String {
66        self.a1_key().as_str().to_owned()
67    }
68
69    /// Renders the plain-uppercase A1 key into a stack buffer, allocating
70    /// nothing. The borrowed form of [`to_a1`](Self::to_a1): identical bytes,
71    /// no heap. Read-side grid operations key the cell map through this.
72    ///
73    /// # Panics
74    ///
75    /// Same bounds requirement as [`Address::to_a1`]: an out-of-bounds `row`
76    /// or `column` overflows the fixed-size stack buffer and panics rather
77    /// than producing a garbage key.
78    pub(crate) fn a1_key(&self) -> A1Key {
79        let mut key = A1Key {
80            buf: [0; A1_KEY_CAPACITY],
81            len: 0,
82        };
83
84        // Column: bijective base-26 digits come out least-significant first,
85        // so stage them and copy back in reverse.
86        let mut letters = [0u8; MAX_COLUMN_LETTERS];
87        let mut n = 0;
88        let mut column = self.column;
89        while column > 0 {
90            letters[n] = b'A' + ((column - 1) % 26) as u8;
91            n += 1;
92            column = (column - 1) / 26;
93        }
94        while n > 0 {
95            n -= 1;
96            key.push(letters[n]);
97        }
98
99        // Row: same story for the decimal digits. A row is always >= 1, so this
100        // never emits the empty string.
101        let mut digits = [0u8; MAX_ROW_DIGITS];
102        let mut d = 0;
103        let mut row = self.row;
104        while row > 0 {
105            digits[d] = b'0' + (row % 10) as u8;
106            d += 1;
107            row /= 10;
108        }
109        while d > 0 {
110            d -= 1;
111            key.push(digits[d]);
112        }
113
114        key
115    }
116}
117
118/// The widest in-bounds column (`ZZZ`) is three letters.
119const MAX_COLUMN_LETTERS: usize = 3;
120/// The widest in-bounds row (`10000000`) is eight digits.
121const MAX_ROW_DIGITS: usize = 8;
122/// Every in-bounds A1 key fits in `ZZZ10000000`.
123const A1_KEY_CAPACITY: usize = MAX_COLUMN_LETTERS + MAX_ROW_DIGITS;
124
125/// A plain-uppercase A1 key rendered into a fixed stack buffer.
126///
127/// A [`Worksheet`](crate::Worksheet) keys its grid by `String`, but
128/// `BTreeMap<String, _>` probes through `Borrow<str>` — a lookup only needs a
129/// `&str`, never an owned key. Rendering here instead of into a `String` is
130/// what keeps a range scan from paying a heap allocation per cell it visits.
131pub(crate) struct A1Key {
132    buf: [u8; A1_KEY_CAPACITY],
133    len: usize,
134}
135
136impl A1Key {
137    fn push(&mut self, byte: u8) {
138        self.buf[self.len] = byte;
139        self.len += 1;
140    }
141
142    /// The rendered key. Byte-for-byte what [`Address::to_a1`] returns.
143    pub(crate) fn as_str(&self) -> &str {
144        // Every byte written is an ASCII uppercase letter or digit.
145        std::str::from_utf8(&self.buf[..self.len]).expect("A1 keys are ASCII")
146    }
147}
148
149/// Parses a plain uppercase A1 address, enforcing the normative key syntax
150/// (`^[A-Z]{1,3}[1-9][0-9]{0,7}$`) and the address bounds (schema spec §3).
151///
152/// Returns `None` on any malformed key or out-of-bounds row/column. Hand-rolled
153/// rather than regex-backed to keep the crate dependency-light and to fold the
154/// bounds check into the same pass. Kept as a free function because the
155/// document validator and named-ref parser call it on untrusted keys.
156pub fn parse_a1(key: &str) -> Option<Address> {
157    let bytes = key.as_bytes();
158    let mut i = 0;
159
160    // 1..=3 uppercase ASCII letters.
161    let mut column: u32 = 0;
162    while i < bytes.len() && bytes[i].is_ascii_uppercase() {
163        if i >= 3 {
164            return None; // more than 3 letters
165        }
166        column = column * 26 + (bytes[i] - b'A' + 1) as u32;
167        i += 1;
168    }
169    if i == 0 {
170        return None; // no leading letters
171    }
172
173    // First digit 1..=9 (no leading zero), then up to 7 more digits.
174    let digits = &bytes[i..];
175    if digits.is_empty() || digits.len() > 8 {
176        return None;
177    }
178    if digits[0] == b'0' {
179        return None; // leading zero forbidden by `[1-9]`
180    }
181    let mut row: u32 = 0;
182    for &b in digits {
183        if !b.is_ascii_digit() {
184            return None;
185        }
186        row = row.checked_mul(10)?.checked_add((b - b'0') as u32)?;
187    }
188
189    Address::new(row, column)
190}