hl7v2 1.2.1

HL7 v2 message parser and processor for Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
//! HL7 v2 data type validation.
//!
//! This module provides validation functions for HL7 v2 data types,
//! including primitive types (ST, ID, DT, TM, TS, NM, etc.) and
//! commonly used validation patterns.
//!
//! # Supported Data Types
//!
//! - `ST` - String Data
//! - `ID` - Coded values for HL7 tables
//! - `IS` - Coded value for user-defined tables
//! - `DT` - Date
//! - `TM` - Time
//! - `TS` - Timestamp
//! - `NM` - Numeric
//! - `SI` - Sequence ID
//! - `TX` - Text Data
//! - `FT` - Formatted Text Data
//! - `PN` - Person Name
//! - `CX` - Extended Composite ID
//! - `HD` - Hierarchic Designator
//!
//! # Example
//!
//! ```
//! use hl7v2::conformance::datatype::{validate_datatype, DataType, DataTypeValidator};
//!
//! // Validate a date
//! assert!(validate_datatype("20250128", "DT"));
//! assert!(!validate_datatype("20251328", "DT")); // Invalid month
//!
//! // Validate a person name
//! assert!(validate_datatype("Smith^John", "PN"));
//!
//! // Use the validator builder
//! let validator = DataTypeValidator::new()
//!     .with_min_length(1)
//!     .with_max_length(50);
//! assert!(validator.validate("Test Value"));
//! ```

/// HL7 datetime parsing and validation helpers.
pub mod datetime;

pub use datetime as hl7v2_datetime;
use regex::Regex;

/// Error type for data type validation
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
pub enum DataTypeError {
    /// The provided datatype name is unknown.
    #[error("Invalid data type '{datatype}': {reason}")]
    InvalidDataType {
        /// The requested datatype code.
        datatype: String,
        /// Human-readable reason for rejection.
        reason: String,
    },

    /// Value length is shorter than the configured minimum.
    #[error("Value too short: {length} < {min}")]
    TooShort {
        /// Actual value length.
        length: usize,
        /// Minimum allowed length.
        min: usize,
    },

    /// Value length exceeds the configured maximum.
    #[error("Value too long: {length} > {max}")]
    TooLong {
        /// Actual value length.
        length: usize,
        /// Maximum allowed length.
        max: usize,
    },

    /// Value does not match the configured pattern.
    #[error("Pattern mismatch: value '{value}' does not match pattern '{pattern}'")]
    PatternMismatch {
        /// Input value provided for validation.
        value: String,
        /// Regex pattern that was not matched.
        pattern: String,
    },

    /// Value is not present in the allowed set.
    #[error("Value not in allowed set: {value}")]
    NotInAllowedSet {
        /// Input value provided for validation.
        value: String,
    },

    /// Checksum validation failed.
    #[error("Checksum validation failed")]
    ChecksumFailed,
}

/// Result type for data type validation
pub type ValidationResult = Result<(), DataTypeError>;

/// HL7 data types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DataType {
    /// String Data
    ST,
    /// Coded values for HL7 tables
    ID,
    /// Coded value for user-defined tables
    IS,
    /// Date
    DT,
    /// Time
    TM,
    /// Timestamp
    TS,
    /// Numeric
    NM,
    /// Sequence ID
    SI,
    /// Text Data
    TX,
    /// Formatted Text Data
    FT,
    /// Person Name
    PN,
    /// Extended Composite ID
    CX,
    /// Hierarchic Designator
    HD,
    /// Address
    AD,
    /// Phone Number
    XTN,
}

impl DataType {
    /// Parse from string
    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,
        }
    }
}

/// Validator for data types with configurable constraints
#[derive(Debug, Clone, Default)]
pub struct DataTypeValidator {
    /// Minimum length constraint
    pub min_length: Option<usize>,
    /// Maximum length constraint
    pub max_length: Option<usize>,
    /// Regex pattern constraint
    pub pattern: Option<String>,
    /// Allowed values constraint
    pub allowed_values: Option<Vec<String>>,
    /// Checksum algorithm
    pub checksum: Option<ChecksumAlgorithm>,
}

/// Checksum algorithms
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChecksumAlgorithm {
    /// Luhn algorithm (for credit cards, etc.)
    Luhn,
    /// Mod 10
    Mod10,
}

impl DataTypeValidator {
    /// Create a new validator
    pub fn new() -> Self {
        Self::default()
    }

    /// Set minimum length
    pub fn with_min_length(mut self, min: usize) -> Self {
        self.min_length = Some(min);
        self
    }

    /// Set maximum length
    pub fn with_max_length(mut self, max: usize) -> Self {
        self.max_length = Some(max);
        self
    }

