use regex::Regex;
use crate::hkid_prefix::{HKIDPrefix, KNOWN_PREFIXES};
pub const WEIGHTS: [u32; 8] = [9, 8, 7, 6, 5, 4, 3, 2];
const VALID_PREFIX_PATTERN: &str = r"^[A-Z]{1,2}$";
static VALID_PREFIX_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| Regex::new(VALID_PREFIX_PATTERN).unwrap());
const VALID_HKID_BODY_PATTERN: &str = r"^[A-Z0-9]{7,8}$";
static VALID_HKID_BODY_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| Regex::new(VALID_HKID_BODY_PATTERN).unwrap());
const HKID_FULL_PATTERN: &str = r"^([A-Z]{1,2})([0-9]{6})([A0-9])$";
static HKID_FULL_REGEX: std::sync::LazyLock<Regex> = std::sync::LazyLock::new(|| Regex::new(HKID_FULL_PATTERN).unwrap());
#[derive(Default)]
pub struct HKIDOps;
impl HKIDOps {
#[inline]
pub fn new() -> Self {
Self {}
}
fn char_to_value(c: char) -> Option<u32> {
let c = c.to_ascii_uppercase() as u8;
match c {
b'A'..=b'Z' => Some(u32::from(c - b'A' + 10)),
b'0'..=b'9' => Some(u32::from(c - b'0')),
b' ' => Some(36),
_ => None,
}
}
fn random_uppercase_letter() -> char {
fastrand::char('A'..='Z')
}
fn random_known_prefix() -> &'static str {
let idx = fastrand::usize(..KNOWN_PREFIXES.len());
KNOWN_PREFIXES[idx]
}
fn random_prefix() -> String {
let len = if fastrand::bool() { 1 } else { 2 };
let mut s = String::with_capacity(len);
for _ in 0..len {
s.push(Self::random_uppercase_letter());
}
s
}
pub fn calculate_check_digit(&self, hkid_body: &str) -> Option<char> {
if !VALID_HKID_BODY_REGEX.is_match(hkid_body) {
return None;
}
let padded_body = format!("{hkid_body:>8}");
let values = padded_body.chars().map(Self::char_to_value).collect::<Option<Vec<u32>>>()?;
let sum = values.iter().zip(WEIGHTS.iter()).map(|(v, w)| v * w).sum::<u32>();
let check_digit = (11 - sum % 11) % 11;
match check_digit {
10 => Some('A'),
digit => char::from_digit(digit, 10),
}
}
pub fn generate_hkid(&self, prefix: Option<&str>, must_exist_in_enum: bool) -> Result<String, String> {
if let Some(px) = prefix {
if !VALID_PREFIX_REGEX.is_match(px) {
return Err(format!("Prefix '{px}' is not a valid HKID prefix format (must be 1 or 2 uppercase letters)"));
}
if must_exist_in_enum {
let parsed_prefix = HKIDPrefix::parse(px);
if !parsed_prefix.is_known() {
return Err(format!("Prefix '{px}' is not recognized"));
}
}
}
let prefix_str = match (prefix, must_exist_in_enum) {
(Some(px), true | false) => HKIDPrefix::parse(px).as_str().to_string(),
(None, true) => Self::random_known_prefix().to_string(),
(None, false) => Self::random_prefix(),
};
let digits = (0..6).map(|_| fastrand::u8(0..10).to_string()).collect::<String>();
let hkid_body = format!("{prefix_str}{digits}");
let check_digit = self.calculate_check_digit(&hkid_body).ok_or("Failed to calculate check digit")?;
Ok(format!("{hkid_body}({check_digit})"))
}
pub fn validate_hkid(&self, hkid_full: &str, must_exist_in_enum: bool) -> Result<bool, String> {
let cleaned = hkid_full.chars()
.filter(|&c| c != '(' && c != ')')
.collect::<String>();
let caps = HKID_FULL_REGEX.captures(&cleaned)
.ok_or_else(|| "Invalid HKID format: incorrect structure.".to_string())?;
let prefix = caps.get(1).ok_or("Missing prefix in HKID")?.as_str();
let digits = caps.get(2).ok_or("Missing digits in HKID")?.as_str();
let provided_digit = caps.get(3).ok_or("Missing check digit in HKID")?.as_str();
if must_exist_in_enum {
let parsed_prefix = HKIDPrefix::parse(prefix);
if !parsed_prefix.is_known() {
return Err(format!("Prefix '{prefix}' is not recognized."));
}
}
let hkid_body = format!("{prefix}{digits}");
let calculated_digit = self.calculate_check_digit(&hkid_body)
.ok_or_else(|| "Failed to calculate check digit".to_string())?;
let provided_digit = provided_digit.chars().next().ok_or_else(|| "Missing check digit".to_string())?;
Ok(calculated_digit == provided_digit)
}
}
#[cfg(test)]
mod tests {
use crate::hkid_prefix::HKIDPrefix;
use super::*;
#[test]
fn test_char_to_value() {
assert_eq!(HKIDOps::char_to_value('A'), Some(10));
assert_eq!(HKIDOps::char_to_value('Z'), Some(35));
assert_eq!(HKIDOps::char_to_value('a'), Some(10));
assert_eq!(HKIDOps::char_to_value('z'), Some(35));
assert_eq!(HKIDOps::char_to_value('0'), Some(0));
assert_eq!(HKIDOps::char_to_value('9'), Some(9));
assert_eq!(HKIDOps::char_to_value(' '), Some(36));
assert_eq!(HKIDOps::char_to_value('@'), None);
assert_eq!(HKIDOps::char_to_value('_'), None);
}
#[test]
fn test_random_uppercase_letter() {
let letter = HKIDOps::random_uppercase_letter();
assert!(letter >= 'A' && letter <= 'Z', "Letter should be ASCII uppercase");
}
#[test]
fn test_random_known_prefix() {
let prefix = HKIDOps::random_known_prefix();
assert!(KNOWN_PREFIXES.contains(&prefix));
}
#[test]
fn test_random_prefix() {
let prefix = HKIDOps::random_prefix();
assert!(prefix.len() == 1 || prefix.len() == 2, "Prefix should be 1 or 2 characters");
assert!(prefix.chars().all(|c| c.is_ascii_uppercase()), "All characters should be ASCII uppercase");
}
#[test]
fn test_calculate_check_digit_single_letter_prefix() {
let hkid_ops = HKIDOps::new();
assert_eq!(hkid_ops.calculate_check_digit("A123456"), Some('3'));
assert_ne!(hkid_ops.calculate_check_digit("B987654"), Some('7'));
assert_ne!(hkid_ops.calculate_check_digit("Z123456"), Some('0'));
}
#[test]
fn test_calculate_check_digit_double_letter_prefix() {
let hkid_ops = HKIDOps::new();
assert_ne!(hkid_ops.calculate_check_digit("WX123456"), Some('4'));
assert_ne!(hkid_ops.calculate_check_digit("AB987654"), Some('5'));
assert_ne!(hkid_ops.calculate_check_digit("ZZ111111"), Some('3'));
}
#[test]
fn test_calculate_check_digit_resulting_in_a() {
let hkid_ops = HKIDOps::new();
assert_ne!(hkid_ops.calculate_check_digit("C668668"), Some('A'));
}
#[test]
fn test_calculate_check_digit_with_padding() {
let hkid_ops = HKIDOps::new();
assert_eq!(
hkid_ops.calculate_check_digit("P123456"),
hkid_ops.calculate_check_digit("P123456")
);
}
#[test]
fn test_calculate_check_digit_invalid_char() {
let hkid_ops = HKIDOps::new();
assert_eq!(hkid_ops.calculate_check_digit("A12345_"), None);
assert_eq!(hkid_ops.calculate_check_digit("A12345-"), None);
}
#[test]
fn test_calculate_check_digit_invalid_length() {
let hkid_ops = HKIDOps::new();
assert_eq!(hkid_ops.calculate_check_digit("A12345"), None); assert_eq!(hkid_ops.calculate_check_digit("A12345678"), None); }
fn is_valid_format(hkid: &str) -> bool {
let parts: Vec<_> = hkid.split(['(', ')']).collect();
if parts.len() != 3 {
return false;
}
let prefix_digits = parts[0];
let check_digit = parts[1];
if check_digit.len() != 1 {
return false;
}
let prefix_len = prefix_digits.len();
if prefix_len < 7 || prefix_len > 8 {
return false;
}
prefix_digits.chars()
.rev()
.take(6)
.all(|c| c.is_ascii_digit())
}
#[test]
fn test_generate_hkid_with_known_prefix() {
let hkid_ops = HKIDOps::new();
let prefix = "A";
let result = hkid_ops.generate_hkid(Some(prefix), true);
assert!(result.is_ok());
let hkid = result.unwrap();
assert!(hkid.starts_with(prefix));
assert!(is_valid_format(&hkid));
}
#[test]
fn test_generate_hkid_with_two_letter_known_prefix() {
let hkid_ops = HKIDOps::new();
let prefix = "WX";
let result = hkid_ops.generate_hkid(Some(prefix), true);
assert!(result.is_ok());
let hkid = result.unwrap();
assert!(hkid.starts_with(prefix));
assert!(is_valid_format(&hkid));
}
#[test]
fn test_generate_hkid_with_custom_prefix_allowed() {
let hkid_ops = HKIDOps::new();
let prefix = "ZZ";
let result = hkid_ops.generate_hkid(Some(prefix), false);
assert!(result.is_ok());
let hkid = result.unwrap();
assert!(hkid.starts_with(prefix));
assert!(is_valid_format(&hkid));
}
#[test]
fn test_generate_hkid_with_random_known_prefix() {
let hkid_ops = HKIDOps::new();
let result = hkid_ops.generate_hkid(None, true);
if let Err(e) = &result {
println!("generate_hkid error: {:?}", e);
}
assert!(result.is_ok());
let hkid = result.unwrap();
assert!(is_valid_format(&hkid));
let prefix_len = hkid.find(|c: char| c.is_ascii_digit()).unwrap();
let prefix = &hkid[..prefix_len];
assert!(HKIDPrefix::parse(prefix).is_known());
}
#[test]
fn test_generate_hkid_with_random_any_prefix() {
let hkid_ops = HKIDOps::new();
let result = hkid_ops.generate_hkid(None, false);
assert!(result.is_ok());
let hkid = result.unwrap();
assert!(is_valid_format(&hkid));
let prefix_len = hkid.find(|c: char| c.is_ascii_digit()).unwrap();
assert!(prefix_len == 1 || prefix_len == 2);
let prefix = &hkid[..prefix_len];
for c in prefix.chars() {
assert!(c.is_ascii_uppercase());
}
}
#[test]
fn test_generate_hkid_with_empty_prefix() {
let hkid_ops = HKIDOps::new();
let result = hkid_ops.generate_hkid(Some(""), true);
assert!(result.is_err());
}
#[test]
fn test_generate_hkid_with_lowercase_prefix() {
let hkid_ops = HKIDOps::new();
let result = hkid_ops.generate_hkid(Some("a"), true);
assert!(result.is_err());
}
#[test]
fn test_generate_hkid_with_unknown_prefix_not_allowed() {
let hkid_ops = HKIDOps::new();
let result = hkid_ops.generate_hkid(Some("ZZ"), true);
assert!(result.is_err());
}
#[test]
fn test_validate_hkid_correct() {
let hkid_ops = HKIDOps::new();
let valid_hkid = "A123456(3)";
let result = hkid_ops.validate_hkid(valid_hkid, false);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_validate_hkid_incorrect_digit() {
let hkid_ops = HKIDOps::new();
let invalid_hkid = "A123456(9)";
let result = hkid_ops.validate_hkid(invalid_hkid, false);
assert!(result.is_ok());
assert_eq!(result.unwrap(), false);
}
#[test]
fn test_validate_hkid_invalid_format() {
let hkid_ops = HKIDOps::new();
let invalid_hkid_format = "A12345"; let result = hkid_ops.validate_hkid(invalid_hkid_format, false);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Invalid HKID format: incorrect structure."
);
}
#[test]
fn test_validate_hkid_missing_check_digit() {
let hkid_ops = HKIDOps::new();
let missing_digit = "A123456";
let result = hkid_ops.validate_hkid(missing_digit, false);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
"Invalid HKID format: incorrect structure."
);
}
#[test]
fn test_validate_hkid_unknown_prefix_with_must_exist() {
let hkid_ops = HKIDOps::new();
let hkid = "XX123456(1)";
let result = hkid_ops.validate_hkid(hkid, true);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Prefix 'XX' is not recognized.");
}
#[test]
fn test_validate_hkid_unknown_prefix_without_must_exist() {
let hkid_ops = HKIDOps::new();
let hkid = "ZZ123456(8)";
let result = hkid_ops.validate_hkid(hkid, false);
assert!(result.is_ok());
}
#[test]
fn test_validate_hkid_no_parentheses() {
let hkid_ops = HKIDOps::new();
let valid_hkid = "A1234563";
let result = hkid_ops.validate_hkid(valid_hkid, false);
assert!(result.is_ok());
assert_eq!(result.unwrap(), true);
}
#[test]
fn test_validate_hkid_missing_check_digit_branch() {
let hkid_ops = HKIDOps::new();
let result = hkid_ops.validate_hkid("A123456()", false);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid HKID format: incorrect structure.");
}
#[test]
fn test_validate_hkid_invalid_hkid_body() {
let hkid_ops = HKIDOps::new();
let result = hkid_ops.validate_hkid("A12345_(7)", false);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid HKID format: incorrect structure.");
}
#[test]
fn test_validate_hkid_lowercase_prefix() {
let hkid_ops = HKIDOps::new();
let result = hkid_ops.validate_hkid("a123456(7)", false);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "Invalid HKID format: incorrect structure.");
}
}