Skip to main content

headless_lms_models/library/credit_registration/
student_number.rs

1//! Catching typos in a student number a person types: today's University of Helsinki numbers are a
2//! `0` and eight digits, the last a check digit.
3//!
4//! Only for typed input. A number from Sisu or Suotar is an opaque identifier and is taken as it
5//! comes, since Sisu may change the format.
6
7/// Weights of the first eight digits, left to right.
8const CHECK_DIGIT_WEIGHTS: [u32; 8] = [3, 7, 1, 3, 7, 1, 3, 7];
9
10/// Why a typed student number was rejected.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum InvalidStudentNumber {
13    /// Not a `0` followed by eight digits.
14    Format,
15    /// Well formed, but the check digit does not match: most likely a typo.
16    CheckDigit,
17}
18
19impl InvalidStudentNumber {
20    /// A sentence for the person who typed the number.
21    pub fn message(self) -> &'static str {
22        match self {
23            Self::Format => "A student number is nine digits and starts with 0.",
24            Self::CheckDigit => "The check digit does not match. Check the number for typos.",
25        }
26    }
27}
28
29/// `raw` with whitespace removed, if it passes as a typed student number. Mirrored by
30/// `studentNumberProblem` in main-frontend.
31pub fn parse_student_number(raw: &str) -> Result<String, InvalidStudentNumber> {
32    let number: String = raw.chars().filter(|c| !c.is_whitespace()).collect();
33    if number.len() != 9 || !number.starts_with('0') || !number.bytes().all(|b| b.is_ascii_digit())
34    {
35        return Err(InvalidStudentNumber::Format);
36    }
37    let sum: u32 = number
38        .bytes()
39        .zip(CHECK_DIGIT_WEIGHTS)
40        .map(|(digit, weight)| u32::from(digit - b'0') * weight)
41        .sum();
42    if (10 - sum % 10) % 10 != u32::from(number.as_bytes()[8] - b'0') {
43        return Err(InvalidStudentNumber::CheckDigit);
44    }
45    Ok(number)
46}