    /// Set regex pattern
    pub fn with_pattern(mut self, pattern: &str) -> Self {
        self.pattern = Some(pattern.to_string());
        self
    }

    /// Set allowed values
    pub fn with_allowed_values(mut self, values: Vec<String>) -> Self {
        self.allowed_values = Some(values);
        self
    }

    /// Set checksum algorithm
    pub fn with_checksum(mut self, algorithm: ChecksumAlgorithm) -> Self {
        self.checksum = Some(algorithm);
        self
    }

    /// Validate a value
    pub fn validate(&self, value: &str) -> bool {
        self.validate_detailed(value).is_ok()
    }

    /// Validate a value with detailed error information.
    ///
    /// # Errors
    ///
    /// Returns [`DataTypeError`] when the value violates a configured length,
    /// pattern, allowed-value, or checksum constraint.
    pub fn validate_detailed(&self, value: &str) -> ValidationResult {
        // Check minimum length
        if let Some(min) = self.min_length
            && value.len() < min
        {
            return Err(DataTypeError::TooShort {
                length: value.len(),
                min,
            });
        }

        // Check maximum length
        if let Some(max) = self.max_length
            && value.len() > max
        {
            return Err(DataTypeError::TooLong {
                length: value.len(),
                max,
            });
        }

        // Check pattern
        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(),
            });
        }

        // Check allowed values
        if let Some(allowed) = &self.allowed_values
            && !allowed.contains(&value.to_string())
        {
            return Err(DataTypeError::NotInAllowedSet {
                value: value.to_string(),
            });
        }

        // Check checksum
        if let Some(algorithm) = self.checksum {
            match algorithm {
                ChecksumAlgorithm::Luhn | ChecksumAlgorithm::Mod10 => {
                    if !validate_luhn_checksum(value) {
                        return Err(DataTypeError::ChecksumFailed);
                    }
                }
            }
        }

        Ok(())
    }
}

/// Validate a value against an HL7 data type
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, // Unknown data type, assume valid
    }
}

/// Check if value is a valid string (always true for parsed values)
pub fn is_string(_value: &str) -> bool {
    true
}

/// Check if value is a valid identifier (alphanumeric + special characters)
pub fn is_identifier(value: &str) -> bool {
    // HL7 identifiers can contain alphanumeric characters and some special characters
    // For simplicity, we'll check if it contains only printable ASCII characters
    value.chars().all(|c| c.is_ascii() && !c.is_control())
}

/// Check if value is a valid coded value (alphanumeric + special characters)
pub fn is_coded_value(value: &str) -> bool {
    // Similar to identifier
    is_identifier(value)
}

/// Check if value is a valid date (YYYYMMDD format)
pub fn is_date(value: &str) -> bool {
    hl7v2_datetime::is_valid_hl7_date(value)
}

/// Check if value is a valid time (HHMM\[SS\[.S\[S\[S\[S\]\]\]\]\] format)
pub fn is_time(value: &str) -> bool {
    hl7v2_datetime::is_valid_hl7_time(value)
}

/// Check if value is a valid timestamp (YYYYMMDD\[HHMM\[SS\[.S\[S\[S\[S\]\]\]\]\]\] format)
pub fn is_timestamp(value: &str) -> bool {
    hl7v2_datetime::is_valid_hl7_timestamp(value)
}

/// Check if value is numeric
pub fn is_numeric(value: &str) -> bool {
    // Can be integer or decimal
    value.parse::<f64>().is_ok()
}

/// Check if value is a sequence ID (positive integer)
pub fn is_sequence_id(value: &str) -> bool {
    match value.parse::<u32>() {
        Ok(num) => num > 0,
        Err(_) => false,
    }
}

/// Check if value is text data (always true for parsed values)
pub fn is_text_data(_value: &str) -> bool {
    true
}

/// Check if value is formatted text (always true for parsed values)
pub fn is_formatted_text(_value: &str) -> bool {
    true
}

/// Check if value is a person name (contains letters, spaces, hyphens, apostrophes)
pub fn is_person_name(value: &str) -> bool {
    value.chars().all(|c| {
        c.is_alphabetic() || c.is_whitespace() || c == '-' || c == '\'' || c == '.' || c == '^'
    })
}

/// Check if value is an extended ID (contains identifier characters)
pub fn is_extended_id(value: &str) -> bool {
    is_identifier(value)
}

/// Check if value is a hierarchic designator (contains identifier characters)
pub fn is_hierarchic_designator(value: &str) -> bool {
    is_identifier(value)
}

