canada_sin/
lib.rs

1//! A library for parsing Canadian social insurance numbers and business numbers.
2
3use std::{convert::TryInto, fmt};
4
5#[derive(Debug, Copy, Clone, PartialEq, Eq)]
6#[non_exhaustive]
7/// An error resulting from parsing a SIN
8pub enum SINParseError {
9    TooLong,
10    TooShort,
11    InvalidChecksum,
12}
13
14/// Types of SINs: All the provinces, plus some other categories.
15#[derive(Debug, Copy, Clone, PartialEq, Eq)]
16#[non_exhaustive]
17pub enum SINType {
18    /// CRA-assigned Individual Tax Numbers, Temporary Tax Numbers and Adoption Tax Numbers. These
19    /// are currently only assigned to natural people.
20    CRAAssigned,
21    TemporaryResident,
22    /// Business numbers and SINs share the same namespace.
23    BusinessNumber,
24    /// Military forces abroad.
25    OverseasForces,
26    Alberta,
27    BritishColumbia,
28    Manitoba,
29    NewBrunswick,
30    NewfoundlandLabrador,
31    NorthwestTerritories,
32    NovaScotia,
33    Nunavut,
34    Ontario,
35    PrinceEdwardIsland,
36    Quebec,
37    Saskatchewan,
38    Yukon,
39}
40
41impl SINType {
42    /// Does the SIN repersent someone in a province?
43    pub fn is_province(self) -> bool {
44        use SINType::*;
45        matches!(
46            self,
47            Alberta
48                | BritishColumbia
49                | Manitoba
50                | NewBrunswick
51                | NewfoundlandLabrador
52                | NorthwestTerritories
53                | NovaScotia
54                | Nunavut
55                | Ontario
56                | PrinceEdwardIsland
57                | Quebec
58                | Saskatchewan
59                | Yukon
60        )
61    }
62    /// Does the SIN repersent a human? Currently only business numbers are assigned to non-humans.
63    pub fn is_human(self) -> bool {
64        !matches!(self, Self::BusinessNumber)
65    }
66}
67
68#[derive(Debug, Copy, Clone, Eq, PartialEq)]
69/// A social insurance number.
70pub struct SIN {
71    inner_digits: [u8; 9],
72}
73
74impl SIN {
75    /// Parses a SIN from a string.
76    ///
77    /// ## Examples
78    /// ```
79    /// use canada_sin::SIN;
80    /// assert!(SIN::parse("046454286".to_string()).is_ok());
81    /// ```
82    pub fn parse(s: String) -> Result<Self, SINParseError> {
83        let mut digits = Vec::with_capacity(9);
84        for khar in s.chars() {
85            if let Some(digit) = khar.to_digit(10) {
86                digits.push(digit as u8);
87            };
88        }
89        match digits.len() {
90            n if n < 9 => return Err(SINParseError::TooShort),
91            n if n > 9 => return Err(SINParseError::TooLong),
92            9 => {
93                // luhn checksum
94                let luhn_sum: u8 = digits
95                    .iter()
96                    .enumerate()
97                    .map(|(idx, digit)| digit * (if idx % 2 == 0 { 1u8 } else { 2u8 }))
98                    .map(|val| {
99                        if val > 9 {
100                            // since 16 turns into 1 + 6, and the max value we will se here is 18,
101                            // this will always give the right value
102                            (val % 10) + 1
103                        } else {
104                            val
105                        }
106                    })
107                    .sum();
108                if dbg!(luhn_sum) % 10 != 0 {
109                    return Err(SINParseError::InvalidChecksum);
110                }
111            }
112            _ => unreachable!(),
113        };
114        let boxed_digits = digits.into_boxed_slice();
115        let boxing_result: Result<Box<[u8; 9]>, _> = boxed_digits.try_into();
116        match boxing_result {
117            Ok(val) => Ok(Self { inner_digits: *val }),
118            Err(_) => unreachable!(),
119        }
120    }
121    /// All types the SIN *could* be. This will often be multiple options, since this is based on
122    /// the first digit, and we are running out of numbers, so there is some overlap. However, the
123    /// following can be determined unambiguously:
124    /// - `CRAAssigned` (starts with 0)
125    /// - `TemporaryResident` (starts with 9)
126    /// - `Quebec` (starts with 2 or 3)
127    /// - `BusinessNumber` sometimes (if it starts with 8 it's a business number, if it starts with 7 it *might* be one)
128    ///
129    /// The logic is based on [this mapping](https://en.wikipedia.org/wiki/Social_Insurance_Number#Geography).
130    pub fn types(&self) -> Vec<SINType> {
131        use SINType::*;
132        match self.inner_digits[0] {
133            0 => vec![CRAAssigned],
134            1 => vec![
135                NovaScotia,
136                NewBrunswick,
137                PrinceEdwardIsland,
138                NewfoundlandLabrador,
139            ],
140            2 | 3 => vec![Quebec],
141            4 | 5 => vec![Ontario, OverseasForces],
142            6 => vec![
143                Ontario,
144                Manitoba,
145                Saskatchewan,
146                Alberta,
147                NorthwestTerritories,
148                Nunavut,
149            ],
150            7 => vec![BritishColumbia, Yukon, BusinessNumber],
151            8 => vec![BusinessNumber],
152            9 => vec![TemporaryResident],
153            _ => unreachable!(),
154        }
155    }
156    /// Returns the parsed digits as an array of digits.
157    pub fn digits(self) -> [u8; 9] {
158        self.inner_digits
159    }
160    fn gen_sin_string_part(part: &[u8]) -> String {
161        part.iter().map(|d| d.to_string()).collect::<String>()
162    }
163    /// Returns the SIN as a string.
164    ///
165    /// ## Examples
166    /// ```
167    /// use canada_sin::SIN;
168    /// let sin = SIN::parse("046454286".to_string()).unwrap();
169    /// assert_eq!(sin.digits_string(), "046454286")
170    /// ```
171    pub fn digits_string(self) -> String {
172        Self::gen_sin_string_part(&self.inner_digits)
173    }
174    /// Returns the SIN as a string with dashes in it.
175    /// ## Examples
176    /// ```
177    /// use canada_sin::SIN;
178    /// let sin = SIN::parse("046454286".to_string()).unwrap();
179    /// assert_eq!(sin.digits_dashed_string(), "046-454-286")
180    /// ```
181    pub fn digits_dashed_string(self) -> String {
182        format!(
183            "{}-{}-{}",
184            Self::gen_sin_string_part(&self.inner_digits[0..3]),
185            Self::gen_sin_string_part(&self.inner_digits[3..6]),
186            Self::gen_sin_string_part(&self.inner_digits[6..9]),
187        )
188    }
189}
190
191impl fmt::Display for SIN {
192    /// Formats the SIN into three parts with dashes.
193    ///
194    /// ## Examples
195    /// ```
196    /// use canada_sin::SIN;
197    /// assert_eq!(
198    ///     format!("Your SIN is {}.", SIN::parse("046454286".to_string()).unwrap()),
199    ///     "Your SIN is 046-454-286.".to_string(),
200    /// );
201    /// ```
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        write!(f, "{}", self.digits_dashed_string())
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn parse_sin_checks_luhn() {
213        assert_eq!(
214            SIN::parse("123456789".to_string()),
215            Err(SINParseError::InvalidChecksum)
216        );
217        assert_eq!(
218            SIN::parse("425453457".to_string()),
219            Err(SINParseError::InvalidChecksum)
220        );
221        assert_eq!(
222            SIN::parse("759268676".to_string()),
223            Err(SINParseError::InvalidChecksum)
224        );
225        assert_eq!(
226            SIN::parse("635563453".to_string()),
227            Err(SINParseError::InvalidChecksum)
228        );
229        // make sure this doesn't cause an overflow
230        assert_eq!(
231            SIN::parse("999999999".to_string()),
232            Err(SINParseError::InvalidChecksum)
233        );
234        assert!(SIN::parse("046454286".to_string()).is_ok());
235        assert!(SIN::parse("000000000".to_string()).is_ok());
236    }
237
238    #[test]
239    fn parse_sin_checks_too_short() {
240        assert_eq!(
241            SIN::parse("12345678".to_string()),
242            Err(SINParseError::TooShort)
243        );
244        assert_eq!(SIN::parse("123".to_string()), Err(SINParseError::TooShort));
245        assert_eq!(SIN::parse("0".to_string()), Err(SINParseError::TooShort));
246        assert_eq!(SIN::parse("".to_string()), Err(SINParseError::TooShort));
247    }
248
249    #[test]
250    fn parse_sin_checks_too_long() {
251        assert_eq!(
252            SIN::parse("0000000000".to_string()),
253            Err(SINParseError::TooLong)
254        );
255        assert_eq!(
256            SIN::parse("4324234237".to_string()),
257            Err(SINParseError::TooLong)
258        );
259        assert_eq!(
260            SIN::parse("635462452452344343".to_string()),
261            Err(SINParseError::TooLong)
262        );
263        assert_eq!(
264            SIN::parse("999999999999999999999999999".to_string()),
265            Err(SINParseError::TooLong)
266        );
267        assert_eq!(
268            SIN::parse("000000000000000000000000000".to_string()),
269            Err(SINParseError::TooLong)
270        );
271        assert_eq!(
272            SIN::parse("543537672346234345464254235".to_string()),
273            Err(SINParseError::TooLong)
274        );
275    }
276
277    #[test]
278    fn digits_string() {
279        let sin = SIN::parse("000-000-000".to_string()).unwrap();
280        assert_eq!(sin.digits_string(), "000000000");
281        let sin = SIN::parse("999999998".to_string()).unwrap();
282        assert_eq!(sin.digits_string(), "999999998");
283    }
284
285    #[test]
286    fn digits_dashed_string() {
287        let sin = SIN::parse("000-000-000".to_string()).unwrap();
288        assert_eq!(sin.digits_dashed_string(), "000-000-000");
289        let sin = SIN::parse("999999998".to_string()).unwrap();
290        assert_eq!(sin.digits_dashed_string(), "999-999-998");
291    }
292}