1use chrono::{Datelike, NaiveDate, NaiveDateTime, Timelike};
35
36#[derive(Debug, Clone, PartialEq, thiserror::Error)]
38pub enum DateTimeError {
39 #[error("Invalid date format: {0}")]
40 InvalidDateFormat(String),
41
42 #[error("Invalid time format: {0}")]
43 InvalidTimeFormat(String),
44
45 #[error("Invalid timestamp format: {0}")]
46 InvalidTimestampFormat(String),
47
48 #[error("Date out of range: {0}")]
49 DateOutOfRange(String),
50
51 #[error("Time out of range: {0}")]
52 TimeOutOfRange(String),
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
57pub enum TimestampPrecision {
58 Year,
60 Month,
62 Day,
64 Hour,
66 Minute,
68 Second,
70 FractionalSecond,
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct ParsedTimestamp {
77 pub datetime: NaiveDateTime,
79 pub precision: TimestampPrecision,
81 pub fractional_seconds: Option<u32>,
83}
84
85impl ParsedTimestamp {
86 pub fn new(datetime: NaiveDateTime, precision: TimestampPrecision) -> Self {
88 Self {
89 datetime,
90 precision,
91 fractional_seconds: None,
92 }
93 }
94
95 pub fn with_fractional(datetime: NaiveDateTime, fractional: u32) -> Self {
97 Self {
98 datetime,
99 precision: TimestampPrecision::FractionalSecond,
100 fractional_seconds: Some(fractional),
101 }
102 }
103
104 pub fn is_same_day(&self, other: &ParsedTimestamp) -> bool {
106 self.datetime.date() == other.datetime.date()
107 }
108
109 pub fn is_before(&self, other: &ParsedTimestamp) -> bool {
111 if self.precision != other.precision {
113 return self.datetime < other.datetime;
116 }
117 self.datetime < other.datetime
118 }
119
120 pub fn is_after(&self, other: &ParsedTimestamp) -> bool {
122 other.is_before(self)
123 }
124
125 pub fn is_equal(&self, other: &ParsedTimestamp) -> bool {
127 let min_precision = std::cmp::min(self.precision, other.precision);
128 let truncated_self = truncate_to_precision(&self.datetime, min_precision);
129 let truncated_other = truncate_to_precision(&other.datetime, min_precision);
130 truncated_self == truncated_other
131 }
132
133 pub fn to_hl7_string(&self) -> String {
135 match self.precision {
136 TimestampPrecision::Year => self.datetime.format("%Y").to_string(),
137 TimestampPrecision::Month => self.datetime.format("%Y%m").to_string(),
138 TimestampPrecision::Day => self.datetime.format("%Y%m%d").to_string(),
139 TimestampPrecision::Hour => self.datetime.format("%Y%m%d%H").to_string(),
140 TimestampPrecision::Minute => self.datetime.format("%Y%m%d%H%M").to_string(),
141 TimestampPrecision::Second => self.datetime.format("%Y%m%d%H%M%S").to_string(),
142 TimestampPrecision::FractionalSecond => {
143 if let Some(frac) = self.fractional_seconds {
144 format!("{}{:06}", self.datetime.format("%Y%m%d%H%M%S"), frac)
145 } else {
146 self.datetime.format("%Y%m%d%H%M%S").to_string()
147 }
148 }
149 }
150 }
151}
152
153pub fn parse_hl7_dt(s: &str) -> Result<NaiveDate, DateTimeError> {
155 let s = s.trim();
156
157 if s.len() != 8 {
158 return Err(DateTimeError::InvalidDateFormat(format!(
159 "Expected 8 characters, got {}",
160 s.len()
161 )));
162 }
163
164 if !s.chars().all(|c| c.is_ascii_digit()) {
165 return Err(DateTimeError::InvalidDateFormat(
166 "Contains non-digit characters".to_string(),
167 ));
168 }
169
170 NaiveDate::parse_from_str(s, "%Y%m%d")
171 .map_err(|e| DateTimeError::InvalidDateFormat(e.to_string()))
172}
173
174pub fn parse_hl7_tm(s: &str) -> Result<(u32, u32, u32, Option<u32>), DateTimeError> {
176 let s = s.trim();
177
178 if s.len() < 4 {
179 return Err(DateTimeError::InvalidTimeFormat(format!(
180 "Expected at least 4 characters, got {}",
181 s.len()
182 )));
183 }
184
185 if !s.is_ascii() {
186 return Err(DateTimeError::InvalidTimeFormat(
187 "Non-ASCII characters".into(),
188 ));
189 }
190
191 let hour: u32 = s[0..2]
193 .parse()
194 .map_err(|_| DateTimeError::TimeOutOfRange("Invalid hour".to_string()))?;
195 let minute: u32 = s[2..4]
196 .parse()
197 .map_err(|_| DateTimeError::TimeOutOfRange("Invalid minute".to_string()))?;
198
199 if hour > 23 {
201 return Err(DateTimeError::TimeOutOfRange(format!(
202 "Hour {} out of range",
203 hour
204 )));
205 }
206 if minute > 59 {
207 return Err(DateTimeError::TimeOutOfRange(format!(
208 "Minute {} out of range",
209 minute
210 )));
211 }
212
213 let (second, fractional) = if s.len() > 4 {
215 let (sec_part, frac_part) = if let Some(dot_pos) = s[4..].find('.') {
217 let sec = &s[4..4 + dot_pos];
218 let frac = &s[4 + dot_pos + 1..];
219 (sec, Some(frac))
220 } else {
221 (&s[4..], None)
222 };
223
224 let sec: u32 = sec_part
225 .parse()
226 .map_err(|_| DateTimeError::TimeOutOfRange("Invalid second".to_string()))?;
227 if sec > 59 {
228 return Err(DateTimeError::TimeOutOfRange(format!(
229 "Second {} out of range",
230 sec
231 )));
232 }
233
234 let frac = if let Some(f) = frac_part {
235 let padded = format!("{:0<6}", f.chars().take(6).collect::<String>());
237 Some(padded.parse::<u32>().unwrap_or(0))
238 } else {
239 None
240 };
241
242 (sec, frac)
243 } else {
244 (0, None)
245 };
246
247 Ok((hour, minute, second, fractional))
248}
249
250pub fn parse_hl7_ts(s: &str) -> Result<NaiveDateTime, DateTimeError> {
252 let s = s.trim();
253
254 if s.len() < 8 {
255 return Err(DateTimeError::InvalidTimestampFormat(format!(
256 "Expected at least 8 characters, got {}",
257 s.len()
258 )));
259 }
260
261 if !s.is_ascii() {
262 return Err(DateTimeError::InvalidTimestampFormat(
263 "Non-ASCII characters".into(),
264 ));
265 }
266
267 let date = parse_hl7_dt(&s[0..8])?;
269
270 if s.len() == 8 {
272 return Ok(date.and_hms_opt(0, 0, 0).unwrap());
273 }
274
275 let time_str = &s[8..];
277 let (hour, minute, second, _) = parse_hl7_tm(time_str)?;
278
279 date.and_hms_opt(hour, minute, second)
280 .ok_or_else(|| DateTimeError::TimeOutOfRange("Invalid time combination".to_string()))
281}
282
283pub fn parse_hl7_ts_with_precision(s: &str) -> Result<ParsedTimestamp, DateTimeError> {
285 let s = s.trim();
286
287 if !s.is_ascii() {
288 return Err(DateTimeError::InvalidTimestampFormat(
289 "Non-ASCII characters".into(),
290 ));
291 }
292
293 let precision = match s.len() {
295 4 => TimestampPrecision::Year,
296 6 => TimestampPrecision::Month,
297 8 => TimestampPrecision::Day,
298 10 => TimestampPrecision::Hour,
299 12 => TimestampPrecision::Minute,
300 14 => TimestampPrecision::Second,
301 n if n > 14 && s[14..].starts_with('.') => TimestampPrecision::FractionalSecond,
302 _ => {
303 return Err(DateTimeError::InvalidTimestampFormat(format!(
304 "Invalid length: {}",
305 s.len()
306 )));
307 }
308 };
309
310 match precision {
312 TimestampPrecision::Year => {
313 let year: i32 = s
314 .parse()
315 .map_err(|_| DateTimeError::InvalidDateFormat("Invalid year".into()))?;
316 let date = NaiveDate::from_ymd_opt(year, 1, 1)
317 .ok_or_else(|| DateTimeError::DateOutOfRange("Invalid year".into()))?;
318 Ok(ParsedTimestamp::new(
319 date.and_hms_opt(0, 0, 0).unwrap(),
320 precision,
321 ))
322 }
323 TimestampPrecision::Month => {
324 let year: i32 = s[0..4]
325 .parse()
326 .map_err(|_| DateTimeError::InvalidDateFormat("Invalid year".into()))?;
327 let month: u32 = s[4..6]
328 .parse()
329 .map_err(|_| DateTimeError::InvalidDateFormat("Invalid month".into()))?;
330 let date = NaiveDate::from_ymd_opt(year, month, 1)
331 .ok_or_else(|| DateTimeError::DateOutOfRange("Invalid month".into()))?;
332 Ok(ParsedTimestamp::new(
333 date.and_hms_opt(0, 0, 0).unwrap(),
334 precision,
335 ))
336 }
337 TimestampPrecision::Day => {
338 let date = parse_hl7_dt(s)?;
339 Ok(ParsedTimestamp::new(
340 date.and_hms_opt(0, 0, 0).unwrap(),
341 precision,
342 ))
343 }
344 TimestampPrecision::Hour => {
345 let date = parse_hl7_dt(&s[0..8])?;
346 let hour: u32 = s[8..10]
347 .parse()
348 .map_err(|_| DateTimeError::TimeOutOfRange("Invalid hour".into()))?;
349 Ok(ParsedTimestamp::new(
350 date.and_hms_opt(hour, 0, 0).unwrap(),
351 precision,
352 ))
353 }
354 TimestampPrecision::Minute => {
355 let date = parse_hl7_dt(&s[0..8])?;
356 let hour: u32 = s[8..10]
357 .parse()
358 .map_err(|_| DateTimeError::TimeOutOfRange("Invalid hour".into()))?;
359 let minute: u32 = s[10..12]
360 .parse()
361 .map_err(|_| DateTimeError::TimeOutOfRange("Invalid minute".into()))?;
362 Ok(ParsedTimestamp::new(
363 date.and_hms_opt(hour, minute, 0).unwrap(),
364 precision,
365 ))
366 }
367 TimestampPrecision::Second => {
368 let dt = parse_hl7_ts(s)?;
369 Ok(ParsedTimestamp::new(dt, precision))
370 }
371 TimestampPrecision::FractionalSecond => {
372 let dt = parse_hl7_ts(&s[0..14])?;
374 let frac_str = &s[15..]; let padded = format!("{:0<6}", frac_str.chars().take(6).collect::<String>());
377 let fractional: u32 = padded.parse().unwrap_or(0);
378 Ok(ParsedTimestamp::with_fractional(dt, fractional))
379 }
380 }
381}
382
383fn truncate_to_precision(dt: &NaiveDateTime, precision: TimestampPrecision) -> NaiveDateTime {
385 match precision {
386 TimestampPrecision::Year => NaiveDate::from_ymd_opt(dt.year(), 1, 1)
387 .and_then(|d| d.and_hms_opt(0, 0, 0))
388 .unwrap_or(*dt),
389 TimestampPrecision::Month => NaiveDate::from_ymd_opt(dt.year(), dt.month(), 1)
390 .and_then(|d| d.and_hms_opt(0, 0, 0))
391 .unwrap_or(*dt),
392 TimestampPrecision::Day => dt.date().and_hms_opt(0, 0, 0).unwrap_or(*dt),
393 TimestampPrecision::Hour => dt
394 .with_minute(0)
395 .and_then(|d| d.with_second(0))
396 .unwrap_or(*dt),
397 TimestampPrecision::Minute => dt.with_second(0).unwrap_or(*dt),
398 TimestampPrecision::Second | TimestampPrecision::FractionalSecond => *dt,
399 }
400}
401
402pub fn is_valid_hl7_date(s: &str) -> bool {
404 parse_hl7_dt(s).is_ok()
405}
406
407pub fn is_valid_hl7_time(s: &str) -> bool {
409 parse_hl7_tm(s).is_ok()
410}
411
412pub fn is_valid_hl7_timestamp(s: &str) -> bool {
414 parse_hl7_ts(s).is_ok()
415}
416
417pub fn now_hl7() -> String {
419 chrono::Utc::now().format("%Y%m%d%H%M%S").to_string()
420}
421
422pub fn today_hl7() -> String {
424 chrono::Utc::now().format("%Y%m%d").to_string()
425}
426
427#[cfg(test)]
428mod tests;