heidi/chi.rs
1// Copyright 2020 Arnau Siches
2
3// Licensed under the MIT license <LICENCE or http://opensource.org/licenses/MIT>.
4// This file may not be copied, modified, or distributed except
5// according to those terms.
6
7//! `heidi` implements the CHI number validation “Modulus 11”. See:
8//! <https://www.ndc.scot.nhs.uk/Data-Dictionary/SMR-Datasets//Patient-Identification-and-Demographic-Information/Community-Health-Index-Number/>
9//!
10//! The CHI Number (Community Health Index) is a unique number allocated to
11//! every patient registered with the NHS in Scotland.
12//!
13//! A CHI Number is always 10 digits long. The first 6 digits are the date of
14//! birth as `DDMMYY`. The next 2 digits are random between 0 and 9. The 9th
15//! digit is random as well but it is always even for females and odd for males.
16//!
17//! The last digit of the number is the “check digit” to aid in integrity checks.
18
19use crate::error::ValidationError;
20use crate::number;
21use std::convert::TryFrom;
22use std::fmt;
23use std::str::FromStr;
24
25/// A digit can be from 0 to 9.
26pub type Digit = u16;
27
28#[derive(PartialEq, Clone, Debug)]
29pub struct Number(number::Number);
30
31impl Number {
32 /// Creates a new Number from the main 9 digits.
33 ///
34 /// Prefer `FromStr` or `TryFrom<[Digit; 10]>` if you have a full NHS number.
35 ///
36 /// # Examples
37 ///
38 /// ```
39 /// use heidi::chi::Number;
40 ///
41 /// let n: [u16; 9] = [0, 1, 0, 1, 9, 9, 0, 0, 1];
42 /// let number = Number::new(n);
43 ///
44 /// assert_eq!(*number.unwrap().checkdigit(), 4);
45 /// ```
46 pub fn new(digits: [Digit; 9]) -> Result<Self, ValidationError> {
47 validate(&digits)?;
48
49 Ok(Number(number::Number::new(digits)?))
50 }
51
52 pub fn checkdigit(&self) -> &Digit {
53 &self.0.checkdigit()
54 }
55
56 pub fn digits(&self) -> &[Digit; 9] {
57 &self.0.digits()
58 }
59}
60
61impl fmt::Display for Number {
62 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
63 self.0.fmt(formatter)
64 }
65}
66
67impl TryFrom<&[Digit; 10]> for Number {
68 type Error = ValidationError;
69
70 fn try_from(value: &[Digit; 10]) -> Result<Self, Self::Error> {
71 Ok(Number(number::Number::try_from(value)?))
72 }
73}
74
75impl TryFrom<String> for Number {
76 type Error = ValidationError;
77
78 fn try_from(value: String) -> Result<Self, Self::Error> {
79 let number = number::Number::try_from(value)?;
80
81 validate(number.digits())?;
82
83 Ok(Number(number))
84 }
85}
86
87impl TryFrom<usize> for Number {
88 type Error = ValidationError;
89
90 fn try_from(value: usize) -> Result<Self, Self::Error> {
91 let number = number::Number::try_from(value)?;
92
93 validate(number.digits())?;
94
95 Ok(Number(number))
96 }
97}
98
99impl FromStr for Number {
100 type Err = ValidationError;
101
102 /// Converts a string slice of 10 digits into a [`Number`].
103 ///
104 /// ```
105 /// use heidi::chi::Number;
106 /// use std::str::FromStr;
107 ///
108 /// let n = "3011203237";
109 /// let number = Number::from_str(n);
110 ///
111 /// assert_eq!(*number.unwrap().checkdigit(), 7);
112 /// ```
113 ///
114 /// # Errors
115 ///
116 /// Fails with [ValidationError] when the check digit cannot be verified.
117 fn from_str(s: &str) -> Result<Self, Self::Err> {
118 let number = number::Number::from_str(s)?;
119
120 validate(number.digits())?;
121
122 Ok(Number(number))
123 }
124}
125
126/// Checks the date boundaries.
127///
128/// TODO: Validaton is naive. Does not check for real month limits nor leap years.
129fn validate(digits: &[u16; 9]) -> Result<(), ValidationError> {
130 let day = digits[0] * 10 + digits[1];
131 let month = digits[2] * 10 + digits[3];
132
133 if day == 0 || day > 31 || month == 0 || month > 12 {
134 return Err(ValidationError::new("Invalid CHI number"));
135 }
136
137 Ok(())
138}
139
140/// Returns a random Chi Number.
141///
142/// If the result is not valid (e.g. the modulus 11 is 10) it will generate a new one.
143///
144/// # Examples
145///
146/// ```
147/// use heidi::chi::lottery;
148///
149/// let number = lottery();
150/// assert!(number.is_ok());
151/// ```
152pub fn lottery() -> Result<Number, ValidationError> {
153 use rand::prelude::*;
154
155 let mut rng = rand::thread_rng();
156 let mut digits = [0u16; 9];
157 let distr = rand::distributions::Uniform::new_inclusive(0, 9);
158
159 for x in &mut digits {
160 *x = rng.sample(distr);
161 }
162
163 match Number::new(digits) {
164 Err(_) => lottery(),
165 number => number,
166 }
167}