pub mod datetime;
pub use datetime as hl7v2_datetime;
use regex::Regex;
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum DataTypeError {
#[error("Invalid data type '{datatype}': {reason}")]
InvalidDataType {
datatype: String,
reason: String,
},
#[error("Value too short: {length} < {min}")]
TooShort {
length: usize,
min: usize,
},
#[error("Value too long: {length} > {max}")]
TooLong {
length: usize,
max: usize,
},
#[error("Pattern mismatch: value '{value}' does not match pattern '{pattern}'")]
PatternMismatch {
value: String,
pattern: String,
},
#[error("Value not in allowed set: {value}")]
NotInAllowedSet {
value: String,
},
#[error("Checksum validation failed")]
ChecksumFailed,
}
pub type ValidationResult = Result<(), DataTypeError>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataType {
ST,
ID,
IS,
DT,
TM,
TS,
NM,
SI,
TX,
FT,
PN,
CX,
HD,
AD,
XTN,
}
impl DataType {
pub fn parse(s: &str) -> Option<Self> {
match s {
"ST" => Some(Self::ST),
"ID" => Some(Self::ID),
"IS" => Some(Self::IS),
"DT" => Some(Self::DT),
"TM" => Some(Self::TM),
"TS" => Some(Self::TS),
"NM" => Some(Self::NM),
"SI" => Some(Self::SI),
"TX" => Some(Self::TX),
"FT" => Some(Self::FT),
"PN" => Some(Self::PN),
"CX" => Some(Self::CX),
"HD" => Some(Self::HD),
"AD" => Some(Self::AD),
"XTN" => Some(Self::XTN),
_ => None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct DataTypeValidator {
pub min_length: Option<usize>,
pub max_length: Option<usize>,
pub pattern: Option<String>,
pub allowed_values: Option<Vec<String>>,
pub checksum: Option<ChecksumAlgorithm>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChecksumAlgorithm {
Luhn,
Mod10,
}
impl DataTypeValidator {
pub fn new() -> Self {
Self::default()
}
pub fn with_min_length(mut self, min: usize) -> Self {
self.min_length = Some(min);
self
}
pub fn with_max_length(mut self, max: usize) -> Self {
self.max_length = Some(max);
self
}
pub fn with_pattern(mut self, pattern: &str) -> Self {
self.pattern = Some(pattern.to_string());
self
}
pub fn with_allowed_values(mut self, values: Vec<String>) -> Self {
self.allowed_values = Some(values);
self
}
pub fn with_checksum(mut self, algorithm: ChecksumAlgorithm) -> Self {
self.checksum = Some(algorithm);
self
}
pub fn validate(&self, value: &str) -> bool {
self.validate_detailed(value).is_ok()
}
pub fn validate_detailed(&self, value: &str) -> ValidationResult {
if let Some(min) = self.min_length
&& value.len() < min
{
return Err(DataTypeError::TooShort {
length: value.len(),
min,
});
}
if let Some(max) = self.max_length
&& value.len() > max
{
return Err(DataTypeError::TooLong {
length: value.len(),
max,
});
}
if let Some(pattern) = &self.pattern
&& let Ok(regex) = Regex::new(pattern)
&& !regex.is_match(value)
{
return Err(DataTypeError::PatternMismatch {
value: value.to_string(),
pattern: pattern.clone(),
});
}
if let Some(allowed) = &self.allowed_values
&& !allowed.contains(&value.to_string())
{
return Err(DataTypeError::NotInAllowedSet {
value: value.to_string(),
});
}
if let Some(algorithm) = self.checksum {
match algorithm {
ChecksumAlgorithm::Luhn | ChecksumAlgorithm::Mod10 => {
if !validate_luhn_checksum(value) {
return Err(DataTypeError::ChecksumFailed);
}
}
}
}
Ok(())
}
}
pub fn validate_datatype(value: &str, datatype: &str) -> bool {
match datatype {
"ST" => is_string(value),
"ID" => is_identifier(value),
"IS" => is_coded_value(value),
"DT" => is_date(value),
"TM" => is_time(value),
"TS" => is_timestamp(value),
"NM" => is_numeric(value),
"SI" => is_sequence_id(value),
"TX" => is_text_data(value),
"FT" => is_formatted_text(value),
"PN" => is_person_name(value),
"CX" => is_extended_id(value),
"HD" => is_hierarchic_designator(value),
"AD" => is_address(value),
"XTN" => is_phone_number(value),
_ => true, }
}
pub fn is_string(_value: &str) -> bool {
true
}
pub fn is_identifier(value: &str) -> bool {
value.chars().all(|c| c.is_ascii() && !c.is_control())
}
pub fn is_coded_value(value: &str) -> bool {
is_identifier(value)
}
pub fn is_date(value: &str) -> bool {
hl7v2_datetime::is_valid_hl7_date(value)
}
pub fn is_time(value: &str) -> bool {
hl7v2_datetime::is_valid_hl7_time(value)
}
pub fn is_timestamp(value: &str) -> bool {
hl7v2_datetime::is_valid_hl7_timestamp(value)
}
pub fn is_numeric(value: &str) -> bool {
value.parse::<f64>().is_ok()
}
pub fn is_sequence_id(value: &str) -> bool {
match value.parse::<u32>() {
Ok(num) => num > 0,
Err(_) => false,
}
}
pub fn is_text_data(_value: &str) -> bool {
true
}
pub fn is_formatted_text(_value: &str) -> bool {
true
}
pub fn is_person_name(value: &str) -> bool {
value.chars().all(|c| {
c.is_alphabetic() || c.is_whitespace() || c == '-' || c == '\'' || c == '.' || c == '^'
})
}
pub fn is_extended_id(value: &str) -> bool {
is_identifier(value)
}
pub fn is_hierarchic_designator(value: &str) -> bool {
is_identifier(value)
}
pub fn is_address(value: &str) -> bool {
value.chars().all(|c| c.is_ascii() && !c.is_control())
}
pub fn is_phone_number(value: &str) -> bool {
let cleaned: String = value.chars().filter(char::is_ascii_digit).collect();
cleaned.len() >= 7 && cleaned.len() <= 15 && cleaned.chars().all(|c| c.is_ascii_digit())
}
pub fn is_email(value: &str) -> bool {
let Some((local_part, domain_part)) = value.split_once('@') else {
return false;
};
if domain_part.contains('@') {
return false;
}
if local_part.is_empty() || domain_part.is_empty() {
return false;
}
if !domain_part.contains('.') {
return false;
}
true
}
pub fn is_ssn(value: &str) -> bool {
let cleaned: String = value.chars().filter(char::is_ascii_digit).collect();
if cleaned.len() != 9 {
return false;
}
let mut digits = cleaned.bytes();
let Some(area) = read_decimal_group(&mut digits, 3) else {
return false;
};
if area == 0 || area == 666 || area >= 900 {
return false;
}
let Some(group) = read_decimal_group(&mut digits, 2) else {
return false;
};
if group == 0 {
return false;
}
let Some(serial) = read_decimal_group(&mut digits, 4) else {
return false;
};
if serial == 0 {
return false;
}
true
}
pub fn validate_luhn_checksum(value: &str) -> bool {
let digits: String = value.chars().filter(char::is_ascii_digit).collect();
if digits.len() < 2 {
return false;
}
let mut sum = 0_u32;
let mut double = false;
for digit_char in digits.chars().rev() {
let Some(digit) = digit_char.to_digit(10) else {
return false;
};
let addend = if double {
let doubled = digit.saturating_mul(2);
if doubled > 9 {
doubled.saturating_sub(9)
} else {
doubled
}
} else {
digit
};
let Some(next_sum) = sum.checked_add(addend) else {
return false;
};
sum = next_sum;
double = !double;
}
sum.checked_rem(10) == Some(0)
}
pub fn validate_mod10_checksum(value: &str) -> bool {
validate_luhn_checksum(value)
}
pub fn is_valid_birth_date(value: &str) -> bool {
if !is_date(value) {
return false;
}
let current_date = chrono::Utc::now().format("%Y%m%d").to_string();
value <= current_date.as_str()
}
pub fn is_valid_age_range(birth_date: &str, reference_date: &str) -> bool {
if !is_date(birth_date) || !is_date(reference_date) {
return false;
}
birth_date <= reference_date
}
pub fn is_within_range(value: &str, min: &str, max: &str) -> bool {
let val: f64 = match value.parse() {
Ok(n) => n,
Err(_) => return false,
};
let min_val: f64 = match min.parse() {
Ok(n) => n,
Err(_) => return false,
};
let max_val: f64 = match max.parse() {
Ok(n) => n,
Err(_) => return false,
};
val >= min_val && val <= max_val
}
pub fn matches_format(value: &str, format: &str, datatype: &str) -> bool {
match (datatype, format) {
("DT", "YYYY-MM-DD") => {
if value.len() != 10 {
return false;
}
let Some((year, month, day)) = split_exact3(value, '-') else {
return false;
};
if !has_exact_digits(year, 4) {
return false;
}
let Some(month) = parse_fixed_u32(month, 2) else {
return false;
};
if !(1..=12).contains(&month) {
return false;
}
let Some(day) = parse_fixed_u32(day, 2) else {
return false;
};
if !(1..=31).contains(&day) {
return false;
}
true
}
("TM", "HH:MM:SS") => {
if value.len() != 8 {
return false;
}
let Some((hour, minute, second)) = split_exact3(value, ':') else {
return false;
};
let Some(hour) = parse_fixed_u32(hour, 2) else {
return false;
};
if hour > 23 {
return false;
}
let Some(minute) = parse_fixed_u32(minute, 2) else {
return false;
};
if minute > 59 {
return false;
}
let Some(second) = parse_fixed_u32(second, 2) else {
return false;
};
if second > 59 {
return false;
}
true
}
_ => true, }
}
fn read_decimal_group<I>(digits: &mut I, count: usize) -> Option<u32>
where
I: Iterator<Item = u8>,
{
let mut value = 0_u32;
for _ in 0..count {
let digit = digits.next()?.checked_sub(b'0')?;
value = value.checked_mul(10)?.checked_add(u32::from(digit))?;
}
Some(value)
}
fn split_exact3(value: &str, delimiter: char) -> Option<(&str, &str, &str)> {
let mut parts = value.split(delimiter);
let first = parts.next()?;
let second = parts.next()?;
let third = parts.next()?;
if parts.next().is_some() {
return None;
}
Some((first, second, third))
}
fn has_exact_digits(value: &str, len: usize) -> bool {
value.len() == len && value.chars().all(|c| c.is_ascii_digit())
}
fn parse_fixed_u32(value: &str, len: usize) -> Option<u32> {
if !has_exact_digits(value, len) {
return None;
}
value.parse().ok()
}