1use std::fmt;
22use std::str::FromStr;
23
24pub const MAX_ID_LEN: usize = 30;
26pub const MAX_BODY_LEN: usize = MAX_ID_LEN - 1;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
30pub enum IdType {
31 Evse,
33 Pool,
35 Station,
37}
38
39impl IdType {
40 pub const fn as_char(self) -> char {
41 match self {
42 IdType::Evse => 'E',
43 IdType::Pool => 'P',
44 IdType::Station => 'S',
45 }
46 }
47
48 pub fn from_char(c: char) -> Option<IdType> {
49 match c.to_ascii_uppercase() {
50 'E' => Some(IdType::Evse),
51 'P' => Some(IdType::Pool),
52 'S' => Some(IdType::Station),
53 _ => None,
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum Error {
60 Empty,
61 InvalidType(char),
62 InvalidCharacter(char),
63 TooLong { len: usize, max: usize },
64 CheckDigitMismatch { expected: char, found: char },
65}
66
67impl fmt::Display for Error {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69 match self {
70 Error::Empty => f.write_str("identifier is empty"),
71 Error::InvalidType(c) => write!(f, "invalid type character '{c}', expected E, P or S"),
72 Error::InvalidCharacter(c) => {
73 write!(f, "invalid character '{c}', only [A-Za-z0-9] allowed")
74 }
75 Error::TooLong { len, max } => {
76 write!(f, "identifier part has {len} characters, maximum is {max}")
77 }
78 Error::CheckDigitMismatch { expected, found } => write!(
79 f,
80 "check character mismatch: expected '{expected}', found '{found}'"
81 ),
82 }
83 }
84}
85
86impl std::error::Error for Error {}
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
90pub struct EvseId {
91 value: String,
93}
94
95impl EvseId {
96 pub fn generate(kind: IdType, body: &str) -> Result<EvseId, Error> {
99 if body.is_empty() {
100 return Err(Error::Empty);
101 }
102 if let Some(c) = body.chars().find(|c| !c.is_ascii_alphanumeric()) {
103 return Err(Error::InvalidCharacter(c));
104 }
105 if body.len() > MAX_BODY_LEN {
106 return Err(Error::TooLong {
107 len: body.len(),
108 max: MAX_BODY_LEN,
109 });
110 }
111 let mut value = String::with_capacity(body.len() + 2);
112 value.push(kind.as_char());
113 value.push_str(&body.to_ascii_uppercase());
114 let check = check_digit(&value)?;
115 value.push(check);
116 Ok(EvseId { value })
117 }
118
119 pub fn station(seq: u64) -> EvseId {
121 EvseId::generate(IdType::Station, &base36(seq, 6)).expect("valid body")
122 }
123
124 pub fn pool(seq: u64) -> EvseId {
126 EvseId::generate(IdType::Pool, &base36(seq, 6)).expect("valid body")
127 }
128
129 pub fn evse_of(station: &EvseId, evse_number: u32) -> Result<EvseId, Error> {
132 let station_body = station.body();
133 EvseId::generate(IdType::Evse, &format!("{station_body}{evse_number:02}"))
134 }
135
136 pub fn parse(s: &str) -> Result<EvseId, Error> {
138 let s = s.trim();
139 let mut chars = s.chars();
140 let t = chars.next().ok_or(Error::Empty)?;
141 let kind = IdType::from_char(t).ok_or(Error::InvalidType(t))?;
142 let rest = chars.as_str();
143 if rest.len() < 2 {
144 return Err(Error::Empty);
145 }
146 if let Some(c) = rest.chars().find(|c| !c.is_ascii_alphanumeric()) {
147 return Err(Error::InvalidCharacter(c));
148 }
149 if rest.len() > MAX_ID_LEN {
150 return Err(Error::TooLong {
151 len: rest.len(),
152 max: MAX_ID_LEN,
153 });
154 }
155 let upper = format!("{}{}", kind.as_char(), rest.to_ascii_uppercase());
156 let (payload, found) = upper.split_at(upper.len() - 1);
157 let expected = check_digit(payload)?;
158 let found = found.chars().next().expect("non-empty");
159 if expected != found {
160 return Err(Error::CheckDigitMismatch { expected, found });
161 }
162 Ok(EvseId { value: upper })
163 }
164
165 pub fn kind(&self) -> IdType {
166 IdType::from_char(self.value.as_bytes()[0] as char).expect("validated")
167 }
168
169 pub fn body(&self) -> &str {
171 &self.value[1..self.value.len() - 1]
172 }
173
174 pub fn check_char(&self) -> char {
175 self.value.as_bytes()[self.value.len() - 1] as char
176 }
177
178 pub fn as_str(&self) -> &str {
179 &self.value
180 }
181
182 pub fn with_operator(&self, country: &str, operator: &str) -> String {
185 format!(
186 "{}*{}*{}",
187 country.to_ascii_uppercase(),
188 operator.to_ascii_uppercase(),
189 self.value
190 )
191 }
192}
193
194impl fmt::Display for EvseId {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 f.write_str(&self.value)
197 }
198}
199
200impl FromStr for EvseId {
201 type Err = Error;
202 fn from_str(s: &str) -> Result<Self, Self::Err> {
203 EvseId::parse(s)
204 }
205}
206
207impl AsRef<str> for EvseId {
208 fn as_ref(&self) -> &str {
209 &self.value
210 }
211}
212
213fn base36(mut n: u64, width: usize) -> String {
214 const DIGITS: &[u8; 36] = b"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
215 let mut buf = Vec::new();
216 loop {
217 buf.push(DIGITS[(n % 36) as usize]);
218 n /= 36;
219 if n == 0 {
220 break;
221 }
222 }
223 while buf.len() < width {
224 buf.push(b'0');
225 }
226 buf.reverse();
227 String::from_utf8(buf).expect("ascii")
228}
229
230type M2 = [[u8; 2]; 2];
235type V2 = [u8; 2];
236
237const P1: M2 = [[0, 1], [1, 1]];
239const P2: M2 = [[0, 1], [1, 2]];
241
242fn mat_mul(a: &M2, b: &M2, m: u8) -> M2 {
243 let mut r = [[0u8; 2]; 2];
244 for (i, row) in r.iter_mut().enumerate() {
245 for (j, cell) in row.iter_mut().enumerate() {
246 *cell = ((a[i][0] as u16 * b[0][j] as u16 + a[i][1] as u16 * b[1][j] as u16) % m as u16)
247 as u8;
248 }
249 }
250 r
251}
252
253fn mat_pow(p: &M2, e: u32, m: u8) -> M2 {
254 let mut r = [[1, 0], [0, 1]];
255 for _ in 0..e {
256 r = mat_mul(&r, p, m);
257 }
258 r
259}
260
261fn vec_mul(v: &V2, p: &M2, m: u8) -> V2 {
263 [
264 ((v[0] as u16 * p[0][0] as u16 + v[1] as u16 * p[1][0] as u16) % m as u16) as u8,
265 ((v[0] as u16 * p[0][1] as u16 + v[1] as u16 * p[1][1] as u16) % m as u16) as u8,
266 ]
267}
268
269fn char_value(c: char) -> Option<u8> {
270 match c.to_ascii_uppercase() {
271 d @ '0'..='9' => Some(d as u8 - b'0'),
272 l @ 'A'..='Z' => Some(l as u8 - b'A' + 10),
273 _ => None,
274 }
275}
276
277pub fn check_digit(input: &str) -> Result<char, Error> {
279 if input.is_empty() {
280 return Err(Error::Empty);
281 }
282 let mut t1: V2 = [0, 0];
283 let mut t2: V2 = [0, 0];
284 let mut n = 0u32;
285 for c in input.chars() {
286 let a = char_value(c).ok_or(Error::InvalidCharacter(c))?;
287 n += 1;
288 let (q, r) = (a / 9, a % 9);
290 let qv: V2 = [q / 2, q % 2];
291 let rv: V2 = [r / 3, r % 3];
292 let a1 = vec_mul(&qv, &mat_pow(&P1, n % 3, 2), 2);
294 let a2 = vec_mul(&rv, &mat_pow(&P2, n % 8, 3), 3);
295 t1 = [(t1[0] + a1[0]) % 2, (t1[1] + a1[1]) % 2];
296 t2 = [(t2[0] + a2[0]) % 3, (t2[1] + a2[1]) % 3];
297 }
298 let k = n + 1;
300 let inv1 = mat_pow(&P1, (3 - k % 3) % 3, 2);
301 let inv2 = mat_pow(&P2, (8 - k % 8) % 8, 3);
302 let q = vec_mul(&t1, &inv1, 2); let r_pos = vec_mul(&t2, &inv2, 3);
304 let r = [(3 - r_pos[0]) % 3, (3 - r_pos[1]) % 3];
305 let value = (q[0] * 2 + q[1]) * 9 + (r[0] * 3 + r[1]);
306 Ok(if value < 10 {
307 (b'0' + value) as char
308 } else {
309 (b'A' + value - 10) as char
310 })
311}
312
313pub fn verify(input_with_check: &str) -> bool {
315 let Some((idx, last)) = input_with_check.char_indices().last() else {
316 return false;
317 };
318 idx > 0 && check_digit(&input_with_check[..idx]).is_ok_and(|c| c == last.to_ascii_uppercase())
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324
325 #[test]
326 fn annex1_worked_example() {
327 assert_eq!(check_digit("DE83DUIEN83QGZ").unwrap(), 'D');
329 assert!(verify("DE83DUIEN83QGZD"));
330 assert!(verify("de83duien83qgzd"));
331 assert!(!verify("DE83DUIEN83QGZE"));
332 }
333
334 #[test]
335 fn matrix_orders() {
336 assert_eq!(mat_pow(&P1, 3, 2), [[1, 0], [0, 1]]);
337 assert_eq!(mat_pow(&P2, 8, 3), [[1, 0], [0, 1]]);
338 assert_eq!(mat_pow(&P1, 2, 2), [[1, 1], [1, 0]]);
340 assert_eq!(mat_pow(&P2, 2, 3), [[1, 2], [2, 2]]);
341 assert_eq!(mat_pow(&P2, 5, 3), [[0, 2], [2, 1]]);
342 }
343
344 #[test]
345 fn detects_common_typing_errors() {
346 let base = "E1234ABCD56";
347 let good = format!("{base}{}", check_digit(base).unwrap());
348 assert!(verify(&good));
349 let bytes = good.as_bytes();
350 for i in 1..bytes.len() - 1 {
352 for c in "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".bytes() {
353 if c == bytes[i] {
354 continue;
355 }
356 let mut b = bytes.to_vec();
357 b[i] = c;
358 assert!(
359 !verify(std::str::from_utf8(&b).unwrap()),
360 "substitution at {i}"
361 );
362 }
363 if bytes[i] != bytes[i + 1] {
364 let mut b = bytes.to_vec();
365 b.swap(i, i + 1);
366 assert!(
367 !verify(std::str::from_utf8(&b).unwrap()),
368 "transposition at {i}"
369 );
370 }
371 }
372 }
373
374 #[test]
375 fn generate_and_parse() {
376 let s = EvseId::station(1);
377 assert_eq!(s.kind(), IdType::Station);
378 assert_eq!(s.body(), "000001");
379 assert_eq!(EvseId::parse(s.as_str()).unwrap(), s);
380 assert_eq!(EvseId::parse(&s.as_str().to_ascii_lowercase()).unwrap(), s);
381
382 let e = EvseId::evse_of(&s, 2).unwrap();
383 assert_eq!(e.kind(), IdType::Evse);
384 assert_eq!(e.body(), "00000102");
385 assert!(verify(e.as_str()));
386 assert!(e.with_operator("fr", "abc").starts_with("FR*ABC*E00000102"));
387
388 assert_eq!(
389 EvseId::generate(IdType::Pool, "x-1"),
390 Err(Error::InvalidCharacter('-'))
391 );
392 assert!(matches!(
393 EvseId::generate(IdType::Pool, &"A".repeat(30)),
394 Err(Error::TooLong { .. })
395 ));
396 assert!(EvseId::generate(IdType::Pool, &"A".repeat(29)).is_ok());
397 assert!(matches!(
398 EvseId::parse("X123"),
399 Err(Error::InvalidType('X'))
400 ));
401 let mut bad = s.as_str().to_owned();
402 bad.replace_range(2..3, "9");
403 assert!(matches!(
404 EvseId::parse(&bad),
405 Err(Error::CheckDigitMismatch { .. })
406 ));
407 assert_eq!(EvseId::station(36 * 36).body(), "000100");
408 }
409}
410
411#[cfg(doctest)]
413#[doc = include_str!("../README.md")]
414pub struct ReadmeDoctests;