use crate::NHSNumber;
use crate::parse_error::ParseError;
use std::str::FromStr;
impl FromStr for NHSNumber {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = s.as_bytes();
fn digit(b: u8) -> Result<i8, ParseError> {
if b.is_ascii_digit() {
Ok((b - b'0') as i8)
} else {
Err(ParseError)
}
}
match bytes.len() {
10 => {
let mut digits: [i8; 10] = [0; 10];
for i in 0..10 {
digits[i] = digit(bytes[i])?
}
Ok(NHSNumber { digits })
}
12 => {
if bytes[3] != b' ' || bytes[7] != b' ' {
return Err(ParseError);
}
let mut digits: [i8; 10] = [0; 10];
for i in 0..3 {
digits[i] = digit(bytes[i])?
}
for i in 0..3 {
digits[i + 3] = digit(bytes[i + 4])?
}
for i in 0..4 {
digits[i + 6] = digit(bytes[i + 8])?
}
Ok(NHSNumber { digits })
}
_ => Err(ParseError),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_from_str_with_length_10_without_spaces() {
let s = String::from("0123456789");
let actual: NHSNumber = NHSNumber::from_str(&s).unwrap();
let expect: NHSNumber = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
assert_eq!(actual, expect);
}
#[test]
fn test_from_str_with_length_12_with_spaces() {
let s = String::from("012 345 6789");
let actual: NHSNumber = NHSNumber::from_str(&s).unwrap();
let expect: NHSNumber = NHSNumber::new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
assert_eq!(actual, expect);
}
#[test]
fn test_from_str_with_str_parse_method() {
let n: NHSNumber = "999 100 0003".parse().unwrap();
assert_eq!(n.digits, [9, 9, 9, 1, 0, 0, 0, 0, 0, 3]);
}
#[test]
fn test_from_str_all_zeros_both_forms() {
let tight = NHSNumber::from_str("0000000000").unwrap();
let canonical = NHSNumber::from_str("000 000 0000").unwrap();
assert_eq!(tight, canonical);
assert_eq!(tight.digits, [0; 10]);
}
#[test]
fn test_from_str_all_nines_both_forms() {
let tight = NHSNumber::from_str("9999999999").unwrap();
let canonical = NHSNumber::from_str("999 999 9999").unwrap();
assert_eq!(tight, canonical);
assert_eq!(tight.digits, [9; 10]);
}
#[test]
fn test_from_str_round_trips_via_display() {
let fixtures = [
NHSNumber::new([0; 10]),
NHSNumber::new([9; 10]),
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, 1, 0, 0, 0, 0, 0, 3]),
];
for n in fixtures {
let s = n.to_string();
let parsed = NHSNumber::from_str(&s).unwrap();
assert_eq!(parsed, n);
}
}
#[test]
fn test_from_str_with_wrong_characters() {
let s = String::from("012-345-6789");
let result: Result<NHSNumber, ParseError> = NHSNumber::from_str(&s);
assert!(result.is_err());
}
#[test]
fn test_from_str_with_wrong_leading_space() {
let s = String::from(" 012 345 6789");
let result: Result<NHSNumber, ParseError> = NHSNumber::from_str(&s);
assert!(result.is_err());
}
#[test]
fn test_from_str_with_first_space_without_last_space() {
let s = String::from("012 3456789");
let result: Result<NHSNumber, ParseError> = NHSNumber::from_str(&s);
assert!(result.is_err());
}
#[test]
fn test_from_str_without_first_space_with_last_space() {
let s = String::from("012345 6789");
let result: Result<NHSNumber, ParseError> = NHSNumber::from_str(&s);
assert!(result.is_err());
}
#[test]
fn test_from_str_with_wrong_inner_space() {
let s = String::from("012 345 6789");
let result: Result<NHSNumber, ParseError> = NHSNumber::from_str(&s);
assert!(result.is_err());
}
#[test]
fn test_from_str_with_wrong_trailing_space() {
let s = String::from("012 345 6789 ");
let result: Result<NHSNumber, ParseError> = NHSNumber::from_str(&s);
assert!(result.is_err());
}
#[test]
fn test_from_str_with_wrong_length() {
let s = String::from("012");
let result: Result<NHSNumber, ParseError> = NHSNumber::from_str(&s);
assert!(result.is_err());
}
#[test]
fn test_from_str_with_empty_string() {
assert!(NHSNumber::from_str("").is_err());
}
#[test]
fn test_from_str_with_length_11() {
assert!(NHSNumber::from_str("01234567890").is_err());
assert!(NHSNumber::from_str("01234 56789").is_err());
}
#[test]
fn test_from_str_with_length_13() {
assert!(NHSNumber::from_str("01234567890ab").is_err());
assert!(NHSNumber::from_str("0123 4567 8901").is_err());
}
#[test]
fn test_from_str_with_letters_only() {
assert!(NHSNumber::from_str("abcdefghij").is_err());
assert!(NHSNumber::from_str("abc def ghij").is_err());
}
#[test]
fn test_from_str_with_letters_mixed_in() {
assert!(NHSNumber::from_str("abc 123 4567").is_err());
assert!(NHSNumber::from_str("012 345 abcd").is_err());
}
#[test]
fn test_from_str_with_nbsp_separators() {
let s = "012\u{00A0}345\u{00A0}6789";
assert_eq!(s.chars().count(), 12);
assert!(NHSNumber::from_str(s).is_err());
}
#[test]
fn test_from_str_with_tab_separators() {
let s = "012\t345\t6789";
assert_eq!(s.chars().count(), 12);
assert!(NHSNumber::from_str(s).is_err());
}
#[test]
fn test_from_str_with_arabic_indic_digits() {
let s = "٠١٢٣٤٥٦٧٨٩";
assert_eq!(s.chars().count(), 10);
assert!(NHSNumber::from_str(s).is_err());
}
#[test]
fn test_from_str_with_negative_sign() {
assert!(NHSNumber::from_str("-123456789").is_err());
}
#[test]
fn test_from_str_with_plus_sign() {
assert!(NHSNumber::from_str("+123456789").is_err());
}
#[test]
fn test_from_str_with_decimal_point() {
assert!(NHSNumber::from_str("12345.6789").is_err());
}
#[test]
fn test_from_str_with_emoji() {
assert!(NHSNumber::from_str("0123🦀5678").is_err());
}
#[test]
fn test_from_str_error_value_equals_parse_error() {
let a = NHSNumber::from_str("bad").unwrap_err();
let b = NHSNumber::from_str("also bad").unwrap_err();
assert_eq!(a, b);
assert_eq!(a, ParseError);
}
#[test]
fn test_from_str_with_embedded_nul_bytes() {
assert!(NHSNumber::from_str("012345678\0").is_err());
assert!(NHSNumber::from_str("\u{0}123456789").is_err());
assert!(NHSNumber::from_str("012 345 678\0").is_err());
}
#[test]
fn test_from_str_with_control_characters() {
assert!(NHSNumber::from_str("01234567\r\n").is_err());
assert!(NHSNumber::from_str("012 345 6789\n").is_err());
assert!(NHSNumber::from_str("\u{8}123456789").is_err()); assert!(NHSNumber::from_str("\u{7f}123456789").is_err()); }
#[test]
fn test_from_str_with_bidi_override_characters() {
assert!(NHSNumber::from_str("\u{202E}0123456789").is_err());
assert!(NHSNumber::from_str("0123456789\u{202E}").is_err());
}
#[test]
fn test_from_str_with_multibyte_char_at_accepted_byte_length() {
let s = "12345678é";
assert_eq!(s.len(), 10);
assert!(NHSNumber::from_str(s).is_err());
let s = "123 456 78é9";
assert!(NHSNumber::from_str(s).is_err());
}
#[test]
fn test_from_str_with_full_width_digits() {
let s = "0123456789";
assert_eq!(s.chars().count(), 10);
assert!(NHSNumber::from_str(s).is_err());
}
#[test]
fn test_from_str_with_superscript_and_lookalike_digits() {
assert!(NHSNumber::from_str("¹²³⁴⁵⁶⁷⁸⁹⁰").is_err());
assert!(NHSNumber::from_str("0l234567l9").is_err());
assert!(NHSNumber::from_str("O123456789").is_err());
}
#[test]
fn test_from_str_with_huge_input_is_rejected_fast() {
let huge = "1".repeat(10 * 1024 * 1024);
assert!(NHSNumber::from_str(&huge).is_err());
}
#[test]
fn test_from_str_with_all_byte_lengths_up_to_32() {
for len in 0..=32 {
let s = "9".repeat(len);
let result = NHSNumber::from_str(&s);
if len == 10 {
assert!(result.is_ok(), "all-digit length {len} must parse");
} else {
assert!(result.is_err(), "length {len} must be rejected");
}
}
}
mod fuzz {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn never_panics_on_arbitrary_strings(s in "\\PC*") {
let _ = NHSNumber::from_str(&s);
}
#[test]
fn never_panics_on_length_10_and_12_strings(
s in proptest::string::string_regex(".{10}|.{12}").unwrap()
) {
let _ = NHSNumber::from_str(&s);
}
#[test]
fn accepted_input_is_canonical(s in "\\PC*") {
if let Ok(n) = NHSNumber::from_str(&s) {
for d in n.digits {
prop_assert!((0..=9).contains(&d));
}
let rendered = n.to_string();
prop_assert_eq!(NHSNumber::from_str(&rendered).unwrap(), n);
let bytes = s.as_bytes();
prop_assert!(bytes.len() == 10 || bytes.len() == 12);
}
}
#[test]
fn tight_form_always_parses(s in "[0-9]{10}") {
let n = NHSNumber::from_str(&s).unwrap();
let tight: String =
n.digits.iter().map(|d| (b'0' + *d as u8) as char).collect();
prop_assert_eq!(tight, s);
}
#[test]
fn canonical_form_round_trips(s in "[0-9]{3} [0-9]{3} [0-9]{4}") {
let n = NHSNumber::from_str(&s).unwrap();
prop_assert_eq!(n.to_string(), s);
}
#[test]
fn corrupted_canonical_form_is_rejected(
s in "[0-9]{3} [0-9]{3} [0-9]{4}",
pos in 0usize..12,
bad in "[!-/:-~]" ) {
let mut bytes = s.into_bytes();
bytes[pos] = bad.as_bytes()[0];
let corrupted = String::from_utf8(bytes).unwrap();
prop_assert!(NHSNumber::from_str(&corrupted).is_err());
}
}
}
}