Skip to main content

alphanumeric_stepper/
lib.rs

1/*!
2# Alphanumeric Stepper
3
4A reversible alphanumeric sequence codec for compact serial codes like 000..999, A00..Z99, AA0..ZZ9, and AAA..ZZZ.
5
6## Examples
7
8Encode a number to an Alphanumeric Stepper string.
9
10```rust
11use alphanumeric_stepper::AlphanumericStepper;
12
13let stepper = AlphanumericStepper::<u16>::new(3).unwrap();
14
15assert_eq!("000", stepper.encode(0).unwrap());
16assert_eq!("001", stepper.encode(1).unwrap());
17assert_eq!("999", stepper.encode(999).unwrap());
18assert_eq!("A00", stepper.encode(1000).unwrap());
19assert_eq!("A99", stepper.encode(1099).unwrap());
20assert_eq!("B00", stepper.encode(1100).unwrap());
21assert_eq!("ZZZ", stepper.encode(27935).unwrap());
22```
23
24Decode an Alphanumeric Stepper string to a number.
25
26```rust
27use alphanumeric_stepper::AlphanumericStepper;
28
29let stepper = AlphanumericStepper::<u16>::new(3).unwrap();
30
31assert_eq!(0, stepper.decode("000").unwrap());
32assert_eq!(1, stepper.decode("001").unwrap());
33assert_eq!(999, stepper.decode("999").unwrap());
34assert_eq!(1000, stepper.decode("A00").unwrap());
35assert_eq!(1099, stepper.decode("A99").unwrap());
36assert_eq!(1100, stepper.decode("B00").unwrap());
37assert_eq!(27935, stepper.decode("ZZZ").unwrap());
38```
39
40## Std
41
42Enable the `std` features to compile this crate with std.
43
44```toml
45[dependencies.alphanumeric-stepper]
46version = "*"
47features = ["std"]
48```
49*/
50
51#![cfg_attr(not(feature = "std"), no_std)]
52#![cfg_attr(docsrs, feature(doc_cfg))]
53
54mod errors;
55
56extern crate alloc;
57
58use alloc::{string::String, vec::Vec};
59use core::fmt::Write;
60
61pub use errors::*;
62
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct AlphanumericStepper<T = u128> {
65    width:       usize,
66    max_numbers: Vec<T>,
67}
68
69impl<T> AlphanumericStepper<T> {
70    /// Returns the width.
71    #[inline]
72    pub const fn width(&self) -> usize {
73        self.width
74    }
75}
76
77impl<T: Copy> AlphanumericStepper<T> {
78    /// Returns the maximum number that can be encoded.
79    #[inline]
80    pub fn max_number(&self) -> T {
81        self.max_numbers[self.width]
82    }
83}
84
85macro_rules! impl_alphanumeric_stepper_backend {
86    ($($integer:ty),+ $(,)?) => {
87        $(
88            impl AlphanumericStepper<$integer> {
89                #[inline]
90                fn checked_pow(base: $integer, exp: usize) -> Option<$integer> {
91                    let mut value: $integer = 1;
92
93                    for _ in 0..exp {
94                        value = value.checked_mul(base)?;
95                    }
96
97                    Some(value)
98                }
99
100                /// Creates a new `AlphanumericStepper` instance with the specified width.
101                #[inline]
102                pub fn new(width: usize) -> Result<Self, AlphanumericStepperBuildError> {
103                    if width == 0 {
104                        return Err(AlphanumericStepperBuildError::InvalidWidth);
105                    }
106
107                    let mut max_numbers = Vec::with_capacity(width + 1);
108                    let mut sum: $integer = 0;
109                    let mut term = Self::checked_pow(10, width)
110                        .ok_or(AlphanumericStepperBuildError::InvalidWidth)?;
111
112                    for alphabet_count in 0..=width {
113                        sum = sum
114                            .checked_add(term)
115                            .ok_or(AlphanumericStepperBuildError::InvalidWidth)?;
116
117                        max_numbers.push(sum - 1);
118
119                        if alphabet_count < width {
120                            term = (term / 10)
121                                .checked_mul(26)
122                                .ok_or(AlphanumericStepperBuildError::InvalidWidth)?;
123                        }
124                    }
125
126                    Ok(Self {
127                        width,
128                        max_numbers,
129                    })
130                }
131
132                /// Encodes a number into a string.
133                pub fn encode(&self, number: $integer) -> Result<String, AlphanumericStepperEncodeError> {
134                    if number > self.max_number() {
135                        return Err(AlphanumericStepperEncodeError::NumberOutOfRange);
136                    }
137
138                    let mut s = String::with_capacity(self.width);
139
140                    if number <= self.max_numbers[0] {
141                        s.write_fmt(format_args!("{:0>width$}", number, width = self.width)).unwrap();
142                    } else {
143                        for (alphabet_count, max_number) in
144                            self.max_numbers.iter().copied().enumerate().skip(1)
145                        {
146                            if number > max_number {
147                                continue;
148                            }
149
150                            let digit_count = self.width - alphabet_count;
151                            let mut n = number - self.max_numbers[alphabet_count - 1] - 1;
152                            let digit_base = Self::checked_pow(10, digit_count).unwrap();
153                            let mut p = digit_base
154                                .checked_mul(Self::checked_pow(26, alphabet_count - 1).unwrap())
155                                .unwrap();
156
157                            for _ in 0..alphabet_count {
158                                let d = n / p;
159
160                                s.push((b'A' + d as u8) as char);
161
162                                n -= d * p;
163                                p /= 26;
164                            }
165
166                            if digit_count > 0 {
167                                s.write_fmt(format_args!("{:0>width$}", n, width = digit_count)).unwrap();
168                            }
169
170                            break;
171                        }
172                    }
173
174                    Ok(s)
175                }
176
177                /// Decodes a string into a number.
178                pub fn decode(&self, s: impl AsRef<str>) -> Result<$integer, AlphanumericStepperDecodeError> {
179                    let s = s.as_ref();
180
181                    if s.len() != self.width {
182                        return Err(AlphanumericStepperDecodeError::InvalidLength);
183                    }
184
185                    let bytes = s.as_bytes();
186                    let mut alphabet_count = 0;
187
188                    while alphabet_count < bytes.len() {
189                        let b = bytes[alphabet_count];
190
191                        if b.is_ascii_uppercase() {
192                            alphabet_count += 1;
193                        } else if b.is_ascii_digit() {
194                            break;
195                        } else {
196                            return Err(AlphanumericStepperDecodeError::InvalidCharacter);
197                        }
198                    }
199
200                    for &b in &bytes[alphabet_count..] {
201                        if !b.is_ascii_digit() {
202                            return Err(AlphanumericStepperDecodeError::InvalidCharacter);
203                        }
204                    }
205
206                    let digit_count = self.width - alphabet_count;
207                    let mut number = if alphabet_count == 0 {
208                        0
209                    } else {
210                        self.max_numbers[alphabet_count - 1] + 1
211                    };
212
213                    let mut n: $integer = 0;
214
215                    for &b in &bytes[..alphabet_count] {
216                        n = n
217                            .checked_mul(26)
218                            .and_then(|value| value.checked_add((b - b'A') as $integer))
219                            .unwrap();
220                    }
221
222                    number = number
223                        .checked_add(n.checked_mul(Self::checked_pow(10, digit_count).unwrap()).unwrap())
224                        .unwrap();
225
226                    n = 0;
227
228                    for &b in &bytes[alphabet_count..] {
229                        n = n
230                            .checked_mul(10)
231                            .and_then(|value| value.checked_add((b - b'0') as $integer))
232                            .unwrap();
233                    }
234
235                    number = number.checked_add(n).unwrap();
236
237                    Ok(number)
238                }
239            }
240        )+
241    };
242}
243
244impl_alphanumeric_stepper_backend!(u8, u16, u32, u64, u128, usize);