Skip to main content

serializer/
base62.rs

1//! Base62 encoding/decoding for DX ∞
2//!
3//! Encodes integers using 0-9a-zA-Z (62 characters) for maximum density.
4//! Examples: 320→5A, 540→8k, 10000→2Bi
5
6use crate::error::{DxError, Result};
7
8// Standard Base62: 0-9 (0-9), A-Z (10-35), a-z (36-61)
9const BASE62_CHARS: &[u8] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
10
11/// Encode an integer to Base62
12#[must_use]
13pub fn encode_base62(mut n: u64) -> String {
14    if n == 0 {
15        return "0".to_string();
16    }
17
18    let mut result = Vec::new();
19    while n > 0 {
20        result.push(BASE62_CHARS[(n % 62) as usize] as char);
21        n /= 62;
22    }
23    result.reverse();
24    result.into_iter().collect()
25}
26
27/// Decode a Base62 string to integer
28#[must_use = "decoding result should be used"]
29pub fn decode_base62(s: &str) -> Result<u64> {
30    let mut result = 0u64;
31
32    for (position, c) in s.chars().enumerate() {
33        let digit = match c {
34            '0'..='9' => (c as u8 - b'0') as u64,
35            'A'..='Z' => (c as u8 - b'A' + 10) as u64,
36            'a'..='z' => (c as u8 - b'a' + 36) as u64,
37            _ => {
38                return Err(DxError::Base62Error {
39                    char: c,
40                    position,
41                    message: "expected 0-9, A-Z, or a-z".to_string(),
42                });
43            }
44        };
45
46        result = result
47            .checked_mul(62)
48            .and_then(|r| r.checked_add(digit))
49            .ok_or(DxError::IntegerOverflow)?;
50    }
51
52    Ok(result)
53}
54
55#[cfg(test)]
56mod tests {
57    use super::*;
58
59    #[test]
60    fn test_base62_encode() {
61        assert_eq!(encode_base62(0), "0");
62        assert_eq!(encode_base62(35), "Z"); // Last uppercase letter
63        assert_eq!(encode_base62(61), "z"); // Last lowercase letter
64        assert_eq!(encode_base62(62), "10");
65        assert_eq!(encode_base62(320), "5A"); // 320 = 5*62 + 10
66        assert_eq!(encode_base62(540), "8i"); // 540 = 8*62 + 44
67        assert_eq!(encode_base62(180), "2u"); // 180 = 2*62 + 56
68        assert_eq!(encode_base62(10000), "2bI");
69    }
70
71    #[test]
72    fn test_base62_decode() {
73        assert_eq!(decode_base62("0").unwrap(), 0);
74        assert_eq!(decode_base62("Z").unwrap(), 35);
75        assert_eq!(decode_base62("z").unwrap(), 61);
76        assert_eq!(decode_base62("10").unwrap(), 62);
77        assert_eq!(decode_base62("5A").unwrap(), 320);
78        assert_eq!(decode_base62("8i").unwrap(), 540);
79        assert_eq!(decode_base62("2u").unwrap(), 180);
80        assert_eq!(decode_base62("2bI").unwrap(), 10000);
81    }
82
83    #[test]
84    fn test_base62_round_trip() {
85        for n in [0, 1, 61, 62, 100, 320, 540, 1000, 10000, 999999] {
86            let encoded = encode_base62(n);
87            let decoded = decode_base62(&encoded).unwrap();
88            assert_eq!(decoded, n, "Failed for {}: {} -> {}", n, encoded, decoded);
89        }
90    }
91
92    #[test]
93    fn test_base62_savings() {
94        // Show byte savings
95        assert!(encode_base62(320).len() < "320".len()); // 2 vs 3 bytes
96        assert!(encode_base62(10000).len() < "10000".len()); // 3 vs 5 bytes
97    }
98}