const UPPER: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
const LOWER: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
#[must_use]
pub fn decode(field: &str, width: u32) -> Option<i64> {
let trimmed = field.trim();
if trimmed.is_empty() {
return None;
}
if trimmed
.bytes()
.all(|byte| byte.is_ascii_digit() || byte == b'-' || byte == b'+')
{
return trimmed.parse().ok();
}
if trimmed.len() != width as usize {
return None;
}
let decimal_range = 10i64.checked_pow(width)?;
let case_span = case_span(width)?;
let lower = trimmed.bytes().any(|byte| byte.is_ascii_lowercase());
let value = from_base36(trimmed, lower)?;
let start = case_start(width)?;
let offset = value.checked_sub(start)?;
if offset < 0 || offset >= case_span {
return None;
}
if lower {
Some(decimal_range + case_span + offset)
} else {
Some(decimal_range + offset)
}
}
#[must_use]
pub fn encode(value: i64, width: u32) -> Option<String> {
let columns = width as usize;
let decimal_range = 10i64.checked_pow(width)?;
if value < 0 {
let text = value.to_string();
return (text.len() <= columns).then(|| format!("{text:>columns$}"));
}
if value < decimal_range {
return Some(format!("{value:>columns$}"));
}
let case_span = case_span(width)?;
let offset = value - decimal_range;
let lower = offset >= case_span;
let offset = if lower { offset - case_span } else { offset };
if offset >= case_span {
return None;
}
to_base36(case_start(width)? + offset, width, lower)
}
#[must_use]
pub fn needs_encoding(value: i64, width: u32) -> bool {
10i64.checked_pow(width).is_some_and(|range| value >= range)
}
fn case_start(width: u32) -> Option<i64> {
10i64.checked_mul(36i64.checked_pow(width.checked_sub(1)?)?)
}
fn case_span(width: u32) -> Option<i64> {
36i64.checked_pow(width)?.checked_sub(case_start(width)?)
}
fn from_base36(text: &str, lower: bool) -> Option<i64> {
let alphabet = if lower { LOWER } else { UPPER };
let mut value = 0i64;
for byte in text.bytes() {
let digit = alphabet.iter().position(|candidate| *candidate == byte)?;
value = value
.checked_mul(36)?
.checked_add(i64::try_from(digit).ok()?)?;
}
Some(value)
}
fn to_base36(mut value: i64, width: u32, lower: bool) -> Option<String> {
let alphabet = if lower { LOWER } else { UPPER };
let mut digits = vec![b'0'; usize::try_from(width).ok()?];
for slot in digits.iter_mut().rev() {
*slot = *alphabet.get(usize::try_from(value % 36).ok()?)?;
value /= 36;
}
if value != 0 {
return None;
}
String::from_utf8(digits).ok()
}
#[cfg(test)]
#[path = "codec_tests.rs"]
mod tests;