spreadsheet_column/
lib.rs1#![no_std]
29#![forbid(unsafe_code)]
30#![doc(html_root_url = "https://docs.rs/spreadsheet-column/0.1.0")]
31
32extern crate alloc;
33
34use alloc::string::String;
35use core::fmt;
36
37#[cfg(doctest)]
39#[doc = include_str!("../README.md")]
40struct ReadmeDoctests;
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44pub enum Error {
45 Negative,
47 ZeroNotAllowed,
49 InvalidString,
51}
52
53impl fmt::Display for Error {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 let message = match self {
56 Error::Negative => "negative numeric coordinate is not allowed",
57 Error::ZeroNotAllowed => "numeric coordinate cannot be zero when starting from one",
58 Error::InvalidString => "column must be a non-empty string of ASCII letters",
59 };
60 f.write_str(message)
61 }
62}
63
64impl core::error::Error for Error {}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
68pub struct SpreadsheetColumn {
69 zero: bool,
70}
71
72impl SpreadsheetColumn {
73 #[must_use]
75 pub fn new() -> Self {
76 Self { zero: false }
77 }
78
79 #[must_use]
81 pub fn zero_based() -> Self {
82 Self { zero: true }
83 }
84
85 #[allow(clippy::cast_sign_loss)] pub fn from_int(&self, n: i64) -> Result<String, Error> {
92 if n < 0 {
93 return Err(Error::Negative);
94 }
95 if !self.zero && n == 0 {
96 return Err(Error::ZeroNotAllowed);
97 }
98
99 let mut value = if self.zero { n + 1 } else { n };
101 let mut letters = String::new();
102 while value > 0 {
103 let remainder = (value - 1) % 26;
104 letters.insert(0, char::from(b'A' + remainder as u8));
105 value = (value - 1) / 26;
106 }
107 Ok(letters)
108 }
109
110 pub fn from_str(&self, column: &str) -> Result<i64, Error> {
115 if column.is_empty() || !column.bytes().all(|b| b.is_ascii_alphabetic()) {
116 return Err(Error::InvalidString);
117 }
118
119 let mut value: i64 = 0;
120 let mut place: i64 = 1;
121 for byte in column.bytes().rev() {
122 let index = i64::from(byte.to_ascii_lowercase() - b'a' + 1);
123 value = value.wrapping_add(index.wrapping_mul(place));
124 place = place.wrapping_mul(26);
125 }
126 Ok(if self.zero { value - 1 } else { value })
127 }
128}
129
130pub fn from_int(n: i64) -> Result<String, Error> {
140 SpreadsheetColumn::new().from_int(n)
141}
142
143pub fn from_str(column: &str) -> Result<i64, Error> {
153 SpreadsheetColumn::new().from_str(column)
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn one_based_int_to_letters() {
162 assert_eq!(from_int(1).unwrap(), "A");
163 assert_eq!(from_int(26).unwrap(), "Z");
164 assert_eq!(from_int(27).unwrap(), "AA");
165 assert_eq!(from_int(52).unwrap(), "AZ");
166 assert_eq!(from_int(53).unwrap(), "BA");
167 assert_eq!(from_int(702).unwrap(), "ZZ");
168 assert_eq!(from_int(703).unwrap(), "AAA");
169 assert_eq!(from_int(16384).unwrap(), "XFD"); }
171
172 #[test]
173 fn one_based_letters_to_int() {
174 assert_eq!(from_str("A").unwrap(), 1);
175 assert_eq!(from_str("Z").unwrap(), 26);
176 assert_eq!(from_str("AA").unwrap(), 27);
177 assert_eq!(from_str("ZZ").unwrap(), 702);
178 assert_eq!(from_str("XFD").unwrap(), 16384);
179 assert_eq!(from_str("aa").unwrap(), 27); }
181
182 #[test]
183 fn round_trip() {
184 for n in 1..=5000 {
185 let letters = from_int(n).unwrap();
186 assert_eq!(from_str(&letters).unwrap(), n, "round-trip {n}");
187 }
188 }
189
190 #[test]
191 fn zero_based() {
192 let sc = SpreadsheetColumn::zero_based();
193 assert_eq!(sc.from_int(0).unwrap(), "A");
194 assert_eq!(sc.from_int(1).unwrap(), "B");
195 assert_eq!(sc.from_int(25).unwrap(), "Z");
196 assert_eq!(sc.from_int(26).unwrap(), "AA");
197 assert_eq!(sc.from_str("A").unwrap(), 0);
198 assert_eq!(sc.from_str("AA").unwrap(), 26);
199 }
200
201 #[test]
202 fn errors() {
203 assert_eq!(from_int(0), Err(Error::ZeroNotAllowed));
204 assert_eq!(from_int(-1), Err(Error::Negative));
205 assert_eq!(from_str(""), Err(Error::InvalidString));
206 assert_eq!(from_str("a1"), Err(Error::InvalidString));
207 assert_eq!(SpreadsheetColumn::zero_based().from_int(0).unwrap(), "A");
209 }
210}