1pub const ALPHABET: &[u8; 36] = b"0123456789abcdefghijklmnopqrstuvwxyz";
22
23pub const BASE: u64 = 36;
25
26pub const CAPACITY_4: u64 = 36_u64.pow(4); pub const CAPACITY_5: u64 = 36_u64.pow(5); pub const CAPACITY_6: u64 = 36_u64.pow(6); pub fn encode(value: u64) -> String {
52 if value == 0 {
53 return "0".to_string();
54 }
55
56 let mut result = Vec::new();
57 let mut n = value;
58
59 while n > 0 {
60 let remainder = (n % BASE) as usize;
61 result.push(ALPHABET[remainder] as char);
62 n /= BASE;
63 }
64
65 result.into_iter().rev().collect()
66}
67
68pub fn encode_padded(value: u64, min_length: usize) -> String {
90 let encoded = encode(value);
91 if encoded.len() >= min_length {
92 encoded
93 } else {
94 let padding = "0".repeat(min_length - encoded.len());
95 format!("{}{}", padding, encoded)
96 }
97}
98
99pub fn decode(s: &str) -> Result<u64, DecodeError> {
126 if s.is_empty() {
127 return Err(DecodeError::EmptyString);
128 }
129
130 let mut result: u64 = 0;
131
132 for c in s.chars() {
133 let digit = match c {
134 '0'..='9' => (c as u64) - ('0' as u64),
135 'a'..='z' => (c as u64) - ('a' as u64) + 10,
136 'A'..='Z' => (c as u64) - ('A' as u64) + 10, _ => return Err(DecodeError::InvalidCharacter(c)),
138 };
139
140 result = result
141 .checked_mul(BASE)
142 .ok_or(DecodeError::Overflow)?
143 .checked_add(digit)
144 .ok_or(DecodeError::Overflow)?;
145 }
146
147 Ok(result)
148}
149
150pub fn required_length(global_id: u64) -> usize {
172 if global_id < CAPACITY_4 {
173 4
174 } else if global_id < CAPACITY_5 {
175 5
176 } else {
177 6
178 }
179}
180
181pub fn is_valid_sid(s: &str) -> bool {
191 let len = s.len();
192 if !(4..=6).contains(&len) {
193 return false;
194 }
195
196 s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
197}
198
199#[derive(Debug, Clone, PartialEq, Eq)]
201pub enum DecodeError {
202 EmptyString,
204 InvalidCharacter(char),
206 Overflow,
208}
209
210impl std::fmt::Display for DecodeError {
211 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
212 match self {
213 DecodeError::EmptyString => write!(f, "empty string"),
214 DecodeError::InvalidCharacter(c) => write!(f, "invalid character: '{}'", c),
215 DecodeError::Overflow => write!(f, "numeric overflow"),
216 }
217 }
218}
219
220impl std::error::Error for DecodeError {}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn test_encode_basic() {
228 assert_eq!(encode(0), "0");
229 assert_eq!(encode(1), "1");
230 assert_eq!(encode(9), "9");
231 assert_eq!(encode(10), "a");
232 assert_eq!(encode(35), "z");
233 assert_eq!(encode(36), "10");
234 assert_eq!(encode(37), "11");
235 }
236
237 #[test]
238 fn test_encode_larger_values() {
239 assert_eq!(encode(1295), "zz");
240 assert_eq!(encode(1296), "100");
241 assert_eq!(encode(46655), "zzz");
242 assert_eq!(encode(46656), "1000");
243 }
244
245 #[test]
246 fn test_encode_padded() {
247 assert_eq!(encode_padded(0, 4), "0000");
248 assert_eq!(encode_padded(1, 4), "0001");
249 assert_eq!(encode_padded(12, 4), "000c");
250 assert_eq!(encode_padded(35, 4), "000z");
251 assert_eq!(encode_padded(36, 4), "0010");
252 assert_eq!(encode_padded(1295, 4), "00zz");
253 assert_eq!(encode_padded(46656, 4), "1000");
254 assert_eq!(encode_padded(1679615, 4), "zzzz");
255 assert_eq!(encode_padded(1679616, 5), "10000");
256 }
257
258 #[test]
259 fn test_decode_basic() {
260 assert_eq!(decode("0").unwrap(), 0);
261 assert_eq!(decode("1").unwrap(), 1);
262 assert_eq!(decode("9").unwrap(), 9);
263 assert_eq!(decode("a").unwrap(), 10);
264 assert_eq!(decode("z").unwrap(), 35);
265 assert_eq!(decode("10").unwrap(), 36);
266 }
267
268 #[test]
269 fn test_decode_with_padding() {
270 assert_eq!(decode("0000").unwrap(), 0);
271 assert_eq!(decode("0001").unwrap(), 1);
272 assert_eq!(decode("000c").unwrap(), 12);
273 assert_eq!(decode("00zz").unwrap(), 1295);
274 }
275
276 #[test]
277 fn test_decode_accepts_uppercase() {
278 assert_eq!(decode("A").unwrap(), 10);
279 assert_eq!(decode("Z").unwrap(), 35);
280 assert_eq!(decode("ZZZZ").unwrap(), 1679615);
281 }
282
283 #[test]
284 fn test_decode_errors() {
285 assert!(matches!(decode(""), Err(DecodeError::EmptyString)));
286 assert!(matches!(decode("!"), Err(DecodeError::InvalidCharacter('!'))));
287 assert!(matches!(decode("a-b"), Err(DecodeError::InvalidCharacter('-'))));
288 }
289
290 #[test]
291 fn test_roundtrip() {
292 for value in [0, 1, 35, 36, 1295, 1296, 46655, 46656, 1679615, 1679616] {
293 let encoded = encode(value);
294 let decoded = decode(&encoded).unwrap();
295 assert_eq!(decoded, value, "roundtrip failed for {}", value);
296 }
297 }
298
299 #[test]
300 fn test_required_length() {
301 assert_eq!(required_length(0), 4);
303 assert_eq!(required_length(1_679_615), 4);
304
305 assert_eq!(required_length(1_679_616), 5);
307 assert_eq!(required_length(60_466_175), 5);
308
309 assert_eq!(required_length(60_466_176), 6);
311 assert_eq!(required_length(2_176_782_335), 6);
312 }
313
314 #[test]
315 fn test_is_valid_sid() {
316 assert!(is_valid_sid("0000"));
318 assert!(is_valid_sid("abcd"));
319 assert!(is_valid_sid("z9a0"));
320 assert!(is_valid_sid("12345"));
321 assert!(is_valid_sid("abcdef"));
322
323 assert!(!is_valid_sid("abc"));
325 assert!(!is_valid_sid("abcdefg"));
326
327 assert!(!is_valid_sid("ABCD"));
329
330 assert!(!is_valid_sid("ab-d"));
332 assert!(!is_valid_sid("ab_d"));
333 }
334
335 #[test]
336 fn test_capacity_constants() {
337 assert_eq!(CAPACITY_4, 1_679_616);
338 assert_eq!(CAPACITY_5, 60_466_176);
339 assert_eq!(CAPACITY_6, 2_176_782_336);
340 }
341}