idkollen_client/models/
ssn.rs1use fmt::Display;
2use serde::{Deserialize, Deserializer, Serialize, Serializer};
3use std::fmt;
4use thiserror::Error;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash)]
15pub struct Pno(String);
16
17#[derive(Debug, Error)]
18#[error("invalid personnummer: {0}")]
19pub struct PnoError(String);
20
21impl Pno {
22 pub fn parse(s: &str) -> Result<Self, PnoError> {
23 let cleaned = s
24 .chars()
25 .filter(|&c| c != '-' && c != '+')
26 .collect::<String>();
27
28 if !cleaned.chars().all(|c| c.is_ascii_digit()) {
29 return Err(PnoError("contains non-digit characters".to_owned()));
30 }
31
32 let ten = match cleaned.len() {
33 10 => cleaned.as_str().to_owned(),
34 12 => cleaned[2..].to_owned(),
35 n => {
36 return Err(PnoError(format!(
37 "expected 10 or 12 digits after stripping separators, got {n}"
38 )));
39 },
40 };
41
42 if !luhn10(&ten) {
43 return Err(PnoError("Luhn check failed".to_owned()));
44 }
45
46 Ok(Self(cleaned))
47 }
48
49 #[inline]
50 #[must_use]
51 pub fn as_str(&self) -> &str {
52 &self.0
53 }
54}
55
56impl From<Pno> for String {
57 #[inline]
58 fn from(p: Pno) -> String {
59 p.0
60 }
61}
62
63impl AsRef<str> for Pno {
64 #[inline]
65 fn as_ref(&self) -> &str {
66 &self.0
67 }
68}
69
70impl Display for Pno {
71 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72 f.write_str(&self.0)
73 }
74}
75
76impl Serialize for Pno {
77 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
78 self.0.serialize(s)
79 }
80}
81
82impl<'de> Deserialize<'de> for Pno {
83 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
84 let s = String::deserialize(d)?;
85
86 Pno::parse(&s).map_err(serde::de::Error::custom)
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub struct Nnin(String);
98
99#[derive(Debug, Error)]
100#[error("invalid fødselsnummer: {0}")]
101pub struct NninError(String);
102
103impl Nnin {
104 pub fn parse(s: &str) -> Result<Self, NninError> {
105 let cleaned = s.chars().filter(|&c| c != '-').collect::<String>();
106
107 if cleaned.len() != 11 {
108 return Err(NninError(format!(
109 "expected 11 digits, got {}",
110 cleaned.len()
111 )));
112 }
113
114 if !cleaned.chars().all(|c| c.is_ascii_digit()) {
115 return Err(NninError("contains non-digit characters".to_owned()));
116 }
117
118 if !nnin_valid(&cleaned) {
119 return Err(NninError("modulo-11 check failed".to_owned()));
120 }
121
122 Ok(Self(cleaned))
123 }
124
125 #[inline]
126 #[must_use]
127 pub fn as_str(&self) -> &str {
128 &self.0
129 }
130}
131
132impl From<Nnin> for String {
133 #[inline]
134 fn from(n: Nnin) -> String {
135 n.0
136 }
137}
138
139impl AsRef<str> for Nnin {
140 #[inline]
141 fn as_ref(&self) -> &str {
142 &self.0
143 }
144}
145
146impl Display for Nnin {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 f.write_str(&self.0)
149 }
150}
151
152impl Serialize for Nnin {
153 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
154 self.0.serialize(s)
155 }
156}
157
158impl<'de> Deserialize<'de> for Nnin {
159 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
160 let s = String::deserialize(d)?;
161
162 Nnin::parse(&s).map_err(serde::de::Error::custom)
163 }
164}
165
166#[derive(Debug, Clone, PartialEq, Eq, Hash)]
177pub struct Cpr(String);
178
179#[derive(Debug, Error)]
180#[error("invalid CPR number: {0}")]
181pub struct CprError(String);
182
183impl Cpr {
184 pub fn parse(s: &str) -> Result<Self, CprError> {
185 let cleaned = s.chars().filter(|&c| c != '-').collect::<String>();
186
187 if cleaned.len() != 10 {
188 return Err(CprError(format!(
189 "expected 10 digits, got {}",
190 cleaned.len()
191 )));
192 }
193
194 if !cleaned.chars().all(|c| c.is_ascii_digit()) {
195 return Err(CprError("contains non-digit characters".to_owned()));
196 }
197
198 let day: u32 = cleaned[..2].parse().unwrap();
199 let month: u32 = cleaned[2..4].parse().unwrap();
200
201 if !(1..=31).contains(&(day % 60)) || !(1..=12).contains(&month) {
203 return Err(CprError(
204 "date portion is not a valid calendar date".to_owned(),
205 ));
206 }
207
208 Ok(Self(cleaned))
209 }
210
211 #[inline]
212 #[must_use]
213 pub fn as_str(&self) -> &str {
214 &self.0
215 }
216}
217
218impl From<Cpr> for String {
219 #[inline]
220 fn from(c: Cpr) -> String {
221 c.0
222 }
223}
224
225impl AsRef<str> for Cpr {
226 #[inline]
227 fn as_ref(&self) -> &str {
228 &self.0
229 }
230}
231
232impl Display for Cpr {
233 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234 f.write_str(&self.0)
235 }
236}
237
238impl Serialize for Cpr {
239 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
240 self.0.serialize(s)
241 }
242}
243
244impl<'de> Deserialize<'de> for Cpr {
245 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
246 let s = String::deserialize(d)?;
247
248 Cpr::parse(&s).map_err(serde::de::Error::custom)
249 }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq, Hash)]
261pub struct Hetu(String);
262
263#[derive(Debug, Error)]
264#[error("invalid henkilötunnus: {0}")]
265pub struct HetuError(String);
266
267const HETU_ALPHABET: &[u8] = b"0123456789ABCDEFHJKLMNPRSTUVWXY";
268
269impl Hetu {
270 pub fn parse(s: &str) -> Result<Self, HetuError> {
271 let upper = s.to_ascii_uppercase();
272 let b = upper.as_bytes();
273
274 if b.len() != 11 {
275 return Err(HetuError(format!(
276 "expected 11 characters, got {}",
277 b.len()
278 )));
279 }
280
281 let sep = b[6] as char;
282
283 if sep != '-' && sep != '+' && sep != 'A' {
284 return Err(HetuError(format!(
285 "invalid century marker '{sep}'; expected '-', '+', or 'A'"
286 )));
287 }
288
289 let date_part = &upper[..6];
290 let ind_part = &upper[7..10];
291
292 if !date_part.chars().all(|c| c.is_ascii_digit()) {
293 return Err(HetuError(
294 "date portion contains non-digit characters".to_owned(),
295 ));
296 }
297
298 if !ind_part.chars().all(|c| c.is_ascii_digit()) {
299 return Err(HetuError(
300 "individual number contains non-digit characters".to_owned(),
301 ));
302 }
303
304 let n: u64 = format!("{date_part}{ind_part}").parse().unwrap();
305 let expected = HETU_ALPHABET[(n % 31) as usize] as char;
306
307 if b[10] as char != expected {
308 return Err(HetuError(format!(
309 "check character mismatch: got '{}', expected '{expected}'",
310 b[10] as char
311 )));
312 }
313
314 Ok(Self(upper))
315 }
316
317 #[inline]
318 #[must_use]
319 pub fn as_str(&self) -> &str {
320 &self.0
321 }
322}
323
324impl From<Hetu> for String {
325 #[inline]
326 fn from(h: Hetu) -> String {
327 h.0
328 }
329}
330
331impl AsRef<str> for Hetu {
332 #[inline]
333 fn as_ref(&self) -> &str {
334 &self.0
335 }
336}
337
338impl Display for Hetu {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 f.write_str(&self.0)
341 }
342}
343
344impl Serialize for Hetu {
345 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
346 self.0.serialize(s)
347 }
348}
349
350impl<'de> Deserialize<'de> for Hetu {
351 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
352 let s = String::deserialize(d)?;
353
354 Hetu::parse(&s).map_err(serde::de::Error::custom)
355 }
356}
357
358fn luhn10(s: &str) -> bool {
362 let sum: u32 = s
363 .chars()
364 .enumerate()
365 .map(|(i, c)| {
366 let d = c.to_digit(10).unwrap();
367 let v = if i % 2 == 0 { d * 2 } else { d };
368 if v >= 10 { v - 9 } else { v }
369 })
370 .sum();
371
372 sum.is_multiple_of(10)
373}
374
375fn nnin_valid(s: &str) -> bool {
377 let d = s
378 .chars()
379 .map(|c| c.to_digit(10).unwrap())
380 .collect::<Vec<_>>();
381 let s1: u32 = [3u32, 7, 6, 1, 8, 9, 4, 5, 2, 1]
382 .iter()
383 .zip(d.iter())
384 .map(|(w, v)| w * v)
385 .sum();
386
387 if !s1.is_multiple_of(11) {
388 return false;
389 }
390
391 let s2: u32 = [5u32, 4, 3, 2, 7, 6, 5, 4, 3, 2, 1]
392 .iter()
393 .zip(d.iter())
394 .map(|(w, v)| w * v)
395 .sum();
396
397 s2.is_multiple_of(11)
398}