#![no_std]
#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/spreadsheet-column/0.1.0")]
extern crate alloc;
use alloc::string::String;
use core::fmt;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Error {
Negative,
ZeroNotAllowed,
InvalidString,
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Error::Negative => "negative numeric coordinate is not allowed",
Error::ZeroNotAllowed => "numeric coordinate cannot be zero when starting from one",
Error::InvalidString => "column must be a non-empty string of ASCII letters",
};
f.write_str(message)
}
}
impl core::error::Error for Error {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct SpreadsheetColumn {
zero: bool,
}
impl SpreadsheetColumn {
#[must_use]
pub fn new() -> Self {
Self { zero: false }
}
#[must_use]
pub fn zero_based() -> Self {
Self { zero: true }
}
#[allow(clippy::cast_sign_loss)] pub fn from_int(&self, n: i64) -> Result<String, Error> {
if n < 0 {
return Err(Error::Negative);
}
if !self.zero && n == 0 {
return Err(Error::ZeroNotAllowed);
}
let mut value = if self.zero { n + 1 } else { n };
let mut letters = String::new();
while value > 0 {
let remainder = (value - 1) % 26;
letters.insert(0, char::from(b'A' + remainder as u8));
value = (value - 1) / 26;
}
Ok(letters)
}
pub fn from_str(&self, column: &str) -> Result<i64, Error> {
if column.is_empty() || !column.bytes().all(|b| b.is_ascii_alphabetic()) {
return Err(Error::InvalidString);
}
let mut value: i64 = 0;
let mut place: i64 = 1;
for byte in column.bytes().rev() {
let index = i64::from(byte.to_ascii_lowercase() - b'a' + 1);
value = value.wrapping_add(index.wrapping_mul(place));
place = place.wrapping_mul(26);
}
Ok(if self.zero { value - 1 } else { value })
}
}
pub fn from_int(n: i64) -> Result<String, Error> {
SpreadsheetColumn::new().from_int(n)
}
pub fn from_str(column: &str) -> Result<i64, Error> {
SpreadsheetColumn::new().from_str(column)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_based_int_to_letters() {
assert_eq!(from_int(1).unwrap(), "A");
assert_eq!(from_int(26).unwrap(), "Z");
assert_eq!(from_int(27).unwrap(), "AA");
assert_eq!(from_int(52).unwrap(), "AZ");
assert_eq!(from_int(53).unwrap(), "BA");
assert_eq!(from_int(702).unwrap(), "ZZ");
assert_eq!(from_int(703).unwrap(), "AAA");
assert_eq!(from_int(16384).unwrap(), "XFD"); }
#[test]
fn one_based_letters_to_int() {
assert_eq!(from_str("A").unwrap(), 1);
assert_eq!(from_str("Z").unwrap(), 26);
assert_eq!(from_str("AA").unwrap(), 27);
assert_eq!(from_str("ZZ").unwrap(), 702);
assert_eq!(from_str("XFD").unwrap(), 16384);
assert_eq!(from_str("aa").unwrap(), 27); }
#[test]
fn round_trip() {
for n in 1..=5000 {
let letters = from_int(n).unwrap();
assert_eq!(from_str(&letters).unwrap(), n, "round-trip {n}");
}
}
#[test]
fn zero_based() {
let sc = SpreadsheetColumn::zero_based();
assert_eq!(sc.from_int(0).unwrap(), "A");
assert_eq!(sc.from_int(1).unwrap(), "B");
assert_eq!(sc.from_int(25).unwrap(), "Z");
assert_eq!(sc.from_int(26).unwrap(), "AA");
assert_eq!(sc.from_str("A").unwrap(), 0);
assert_eq!(sc.from_str("AA").unwrap(), 26);
}
#[test]
fn errors() {
assert_eq!(from_int(0), Err(Error::ZeroNotAllowed));
assert_eq!(from_int(-1), Err(Error::Negative));
assert_eq!(from_str(""), Err(Error::InvalidString));
assert_eq!(from_str("a1"), Err(Error::InvalidString));
assert_eq!(SpreadsheetColumn::zero_based().from_int(0).unwrap(), "A");
}
}