use std::sync::LazyLock;
use jiff::civil::DateTime;
use jiff::{SignedDuration, Timestamp, Zoned};
use regex::Regex;
use crate::error::Hl7Error;
const LINE_SEPARATORS: [&str; 4] = ["\r\n", "\n\r", "\r", "\n"];
static DATETIME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(
r"^\s*((?:18|19|20)[0-9]{2})(?:(1[0-2]|0[1-9])(?:(3[0-1]|[1-2][0-9]|0[1-9])(?:([0-1][0-9]|2[0-3])(?:([0-5][0-9])(?:([0-5][0-9](?:\.[0-9]{1,4})?)?)?)?)?)?)?(?:([+-][0-1][0-9]|[+-]2[0-3])([0-5][0-9]))?\s*$",
)
.expect("valid datetime regex")
});
static MLLP_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?s)\x0B(.*?)\x1C\x0D").expect("valid MLLP regex"));
pub fn split_message(message: &str) -> Vec<String> {
let chars: Vec<char> = message.chars().collect();
let mut result = Vec::new();
let mut current = String::new();
let mut i = 0;
while i < chars.len() {
let mut matched = None;
for sep in LINE_SEPARATORS {
let sc: Vec<char> = sep.chars().collect();
if i + sc.len() <= chars.len() && chars[i..i + sc.len()] == sc[..] {
matched = Some(sc.len());
break;
}
}
if let Some(len) = matched {
result.push(std::mem::take(&mut current));
i += len;
} else {
current.push(chars[i]);
i += 1;
}
}
result.push(current);
result.into_iter().filter(|s| !s.trim().is_empty()).collect()
}
pub fn long_date_with_fraction_of_second(dt: DateTime) -> String {
let base = format!(
"{:04}{:02}{:02}{:02}{:02}{:02}",
dt.year(),
dt.month(),
dt.day(),
dt.hour(),
dt.minute(),
dt.second(),
);
let frac = dt.subsec_nanosecond() / 100_000;
if frac == 0 {
base
} else {
let digits = format!("{frac:04}");
let trimmed = digits.trim_end_matches('0');
format!("{base}.{trimmed}")
}
}
pub fn now_long_date() -> String {
long_date_with_fraction_of_second(Zoned::now().datetime())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Hl7DateTime {
pub naive: DateTime,
pub offset_seconds: i64,
}
impl Hl7DateTime {
pub fn to_utc(&self) -> Result<Timestamp, Hl7Error> {
let ts = self
.naive
.to_zoned(jiff::tz::TimeZone::UTC)
.map_err(|e| Hl7Error::new(e.to_string()))?
.timestamp();
Ok(ts - SignedDuration::from_secs(self.offset_seconds))
}
}
pub fn parse_date_time(date_time_string: &str) -> Result<Hl7DateTime, Hl7Error> {
let caps = DATETIME_REGEX
.captures(date_time_string)
.ok_or_else(|| Hl7Error::new("Invalid date format"))?;
let group_i64 = |i: usize, default: i64| -> Result<i64, Hl7Error> {
match caps.get(i) {
Some(m) => m.as_str().parse::<i64>().map_err(|_| Hl7Error::new("Invalid date format")),
None => Ok(default),
}
};
let year: i16 = group_i64(1, 0)? as i16;
let month = group_i64(2, 1)? as i8;
let day = group_i64(3, 1)? as i8;
let hours = group_i64(4, 0)? as i8;
let mins = group_i64(5, 0)? as i8;
let secs: f64 = match caps.get(6) {
Some(m) => m.as_str().parse::<f64>().map_err(|_| Hl7Error::new("Invalid date format"))?,
None => 0.0,
};
let tzh = group_i64(7, 0)?;
let tzm = group_i64(8, 0)?;
let offset_seconds = tzh * 3600 + tzm * 60;
let whole_secs = secs.trunc() as i8;
let nanos = ((secs - secs.trunc()) * 1_000_000_000.0).round() as i32;
let naive = DateTime::new(year, month, day, hours, mins, whole_secs, nanos)
.map_err(|e| Hl7Error::new(e.to_string()))?;
Ok(Hl7DateTime { naive, offset_seconds })
}
pub fn extract_messages(messages: &str) -> Vec<String> {
MLLP_REGEX
.captures_iter(messages)
.map(|c| c[1].to_string())
.collect()
}
pub fn get_mllp(message: &str) -> Vec<u8> {
let data = message.as_bytes();
let mut buffer = Vec::with_capacity(data.len() + 3);
buffer.push(11); buffer.extend_from_slice(data);
buffer.push(28); buffer.push(13); buffer
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn splits_on_mixed_line_separators() {
let lines = split_message("AAA|1\r\nBBB|2\n\nCCC|3\r");
assert_eq!(lines, vec!["AAA|1", "BBB|2", "CCC|3"]);
}
#[test]
fn parses_full_datetime_with_offset() {
let dt = parse_date_time("20200101120000.5+0100").unwrap();
assert_eq!(dt.naive.year(), 2020);
assert_eq!(dt.naive.month(), 1);
assert_eq!(dt.naive.day(), 1);
assert_eq!(dt.naive.hour(), 12);
assert_eq!(dt.naive.subsec_nanosecond(), 500_000_000);
assert_eq!(dt.offset_seconds, 3600);
let utc = dt.to_utc().unwrap();
assert_eq!(utc.to_string(), "2020-01-01T11:00:00.5Z");
}
#[test]
fn parses_date_only() {
let dt = parse_date_time("19610615").unwrap();
assert_eq!(dt.naive.year(), 1961);
assert_eq!(dt.naive.month(), 6);
assert_eq!(dt.naive.day(), 15);
assert_eq!(dt.offset_seconds, 0);
}
#[test]
fn rejects_bad_datetime() {
assert!(parse_date_time("not-a-date").is_err());
}
#[test]
fn long_date_omits_zero_fraction() {
let dt = jiff::civil::date(2020, 1, 2).at(3, 4, 5, 0);
assert_eq!(long_date_with_fraction_of_second(dt), "20200102030405");
let dt = jiff::civil::date(2020, 1, 2).at(3, 4, 5, 120_000_000);
assert_eq!(long_date_with_fraction_of_second(dt), "20200102030405.12");
}
#[test]
fn extracts_mllp_framed_messages() {
let stream = "\x0BMSH|^~\\&|A\rPID|1\r\x1C\x0D\x0BMSH|^~\\&|B\r\x1C\x0D";
let msgs = extract_messages(stream);
assert_eq!(msgs.len(), 2);
assert!(msgs[0].starts_with("MSH|^~\\&|A"));
assert!(msgs[1].starts_with("MSH|^~\\&|B"));
}
}