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    pub fn to_a1(&self) -> String {
54        let mut s = column_to_letters(self.column);
55        s.push_str(&self.row.to_string());
56        s
57    }
58}
59
60/// Parses a plain uppercase A1 address, enforcing the normative key syntax
61/// (`^[A-Z]{1,3}[1-9][0-9]{0,7}$`) and the address bounds (schema spec §3).
62///
63/// Returns `None` on any malformed key or out-of-bounds row/column. Hand-rolled
64/// rather than regex-backed to keep the crate dependency-light and to fold the
65/// bounds check into the same pass. Kept as a free function because the
66/// document validator and named-ref parser call it on untrusted keys.
67pub fn parse_a1(key: &str) -> Option<Address> {
68    let bytes = key.as_bytes();
69    let mut i = 0;
70
71    // 1..=3 uppercase ASCII letters.
72    let mut column: u32 = 0;
73    while i < bytes.len() && bytes[i].is_ascii_uppercase() {
74        if i >= 3 {
75            return None; // more than 3 letters
76        }
77        column = column * 26 + (bytes[i] - b'A' + 1) as u32;
78        i += 1;
79    }
80    if i == 0 {
81        return None; // no leading letters
82    }
83
84    // First digit 1..=9 (no leading zero), then up to 7 more digits.
85    let digits = &bytes[i..];
86    if digits.is_empty() || digits.len() > 8 {
87        return None;
88    }
89    if digits[0] == b'0' {
90        return None; // leading zero forbidden by `[1-9]`
91    }
92    let mut row: u32 = 0;
93    for &b in digits {
94        if !b.is_ascii_digit() {
95            return None;
96        }
97        row = row.checked_mul(10)?.checked_add((b - b'0') as u32)?;
98    }
99
100    Address::new(row, column)
101}
102
103/// Converts a 1-based column index to its bijective base-26 letters
104/// (`1 → "A"`, `26 → "Z"`, `27 → "AA"`, `18_278 → "ZZZ"`). The input is
105/// assumed in bounds (it always is when called on an [`Address`] column).
106pub(crate) fn column_to_letters(mut column: u32) -> String {
107    let mut buf = [0u8; 3]; // ZZZ is the widest in-bounds column.
108    let mut len = 0;
109    while column > 0 {
110        let rem = ((column - 1) % 26) as u8;
111        buf[len] = b'A' + rem;
112        len += 1;
113        column = (column - 1) / 26;
114    }
115    buf[..len].reverse();
116    // Safe: every byte written is an ASCII uppercase letter.
117    String::from_utf8(buf[..len].to_vec()).expect("ASCII letters are valid UTF-8")
118}