/// Check if value is a valid address
pub fn is_address(value: &str) -> bool {
    // Address can contain most printable characters
    value.chars().all(|c| c.is_ascii() && !c.is_control())
}

/// Check if value is a valid phone number (basic validation)
pub fn is_phone_number(value: &str) -> bool {
    // Remove common phone number formatting characters
    let cleaned: String = value.chars().filter(char::is_ascii_digit).collect();

    // Basic phone number validation (7-15 digits)
    cleaned.len() >= 7 && cleaned.len() <= 15 && cleaned.chars().all(|c| c.is_ascii_digit())
}

/// Check if value is a valid email address (basic validation)
pub fn is_email(value: &str) -> bool {
    // Basic email validation - contains @ and has characters before and after
    let Some((local_part, domain_part)) = value.split_once('@') else {
        return false;
    };
    if domain_part.contains('@') {
        return false;
    }

    // Check that both parts are non-empty
    if local_part.is_empty() || domain_part.is_empty() {
        return false;
    }

    // Check that domain contains at least one dot
    if !domain_part.contains('.') {
        return false;
    }

    true
}

/// Check if value is a valid SSN (Social Security Number) format
pub fn is_ssn(value: &str) -> bool {
    // Remove dashes and spaces
    let cleaned: String = value.chars().filter(char::is_ascii_digit).collect();

    // SSN should be exactly 9 digits
    if cleaned.len() != 9 {
        return false;
    }

    let mut digits = cleaned.bytes();

    // First 3 digits cannot be 000, 666, or 900-999
    let Some(area) = read_decimal_group(&mut digits, 3) else {
        return false;
    };
    if area == 0 || area == 666 || area >= 900 {
        return false;
    }

    // Next 2 digits cannot be 00
    let Some(group) = read_decimal_group(&mut digits, 2) else {
        return false;
    };
    if group == 0 {
        return false;
    }

    // Last 4 digits cannot be 0000
    let Some(serial) = read_decimal_group(&mut digits, 4) else {
        return false;
    };
    if serial == 0 {
        return false;
    }

    true
}

/// Validate Luhn checksum (used for credit cards, etc.)
pub fn validate_luhn_checksum(value: &str) -> bool {
    // Remove any non-digit characters
    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;

    // Process digits from right to left
    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)
}

/// Validate Mod10 checksum
pub fn validate_mod10_checksum(value: &str) -> bool {
    validate_luhn_checksum(value)
}

/// Check if a date is valid and not in the future
pub fn is_valid_birth_date(value: &str) -> bool {
    if !is_date(value) {
        return false;
    }

    // Check if date is not in the future
    let current_date = chrono::Utc::now().format("%Y%m%d").to_string();
    value <= current_date.as_str()
}

/// Check if two dates represent a valid age range (e.g., birth date vs admission date)
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 should be before or equal to reference date
    birth_date <= reference_date
}

/// Check if a value is within a specified range (inclusive)
pub fn is_within_range(value: &str, min: &str, max: &str) -> bool {
    // Parse all values as numbers
    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
}

/// Validate format specification
pub fn matches_format(value: &str, format: &str, datatype: &str) -> bool {
    match (datatype, format) {
        ("DT", "YYYY-MM-DD") => {
            // Check if value matches YYYY-MM-DD format
            if value.len() != 10 {
                return false;
            }
            let Some((year, month, day)) = split_exact3(value, '-') else {
                return false;
            };
            // Check year (4 digits)
            if !has_exact_digits(year, 4) {
                return false;
            }
            // Check month (2 digits)
            let Some(month) = parse_fixed_u32(month, 2) else {
                return false;
            };
            if !(1..=12).contains(&month) {
                return false;
            }
            // Check day (2 digits)
            let Some(day) = parse_fixed_u32(day, 2) else {
                return false;
            };
            if !(1..=31).contains(&day) {
                return false;
            }
            true
        }
        ("TM", "HH:MM:SS") => {
            // Check if value matches HH:MM:SS format
            if value.len() != 8 {
                return false;
            }
            let Some((hour, minute, second)) = split_exact3(value, ':') else {
                return false;
            };
            // Check hour (2 digits)
            let Some(hour) = parse_fixed_u32(hour, 2) else {
                return false;
            };
            if hour > 23 {
                return false;
            }
            // Check minute (2 digits)
            let Some(minute) = parse_fixed_u32(minute, 2) else {
                return false;
            };
            if minute > 59 {
                return false;
            }
            // Check second (2 digits)
            let Some(second) = parse_fixed_u32(second, 2) else {
                return false;
            };
            if second > 59 {
                return false;
            }
            true
        }
        _ => true, // Unknown format, assume valid
    }
}

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()
}