use serde::{Deserialize, Serialize};
use std::fmt;
pub mod from_str;
pub mod parse_error;
pub mod testable;
pub use testable::*;
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Serialize, Deserialize)]
pub struct NHSNumber {
pub digits: [i8; 10],
}
impl NHSNumber {
#[allow(dead_code)]
pub fn new(digits: [i8; 10]) -> Self {
NHSNumber { digits }
}
#[allow(dead_code)]
pub fn check_digit(&self) -> i8 {
crate::check_digit(self.digits)
}
#[allow(dead_code)]
pub fn calculate_check_digit(&self) -> i8 {
crate::calculate_check_digit(self.digits)
}
#[allow(dead_code)]
pub fn validate_check_digit(&self) -> bool {
crate::validate_check_digit(self.digits)
}
#[allow(dead_code)]
pub fn testable_random_sample() -> NHSNumber {
crate::testable_random_sample()
}
}
impl fmt::Display for NHSNumber {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"{}{}{} {}{}{} {}{}{}{}",
self.digits[0],
self.digits[1],
self.digits[2],
self.digits[3],
self.digits[4],
self.digits[5],
self.digits[6],
self.digits[7],
self.digits[8],
self.digits[9],
)
}
}
impl From<NHSNumber> for String {
fn from(n: NHSNumber) -> String {
n.to_string()
}
}
#[allow(dead_code)]
pub fn format(digits: [i8; 10]) -> String {
format!(
"{}{}{} {}{}{} {}{}{}{}",
digits[0],
digits[1],
digits[2],
digits[3],
digits[4],
digits[5],
digits[6],
digits[7],
digits[8],
digits[9],
)
}
#[allow(dead_code)]
pub fn check_digit(digits: [i8; 10]) -> i8 {
digits[9]
}
#[allow(dead_code)]
pub fn calculate_check_digit(digits: [i8; 10]) -> i8 {
let sum: usize = digits
.iter()
.take(9)
.enumerate()
.map(|(i, &d)| d as usize * (10 - i))
.sum();
let raw = 11 - (sum % 11);
if raw == 11 { 0 } else { raw as i8 }
}
#[allow(dead_code)]
pub fn validate_check_digit(digits: [i8; 10]) -> bool {
crate::check_digit(digits) == crate::calculate_check_digit(digits)
}
#[cfg(test)]
mod tests {
mod structure {
use super::super::*;
#[test]
fn test_new() {
let a: NHSNumber = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let actual = a.to_string();
let expect = "012 345 6789";
assert_eq!(actual, expect);
}
#[test]
fn test_new_preserves_digits() {
let digits = [9, 4, 3, 4, 7, 6, 5, 9, 1, 9];
let n = NHSNumber::new(digits);
assert_eq!(n.digits, digits);
}
#[test]
fn test_struct_literal_construction() {
let a = NHSNumber {
digits: [9, 9, 9, 1, 0, 0, 0, 0, 0, 3],
};
let b = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, 3]);
assert_eq!(a, b);
}
#[test]
fn test_display() {
let a: NHSNumber = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let actual = a.to_string();
let expect = "012 345 6789";
assert_eq!(actual, expect);
}
#[test]
fn test_display_length_is_always_twelve() {
for first in 0..=9 {
let n = NHSNumber::new([first as i8; 10]);
assert_eq!(n.to_string().chars().count(), 12);
}
}
#[test]
fn test_display_spaces_at_positions_3_and_7() {
let n = NHSNumber::new([9, 4, 3, 4, 7, 6, 5, 9, 1, 9]);
let s = n.to_string();
let bytes = s.as_bytes();
assert_eq!(bytes[3], b' ');
assert_eq!(bytes[7], b' ');
for (i, b) in bytes.iter().enumerate() {
if i == 3 || i == 7 {
continue;
}
assert!(b.is_ascii_digit(), "byte at {i} should be ASCII digit");
}
}
#[test]
fn test_into_string() {
let a: NHSNumber = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let actual: String = a.into();
let expect = "012 345 6789";
assert_eq!(actual, expect);
}
#[test]
fn test_string_from() {
let n = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let actual = String::from(n);
let expect = "012 345 6789";
assert_eq!(actual, expect);
}
#[test]
fn test_into_string_agrees_with_display() {
let n = NHSNumber::new([9, 4, 3, 4, 7, 6, 5, 9, 1, 9]);
let via_into: String = n.into();
let via_display = n.to_string();
assert_eq!(via_into, via_display);
}
#[test]
fn test_string_from_agrees_with_display_and_into() {
let n = NHSNumber::new([9, 4, 3, 4, 7, 6, 5, 9, 1, 9]);
let via_from = String::from(n);
let via_into: String = n.into();
let via_display = n.to_string();
assert_eq!(via_from, via_display);
assert_eq!(via_into, via_display);
assert_eq!(via_from, via_into);
}
#[test]
fn test_partial_eq() {
{
let a: NHSNumber = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let b: NHSNumber = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
assert_eq!(a, b);
}
{
let a: NHSNumber = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let b: NHSNumber = NHSNumber::new([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]);
assert_ne!(a, b);
}
}
#[test]
fn test_partial_eq_per_position() {
let base = [0i8; 10];
for i in 0..10 {
let mut other = base;
other[i] = 1;
assert_ne!(NHSNumber::new(base), NHSNumber::new(other));
}
}
#[test]
fn test_check_digit() {
let a = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
let actual: i8 = a.check_digit();
let expect: i8 = 9;
assert_eq!(actual, expect);
}
#[test]
fn test_check_digit_reads_tenth_position() {
for tenth in 0i8..=9 {
let n = NHSNumber::new([0, 0, 0, 0, 0, 0, 0, 0, 0, tenth]);
assert_eq!(n.check_digit(), tenth);
}
}
#[test]
fn test_calculate_check_digit() {
let a: NHSNumber = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, 3]);
assert_eq!(a.calculate_check_digit(), 3);
let b: NHSNumber = NHSNumber::new([9, 4, 3, 4, 7, 6, 5, 9, 1, 9]);
assert_eq!(b.calculate_check_digit(), 9);
let c: NHSNumber = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 1, 0, 0]);
assert_eq!(c.calculate_check_digit(), 0);
let d: NHSNumber = NHSNumber::new([9, 9, 9, 1, 2, 3, 4, 5, 6, 0]);
assert_eq!(d.calculate_check_digit(), 10);
}
#[test]
fn test_calculate_check_digit_ignores_tenth_position() {
for stored in 0i8..=9 {
let n = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, stored]);
assert_eq!(n.calculate_check_digit(), 3);
}
}
#[test]
fn test_validate_check_digit() {
{
let a: NHSNumber = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, 3]);
assert!(a.validate_check_digit());
}
{
let a: NHSNumber = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, 4]);
assert!(!a.validate_check_digit());
}
{
for stored in 0i8..=9 {
let n = NHSNumber::new([9, 9, 9, 1, 2, 3, 4, 5, 6, stored]);
assert!(
!n.validate_check_digit(),
"999 123 456{stored} must be invalid (sum % 11 == 1)"
);
}
}
}
#[test]
fn test_testable_random_sample() {
let a: NHSNumber = NHSNumber::testable_random_sample();
assert!(a >= *crate::testable::TESTABLE_MIN);
assert!(a <= *crate::testable::TESTABLE_MAX);
}
#[test]
fn test_testable_random_sample_starts_with_999() {
for _ in 0..32 {
let n = NHSNumber::testable_random_sample();
assert_eq!(&n.digits[0..3], &[9, 9, 9]);
}
}
}
mod utilities {
#[test]
fn test_format() {
let digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let actual = crate::format(digits);
let expect = "012 345 6789";
assert_eq!(actual, expect);
}
#[test]
fn test_format_all_zeros() {
assert_eq!(crate::format([0; 10]), "000 000 0000");
}
#[test]
fn test_format_all_nines() {
assert_eq!(crate::format([9; 10]), "999 999 9999");
}
#[test]
fn test_check_digit() {
let digits = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
let actual: i8 = crate::check_digit(digits);
let expect: i8 = 9;
assert_eq!(actual, expect);
}
#[test]
fn test_calculate_check_digit() {
let digits = [9, 9, 9, 1, 0, 0, 0, 0, 0, 3];
assert_eq!(crate::calculate_check_digit(digits), 3);
let digits = [9, 9, 9, 1, 0, 0, 0, 1, 0, 0];
assert_eq!(crate::calculate_check_digit(digits), 0);
let digits = [9, 9, 9, 1, 2, 3, 4, 5, 6, 0];
assert_eq!(crate::calculate_check_digit(digits), 10);
}
#[test]
fn test_calculate_check_digit_all_zeros() {
assert_eq!(crate::calculate_check_digit([0; 10]), 0);
}
#[test]
fn test_validate_check_digit_free_fn() {
assert!(crate::validate_check_digit([9, 9, 9, 1, 0, 0, 0, 0, 0, 3]));
assert!(!crate::validate_check_digit([9, 9, 9, 1, 0, 0, 0, 0, 0, 4]));
}
}
mod properties {
use super::super::*;
use std::str::FromStr;
fn sample_fixtures() -> Vec<NHSNumber> {
vec![
NHSNumber::new([0; 10]),
NHSNumber::new([9; 10]),
NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
NHSNumber::new([9, 8, 7, 6, 5, 4, 3, 2, 1, 0]),
NHSNumber::new([9, 4, 3, 4, 7, 6, 5, 9, 1, 9]), NHSNumber::new([9, 8, 7, 6, 5, 4, 4, 3, 2, 1]), NHSNumber::new([9, 9, 9, 0, 0, 0, 0, 0, 0, 0]), NHSNumber::new([9, 9, 9, 9, 9, 9, 9, 9, 9, 9]), NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, 3]), ]
}
#[test]
fn round_trip_via_canonical_form() {
for n in sample_fixtures() {
let s = n.to_string();
let parsed = NHSNumber::from_str(&s).expect("display form must parse");
assert_eq!(parsed, n, "round-trip failed for {s}");
}
}
#[test]
fn round_trip_via_tight_form() {
for n in sample_fixtures() {
let tight: String = n.digits.iter().map(|d| (b'0' + *d as u8) as char).collect();
let parsed = NHSNumber::from_str(&tight).expect("tight form must parse");
assert_eq!(parsed, n, "round-trip failed for {tight}");
}
}
#[test]
fn method_and_free_fn_format_agree() {
for n in sample_fixtures() {
assert_eq!(n.to_string(), crate::format(n.digits));
}
}
#[test]
fn method_and_free_fn_check_digit_agree() {
for n in sample_fixtures() {
assert_eq!(n.check_digit(), crate::check_digit(n.digits));
}
}
#[test]
fn method_and_free_fn_calculate_check_digit_agree() {
for n in sample_fixtures() {
assert_eq!(
n.calculate_check_digit(),
crate::calculate_check_digit(n.digits)
);
}
}
#[test]
fn method_and_free_fn_validate_check_digit_agree() {
for n in sample_fixtures() {
assert_eq!(
n.validate_check_digit(),
crate::validate_check_digit(n.digits)
);
}
}
#[test]
fn calculate_check_digit_is_in_valid_range() {
for n in sample_fixtures() {
let c = n.calculate_check_digit();
assert!((0..=10).contains(&c), "{c} out of [0..=10]");
}
}
}
mod boundaries {
use super::super::*;
#[test]
fn sum_mod_11_eq_0_yields_check_digit_zero() {
let digits = [9, 9, 9, 1, 0, 0, 0, 1, 0, 0];
assert_eq!(crate::calculate_check_digit(digits), 0);
}
#[test]
fn sum_mod_11_eq_1_yields_sentinel_ten() {
let digits = [9, 9, 9, 1, 2, 3, 4, 5, 6, 0];
assert_eq!(crate::calculate_check_digit(digits), 10);
}
#[test]
fn sum_mod_11_in_2_to_10_yields_eleven_minus_remainder() {
for d8 in 0i8..=9 {
let digits = [0, 0, 0, 0, 0, 0, 0, 0, d8, 0];
let sum = d8 as usize * 2;
let raw = 11 - (sum % 11);
let expected = if raw == 11 {
0
} else if raw == 10 {
10
} else {
raw as i8
};
assert_eq!(
crate::calculate_check_digit(digits),
expected,
"for d[8]={d8}"
);
}
}
#[test]
fn all_zeros_round_trips() {
let zeros = NHSNumber::new([0; 10]);
assert_eq!(zeros.to_string(), "000 000 0000");
assert!(zeros.validate_check_digit());
}
#[test]
fn all_nines_round_trips() {
let nines = NHSNumber::new([9; 10]);
assert_eq!(nines.to_string(), "999 999 9999");
assert_eq!(nines.calculate_check_digit(), 9);
assert!(nines.validate_check_digit());
}
}
mod ordering {
use super::super::*;
use std::collections::{BTreeMap, BTreeSet};
#[test]
fn ord_matches_numeric_intuition() {
let lo = NHSNumber::new([0; 10]);
let mid = NHSNumber::new([5; 10]);
let hi = NHSNumber::new([9; 10]);
assert!(lo < mid);
assert!(mid < hi);
assert!(lo < hi);
}
#[test]
fn ord_breaks_ties_left_to_right() {
let a = NHSNumber::new([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]);
let b = NHSNumber::new([1, 2, 3, 4, 5, 7, 7, 8, 9, 0]);
assert!(a < b);
}
#[test]
fn vec_sort_is_ascending() {
let mut v = vec![
NHSNumber::new([9; 10]),
NHSNumber::new([0; 10]),
NHSNumber::new([5; 10]),
];
v.sort();
assert_eq!(
v,
vec![
NHSNumber::new([0; 10]),
NHSNumber::new([5; 10]),
NHSNumber::new([9; 10]),
]
);
}
#[test]
fn btreeset_dedups_and_orders() {
let mut set = BTreeSet::new();
set.insert(NHSNumber::new([9; 10]));
set.insert(NHSNumber::new([0; 10]));
set.insert(NHSNumber::new([0; 10])); assert_eq!(set.len(), 2);
let first = set.iter().next().copied().unwrap();
assert_eq!(first, NHSNumber::new([0; 10]));
}
#[test]
fn btreemap_use_as_key() {
let mut map: BTreeMap<NHSNumber, &'static str> = BTreeMap::new();
map.insert(NHSNumber::new([0; 10]), "min");
map.insert(NHSNumber::new([9; 10]), "max");
assert_eq!(map.get(&NHSNumber::new([0; 10])), Some(&"min"));
assert_eq!(map.get(&NHSNumber::new([9; 10])), Some(&"max"));
}
}
mod traits {
use super::super::*;
fn assert_copy<T: Copy>() {}
fn assert_clone<T: Clone>() {}
fn assert_send_sync<T: Send + Sync>() {}
fn assert_serde<T: serde::Serialize + serde::de::DeserializeOwned>() {}
#[test]
fn nhs_number_is_copy_clone_send_sync_serde() {
assert_copy::<NHSNumber>();
assert_clone::<NHSNumber>();
assert_send_sync::<NHSNumber>();
assert_serde::<NHSNumber>();
}
#[test]
fn copy_does_not_move() {
let a = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, 3]);
let b = a;
assert_eq!(a, b);
}
#[test]
fn clone_produces_equal_value() {
let a = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, 3]);
#[allow(clippy::clone_on_copy)]
let b = a.clone();
assert_eq!(a, b);
}
#[test]
fn debug_format_is_non_empty() {
let n = NHSNumber::new([9, 9, 9, 1, 0, 0, 0, 0, 0, 3]);
let dbg = format!("{n:?}");
assert!(dbg.contains("NHSNumber"));
assert!(dbg.contains("digits"));
}
}
}