Skip to main content

norwegian_tin_validator/
lib.rs

1use core::str;
2use std::str::FromStr;
3
4const SEQUENCE_FIRST_CHECKSUM_DIGITS: &'static [u8; 9] = &[3, 7, 6, 1, 8, 9, 4, 5, 2];
5const SEQUENCE_SECOND_CHECKSUM_DIGITS: &'static [u8; 10] = &[5, 4, 3, 2, 7, 6, 5, 4, 3, 2];
6const TIN_LENGTH: usize = 11;
7const ORG_LENGTH: usize = 9;
8const SEQUENCE_ORG_CHECKSUM_DIGITS: &'static [u8; 8] = &[3, 2, 7, 6, 5, 4, 3, 2];
9
10#[derive(Debug, PartialEq, Eq, Clone, Copy)]
11#[non_exhaustive]
12pub enum PersonKind {
13    Normal,
14    HNumber,
15    Anonymous,
16    Synthetic,
17}
18
19#[derive(Debug, PartialEq, Eq, Clone, Copy)]
20pub struct PersonNumber {
21    kind: PersonKind,
22    value: [u8; TIN_LENGTH],
23}
24
25#[derive(Debug, PartialEq, Eq, Clone, Copy)]
26pub struct OrgNumber {
27    value: [u8; ORG_LENGTH],
28}
29
30#[derive(Debug, PartialEq, Eq, Clone, Copy)]
31#[non_exhaustive]
32pub enum NorwegianTin {
33    FNumber(PersonNumber),
34    DNumber(PersonNumber),
35    OrgNumber(OrgNumber),
36}
37
38#[derive(Debug, PartialEq)]
39pub enum NorwegianTinError {
40    InvalidLength,
41    NonNumericValue,
42    InvalidChecksum,
43    InvalidDate,
44}
45
46impl std::fmt::Display for NorwegianTinError {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        match self {
49            NorwegianTinError::InvalidLength => write!(f, "InvalidLength"),
50            NorwegianTinError::NonNumericValue => write!(f, "NonNumericValue"),
51            NorwegianTinError::InvalidChecksum => write!(f, "InvalidChecksum"),
52            NorwegianTinError::InvalidDate => write!(f, "InvalidDate"),
53        }
54    }
55}
56
57impl FromStr for NorwegianTin {
58    type Err = NorwegianTinError;
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        Self::parse(s)
61    }
62}
63
64impl Into<String> for NorwegianTin {
65    fn into(self) -> String {
66        let bytes = self.get_value();
67        let s: String = bytes.iter().map(|&d| (d + b'0') as char).collect();
68        s
69    }
70}
71
72impl AsRef<[u8]> for NorwegianTin {
73    fn as_ref(&self) -> &[u8] {
74        self.get_value()
75    }
76}
77
78impl std::fmt::Display for NorwegianTin {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        let s: String = self.clone().into();
81        let kind = match self.get_kind() {
82            PersonKind::Anonymous => " (Anonymous) ",
83            PersonKind::HNumber => " (H-Number) ",
84            PersonKind::Synthetic => " (Synthetic) ",
85            PersonKind::Normal => "",
86        };
87        // Masking the last 5 digits for privacy
88        write!(f, "{}{}*****", kind, &s[0..6])
89    }
90}
91impl NorwegianTin {
92    pub fn get_value(&self) -> &[u8] {
93        match self {
94            NorwegianTin::FNumber(fnr) => &fnr.value,
95            NorwegianTin::DNumber(dnr) => &dnr.value,
96            NorwegianTin::OrgNumber(org) => &org.value,
97        }
98    }
99    pub fn get_kind(&self) -> PersonKind {
100        match self {
101            NorwegianTin::FNumber(fnr) => fnr.kind,
102            NorwegianTin::DNumber(dnr) => dnr.kind,
103            NorwegianTin::OrgNumber(_) => PersonKind::Normal, // Org numbers are not categorized by kind
104        }
105    }
106
107    pub fn parse(s: &str) -> Result<NorwegianTin, NorwegianTinError> {
108        let bytes = s.as_bytes();
109        if bytes.len() != TIN_LENGTH && bytes.len() != ORG_LENGTH {
110            return Err(NorwegianTinError::InvalidLength);
111        }
112
113        let mut digits = [0u8; TIN_LENGTH];
114        for (i, &b) in bytes.iter().enumerate() {
115            if b < b'0' || b > b'9' {
116                return Err(NorwegianTinError::NonNumericValue);
117            }
118            digits[i] = b - b'0';
119        }
120        if bytes.len() == ORG_LENGTH {
121            let _ =
122                Self::calculate_checksum(
123                    &digits[0..8],
124                    SEQUENCE_ORG_CHECKSUM_DIGITS,
125                    |r| match (11 - r) % 11 {
126                        10 => Err(NorwegianTinError::InvalidChecksum),
127                        v if v == digits[8] => Ok(()),
128                        _ => Err(NorwegianTinError::InvalidChecksum),
129                    },
130                )?;
131
132            return Ok(NorwegianTin::OrgNumber(OrgNumber {
133                value: digits[0..9].try_into().unwrap(),
134            }));
135        }
136
137        let _ =
138            Self::calculate_checksum(&digits[0..9], SEQUENCE_FIRST_CHECKSUM_DIGITS, |r| {
139                match (r + digits[9]) % 11 {
140                    0..=3 => Ok(()),
141                    _ => Err(NorwegianTinError::InvalidChecksum),
142                }
143            })?;
144        let _ =
145            Self::calculate_checksum(
146                &digits[0..10],
147                SEQUENCE_SECOND_CHECKSUM_DIGITS,
148                |r| match (r + digits[10]) % 11 {
149                    0 => Ok(()),
150                    _ => Err(NorwegianTinError::InvalidChecksum),
151                },
152            )?;
153        let kind = Self::check_kind(digits[2])?;
154
155        let day = digits[0] * 10 + digits[1];
156        let month = kind.get_base_month(digits[2] * 10 + digits[3]);
157        let year = digits[4] as u16 * 10 + digits[5] as u16;
158        // Determine if it's a D-number or F-number
159        match digits[0] {
160            0..=3 => {
161                // F-number
162                if !Self::is_valid_date(day, month, year) {
163                    return Err(NorwegianTinError::InvalidDate);
164                }
165                Ok(NorwegianTin::FNumber(PersonNumber {
166                    kind: kind,
167                    value: digits,
168                }))
169            }
170            4..=7 => {
171                let actual_day = day - 40;
172                if !Self::is_valid_date(actual_day, month, year) {
173                    return Err(NorwegianTinError::InvalidDate);
174                }
175                Ok(NorwegianTin::DNumber(PersonNumber {
176                    kind: kind,
177                    value: digits,
178                }))
179            }
180            _ => Err(NorwegianTinError::InvalidDate),
181        }
182    }
183
184    fn check_kind(month: u8) -> Result<PersonKind, NorwegianTinError> {
185        match month {
186            0..=1 => Ok(PersonKind::Normal),
187            4..=5 => Ok(PersonKind::HNumber),
188            6..=7 => Ok(PersonKind::Anonymous),
189            8..=9 => Ok(PersonKind::Synthetic),
190            _ => Err(NorwegianTinError::InvalidDate),
191        }
192    }
193
194    fn calculate_checksum<T: FnOnce(u8) -> Result<(), NorwegianTinError>>(
195        digits: &[u8],
196        weights: &[u8],
197        matcher: T,
198    ) -> Result<(), NorwegianTinError> {
199        let sum: u32 = weights
200            .iter()
201            .zip(digits.iter())
202            .map(|(&w, &d)| w as u32 * d as u32)
203            .sum();
204        let remainder = (sum % 11) as u8;
205        matcher(remainder)
206    }
207
208    fn is_valid_date(day: u8, month: u8, year: u16) -> bool {
209        if month == 0 || month > 12 || day == 0 || year >= 100 {
210            return false;
211        }
212        let days_in_month = match month {
213            1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
214            4 | 6 | 9 | 11 => 30,
215            2 => {
216                if year % 4 == 0 {
217                    29
218                } else {
219                    28
220                }
221            }
222            _ => return false,
223        };
224        day <= days_in_month
225    }
226}
227
228impl PersonKind {
229    pub fn is_test_id(&self) -> bool {
230        match self {
231            PersonKind::Normal => false,
232            PersonKind::HNumber => true,
233            PersonKind::Anonymous => true,
234            PersonKind::Synthetic => true,
235        }
236    }
237    fn get_base_month(&self, month: u8) -> u8 {
238        match self {
239            PersonKind::Normal => month,
240            PersonKind::HNumber => month - 40,
241            PersonKind::Anonymous => month - 60,
242            PersonKind::Synthetic => month - 80,
243        }
244    }
245}
246
247#[cfg(test)]
248mod test {
249    use std::vec;
250
251    pub use super::*;
252    #[test]
253    fn test_invalid_length() {
254        let tins = vec!["0123456789", "123456789012", "123", "12345678", ""];
255        for tin in tins {
256            assert_eq!(
257                NorwegianTin::parse(tin).unwrap_err(),
258                NorwegianTinError::InvalidLength
259            );
260        }
261    }
262    #[test]
263    fn test_invalid_characters() {
264        let tins = vec!["1234567890a", "abcdefghijk", "12345abc678"];
265        for tin in tins {
266            assert_eq!(
267                NorwegianTin::parse(tin).unwrap_err(),
268                NorwegianTinError::NonNumericValue
269            );
270        }
271    }
272    #[test]
273    fn test_invalid_checksum() {
274        let tins = vec![
275            "12345678901",
276            "11111111111",
277            "22222222222",
278            "33333333333",
279            "44444444444",
280            "55555555555",
281            "66666666666",
282            "77777777777",
283            "88888888888",
284            "99999999999",
285            "12345678912",
286            "98765432109",
287        ];
288        for tin in tins {
289            assert_eq!(
290                NorwegianTin::parse(tin).unwrap_err(),
291                NorwegianTinError::InvalidChecksum
292            );
293        }
294    }
295
296    #[test]
297    fn test_2032_format() {
298        let tins = vec!["11010000000", "11010000019", "11010000027", "11010000035"];
299        for tin in tins {
300            assert!(NorwegianTin::parse(tin).is_ok());
301        }
302    }
303
304    #[test]
305    fn test_invalid_date() {
306        let tins = vec!["00000000000", "11001000073"];
307        for tin in tins {
308            assert_eq!(
309                NorwegianTin::parse(tin).unwrap_err(),
310                NorwegianTinError::InvalidDate
311            );
312        }
313    }
314
315    #[test]
316    fn test_display() {
317        let err: NorwegianTinError = "00000000000".parse::<NorwegianTin>().unwrap_err();
318        assert_eq!(format!("{}", err), "InvalidDate");
319
320        let tin: NorwegianTin = "16057902284".parse().unwrap();
321        assert_eq!(format!("{}", tin), "160579*****");
322
323        let tin: NorwegianTin = "22517149261".parse().unwrap();
324        assert_eq!(format!("{}", tin), " (H-Number) 225171*****");
325
326        let tin: NorwegianTin = "08639815316".parse().unwrap();
327        assert_eq!(format!("{}", tin), " (Anonymous) 086398*****");
328
329        let tin: NorwegianTin = "70887100797".parse().unwrap();
330        assert_eq!(format!("{}", tin), " (Synthetic) 708871*****");
331    }
332
333    #[test]
334    fn test_valid_f_number() {
335        let tins = vec![
336            "16057902284",
337            "09063332523",
338            "17068632781",
339            "20069290990",
340            "06071732280",
341            "14033147466",
342            "24059638544",
343            "26073600501",
344            "24043444170",
345            "06113401662",
346            "25098514123",
347            "04015896960",
348            "28076429032",
349            "27017709351",
350            "17051023862",
351            "10093039144",
352            "16048999194",
353            "15019401615",
354            "08057294664",
355            "19045132024",
356            "29067796398",
357            "14056549677",
358            "26111739128",
359            "10103621777",
360            "17120108360",
361            "24041646671",
362            "23082311440",
363            "12042637996",
364            "01052609131",
365            "10016402791",
366            "10023737472",
367            "05048334972",
368            "23129407840",
369            "20090624106",
370            "17047438926",
371            "28093304286",
372            "24052024052",
373            "28128590643",
374            "12123717110",
375            "30123017591",
376            "17093748212",
377            "24084932503",
378            "30091723170",
379            "16032039514",
380            "09100106539",
381            "03078922159",
382            "23016141046",
383            "07119746641",
384            "14093744021",
385            "19057647348",
386            "02013299997",
387        ];
388        for tin in tins {
389            assert_eq!(NorwegianTin::parse(tin).is_ok(), true);
390            assert!(matches!(
391                NorwegianTin::parse(tin).unwrap(),
392                NorwegianTin::FNumber(_)
393            ));
394            assert_eq!(
395                NorwegianTin::parse(tin).unwrap().get_kind(),
396                PersonKind::Normal
397            );
398        }
399    }
400    #[test]
401    fn test_h_number_fnr() {
402        let tins = vec![
403            "22517149261",
404            "16501622854",
405            "10496524328",
406            "22492434063",
407            "30512441595",
408            "26420623894",
409            "15446406660",
410            "11490936698",
411            "31419107320",
412            "09486435124",
413            "29497811978",
414            "24516544984",
415            "28449596104",
416            "21482101531",
417            "09526738303",
418            "31473336629",
419            "04520606691",
420            "07502323096",
421            "24484292627",
422            "19468111414",
423            "07467214181",
424            "30418893342",
425            "09449633846",
426            "17507311458",
427            "03477998770",
428            "22487823574",
429            "02492215786",
430            "12512213926",
431            "21434290810",
432            "03431642904",
433            "06490734730",
434            "04424032617",
435            "16484997113",
436            "15424333392",
437            "02455927105",
438            "19431825034",
439            "23453707415",
440            "02498505069",
441            "14419493563",
442            "28519698023",
443            "08474914586",
444            "29457592246",
445            "10467613974",
446            "21436941169",
447            "09443311979",
448            "25431429198",
449            "19412412594",
450            "01527994698",
451            "18483710999",
452            "29500441176",
453        ];
454
455        for tin in tins {
456            assert_eq!(NorwegianTin::parse(tin).is_ok(), true);
457            assert!(matches!(
458                NorwegianTin::parse(tin).unwrap(),
459                NorwegianTin::FNumber(_)
460            ));
461            assert_eq!(
462                NorwegianTin::parse(tin).unwrap().get_kind(),
463                PersonKind::HNumber
464            );
465        }
466    }
467
468    #[test]
469    fn test_synthetic_dnr() {
470        let dnr = vec![
471            "70848000149",
472            "56865400190",
473            "60889201749",
474            "70859800961",
475            "61915201511",
476            "70887100797",
477            "47914500210",
478            "52909301009",
479            "49867500528",
480            "55896000267",
481            "62847600204",
482            "65929301905",
483            "45817500521",
484            "56878500771",
485            "52910875191",
486            "44908800521",
487            "54815601311",
488            "57839200716",
489            "44915500312",
490            "61887100240",
491            "70875300782",
492            "47858100948",
493            "64857300139",
494            "46826902183",
495            "45819901183",
496            "50828300927",
497            "55826700334",
498            "50924700936",
499            "49878300261",
500            "48898400142",
501            "46885500463",
502            "58925800463",
503            "44883300154",
504            "41901200279",
505            "69915100880",
506            "45896500361",
507            "59918601168",
508            "54928500449",
509            "45875301105",
510            "64858900172",
511            "50866301562",
512            "41916500333",
513            "57875400381",
514            "47869000198",
515            "56836700363",
516            "63843400118",
517            "55918100171",
518            "49818302737",
519            "50866900512",
520            "70924700201",
521        ];
522        for tin in dnr {
523            assert_eq!(NorwegianTin::parse(tin).is_ok(), true);
524            assert!(matches!(
525                NorwegianTin::parse(tin).unwrap(),
526                NorwegianTin::DNumber(_)
527            ));
528            assert_eq!(
529                NorwegianTin::parse(tin).unwrap().get_kind(),
530                PersonKind::Synthetic
531            );
532        }
533    }
534    #[test]
535    fn test_anonymous_fnr() {
536        let tins = vec![
537            "08639815316",
538            "13620315545",
539            "26622700351",
540            "09624093701",
541            "06673803375",
542            "17639516431",
543            "06721148228",
544            "08610428933",
545            "01713417137",
546            "02611935342",
547            "21646200102",
548            "05654134997",
549            "28616909969",
550            "06695929704",
551            "03690058307",
552            "28711842685",
553            "10706095352",
554            "19655548648",
555            "11671742127",
556            "20640081725",
557        ];
558        for tin in tins {
559            assert_eq!(NorwegianTin::parse(tin).is_ok(), true);
560            assert!(matches!(
561                NorwegianTin::parse(tin).unwrap(),
562                NorwegianTin::FNumber(_)
563            ));
564            assert_eq!(
565                NorwegianTin::parse(tin).unwrap().get_kind(),
566                PersonKind::Anonymous
567            );
568        }
569    }
570
571    #[test]
572    fn test_org_number() {
573        let orgs = vec![
574            "905661833",
575            "085649779",
576            "255399985",
577            "917766150",
578            "406099474",
579            "169994803",
580            "127412626",
581            "661532777",
582            "627143508",
583            "532464390",
584            "625711045",
585            "968668056",
586            "452238063",
587            "882897311",
588            "428621840",
589            "134511966",
590            "565768123",
591            "888302964",
592            "360559912",
593            "635203536",
594            "557117245",
595            "021244716",
596            "331370207",
597            "639703991",
598            "769676260",
599            "084416371",
600            "635606576",
601            "190537102",
602            "945867159",
603            "350759131",
604            "008303274",
605            "515559396",
606            "381740196",
607            "001686313",
608            "977279410",
609            "282720493",
610            "052965152",
611            "310958352",
612            "687739515",
613            "399784646",
614            "389213292",
615            "638522837",
616            "981000463",
617            "973289829",
618            "934708431",
619            "094979358",
620            "003221571",
621            "283978958",
622            "003437353",
623            "123480759",
624        ];
625        for org in orgs {
626            assert!(matches!(
627                NorwegianTin::parse(org).unwrap(),
628                NorwegianTin::OrgNumber(_)
629            ));
630        }
631    }
632    #[test]
633    fn test_org_number_invalid() {
634        let orgs = vec!["905661834", "085649778", "255399984", "917766151"];
635        for org in orgs {
636            assert_eq!(
637                NorwegianTin::parse(org).unwrap_err(),
638                NorwegianTinError::InvalidChecksum
639            );
640        }
641    }
642}