Skip to main content

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}