use crate::dataset::DataSet;
use dicom_toolkit_core::error::{DcmError, DcmResult};
use dicom_toolkit_dict::Tag;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct DicomDate {
pub year: u16,
pub month: u8,
pub day: u8,
}
impl DicomDate {
pub fn parse(s: &str) -> DcmResult<Self> {
let s = s.trim();
match s.len() {
4 => {
let year = parse_u16_str(&s[0..4])?;
Ok(Self {
year,
month: 0,
day: 0,
})
}
6 => {
let year = parse_u16_str(&s[0..4])?;
let month = parse_u8_str(&s[4..6])?;
Ok(Self {
year,
month,
day: 0,
})
}
8 => {
let year = parse_u16_str(&s[0..4])?;
let month = parse_u8_str(&s[4..6])?;
let day = parse_u8_str(&s[6..8])?;
Ok(Self { year, month, day })
}
_ => Err(DcmError::Other(format!("invalid DICOM date: {:?}", s))),
}
}
pub fn from_da_str(s: &str) -> DcmResult<Self> {
let s = s.trim();
if s.len() == 10 && s.as_bytes().get(4) == Some(&b'.') && s.as_bytes().get(7) == Some(&b'.')
{
let year = parse_u16_str(&s[0..4])?;
let month = parse_u8_str(&s[5..7])?;
let day = parse_u8_str(&s[8..10])?;
return Ok(Self { year, month, day });
}
Self::parse(s)
}
}
impl fmt::Display for DicomDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.month == 0 {
write!(f, "{:04}", self.year)
} else if self.day == 0 {
write!(f, "{:04}{:02}", self.year, self.month)
} else {
write!(f, "{:04}{:02}{:02}", self.year, self.month, self.day)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DicomTime {
pub hour: u8,
pub minute: u8,
pub second: u8,
pub fraction: u32,
}
impl DicomTime {
pub fn parse(s: &str) -> DcmResult<Self> {
let s = s.trim();
if s.is_empty() {
return Err(DcmError::Other("empty DICOM time string".into()));
}
let (time_part, fraction) = if let Some(dot_pos) = s.find('.') {
let frac_str = &s[dot_pos + 1..];
let mut padded = String::from(frac_str);
while padded.len() < 6 {
padded.push('0');
}
let frac = parse_u32_str(&padded[..6])?;
(&s[..dot_pos], frac)
} else {
(s, 0u32)
};
match time_part.len() {
2 => {
let hour = parse_u8_str(&time_part[0..2])?;
if hour > 23 {
return Err(DcmError::Other(format!(
"invalid hour in DICOM time: {hour}"
)));
}
Ok(Self {
hour,
minute: 0,
second: 0,
fraction: 0,
})
}
4 => {
let hour = parse_u8_str(&time_part[0..2])?;
let minute = parse_u8_str(&time_part[2..4])?;
if hour > 23 {
return Err(DcmError::Other(format!(
"invalid hour in DICOM time: {hour}"
)));
}
if minute > 59 {
return Err(DcmError::Other(format!(
"invalid minute in DICOM time: {minute}"
)));
}
Ok(Self {
hour,
minute,
second: 0,
fraction: 0,
})
}
6 => {
let hour = parse_u8_str(&time_part[0..2])?;
let minute = parse_u8_str(&time_part[2..4])?;
let second = parse_u8_str(&time_part[4..6])?;
if hour > 23 {
return Err(DcmError::Other(format!(
"invalid hour in DICOM time: {hour}"
)));
}
if minute > 59 {
return Err(DcmError::Other(format!(
"invalid minute in DICOM time: {minute}"
)));
}
if second > 59 {
return Err(DcmError::Other(format!(
"invalid second in DICOM time: {second}"
)));
}
Ok(Self {
hour,
minute,
second,
fraction,
})
}
_ => Err(DcmError::Other(format!("invalid DICOM time: {:?}", s))),
}
}
}
impl fmt::Display for DicomTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:02}{:02}{:02}", self.hour, self.minute, self.second)?;
if self.fraction > 0 {
write!(f, ".{:06}", self.fraction)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct DicomDateTime {
pub date: DicomDate,
pub time: Option<DicomTime>,
pub offset_minutes: Option<i16>,
}
impl DicomDateTime {
pub fn parse(s: &str) -> DcmResult<Self> {
let s = s.trim();
if s.len() < 4 {
return Err(DcmError::Other(format!("invalid DICOM datetime: {:?}", s)));
}
let (dt_part, offset_minutes) = extract_tz_offset(s)?;
let date_len = dt_part.len().min(8);
let date_str = &dt_part[..date_len];
let date = DicomDate::parse(date_str)?;
let time = if dt_part.len() > 8 {
Some(DicomTime::parse(&dt_part[8..])?)
} else {
None
};
Ok(Self {
date,
time,
offset_minutes,
})
}
}
fn extract_tz_offset(s: &str) -> DcmResult<(&str, Option<i16>)> {
let bytes = s.as_bytes();
for i in (1..s.len()).rev() {
if bytes[i] == b'+' || bytes[i] == b'-' {
let tz_str = &s[i..];
if tz_str.len() == 5 {
let sign: i16 = if bytes[i] == b'+' { 1 } else { -1 };
let hh = parse_u8_str(&tz_str[1..3])? as i16;
let mm = parse_u8_str(&tz_str[3..5])? as i16;
return Ok((&s[..i], Some(sign * (hh * 60 + mm))));
}
}
}
Ok((s, None))
}
impl fmt::Display for DicomDateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.date)?;
if let Some(ref t) = self.time {
write!(f, "{}", t)?;
}
if let Some(offset) = self.offset_minutes {
let sign = if offset >= 0 { '+' } else { '-' };
let abs = offset.unsigned_abs();
write!(f, "{}{:02}{:02}", sign, abs / 60, abs % 60)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PersonName {
pub alphabetic: String,
pub ideographic: String,
pub phonetic: String,
}
impl PersonName {
pub fn parse(s: &str) -> Self {
let mut parts = s.splitn(3, '=');
PersonName {
alphabetic: parts.next().unwrap_or("").to_string(),
ideographic: parts.next().unwrap_or("").to_string(),
phonetic: parts.next().unwrap_or("").to_string(),
}
}
fn component(group: &str, index: usize) -> &str {
group.split('^').nth(index).unwrap_or("")
}
pub fn last_name(&self) -> &str {
Self::component(&self.alphabetic, 0)
}
pub fn first_name(&self) -> &str {
Self::component(&self.alphabetic, 1)
}
pub fn middle_name(&self) -> &str {
Self::component(&self.alphabetic, 2)
}
pub fn prefix(&self) -> &str {
Self::component(&self.alphabetic, 3)
}
pub fn suffix(&self) -> &str {
Self::component(&self.alphabetic, 4)
}
}
impl fmt::Display for PersonName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if !self.phonetic.is_empty() {
write!(
f,
"{}={}={}",
self.alphabetic, self.ideographic, self.phonetic
)
} else if !self.ideographic.is_empty() {
write!(f, "{}={}", self.alphabetic, self.ideographic)
} else {
write!(f, "{}", self.alphabetic)
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PixelData {
Native { bytes: Vec<u8> },
Encapsulated {
offset_table: Vec<u32>,
fragments: Vec<Vec<u8>>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Empty,
Strings(Vec<String>),
PersonNames(Vec<PersonName>),
Uid(String),
Date(Vec<DicomDate>),
Time(Vec<DicomTime>),
DateTime(Vec<DicomDateTime>),
Ints(Vec<i64>),
Decimals(Vec<f64>),
U8(Vec<u8>),
U16(Vec<u16>),
I16(Vec<i16>),
U32(Vec<u32>),
I32(Vec<i32>),
U64(Vec<u64>),
I64(Vec<i64>),
F32(Vec<f32>),
F64(Vec<f64>),
Tags(Vec<Tag>),
Sequence(Vec<DataSet>),
PixelData(PixelData),
}
impl Value {
pub fn multiplicity(&self) -> usize {
match self {
Value::Empty => 0,
Value::Strings(v) => v.len(),
Value::PersonNames(v) => v.len(),
Value::Uid(_) => 1,
Value::Date(v) => v.len(),
Value::Time(v) => v.len(),
Value::DateTime(v) => v.len(),
Value::Ints(v) => v.len(),
Value::Decimals(v) => v.len(),
Value::U8(v) => v.len(),
Value::U16(v) => v.len(),
Value::I16(v) => v.len(),
Value::U32(v) => v.len(),
Value::I32(v) => v.len(),
Value::U64(v) => v.len(),
Value::I64(v) => v.len(),
Value::F32(v) => v.len(),
Value::F64(v) => v.len(),
Value::Tags(v) => v.len(),
Value::Sequence(v) => v.len(),
Value::PixelData(_) => 1,
}
}
pub fn is_empty(&self) -> bool {
self.multiplicity() == 0
}
pub fn as_string(&self) -> Option<&str> {
match self {
Value::Strings(v) => v.first().map(|s| s.as_str()),
Value::Uid(s) => Some(s.as_str()),
Value::PersonNames(v) => v.first().map(|p| p.alphabetic.as_str()),
_ => None,
}
}
pub fn as_strings(&self) -> Option<&[String]> {
match self {
Value::Strings(v) => Some(v.as_slice()),
_ => None,
}
}
pub fn as_u16(&self) -> Option<u16> {
match self {
Value::U16(v) => v.first().copied(),
_ => None,
}
}
pub fn as_u32(&self) -> Option<u32> {
match self {
Value::U32(v) => v.first().copied(),
_ => None,
}
}
pub fn as_i32(&self) -> Option<i32> {
match self {
Value::I32(v) => v.first().copied(),
_ => None,
}
}
pub fn as_f64(&self) -> Option<f64> {
match self {
Value::F64(v) => v.first().copied(),
Value::Decimals(v) => v.first().copied(),
_ => None,
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
Value::U8(v) => Some(v.as_slice()),
Value::PixelData(PixelData::Native { bytes }) => Some(bytes.as_slice()),
_ => None,
}
}
pub fn to_display_string(&self) -> String {
match self {
Value::Empty => String::new(),
Value::Strings(v) => v.join("\\"),
Value::PersonNames(v) => v
.iter()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::Uid(s) => s.clone(),
Value::Date(v) => v
.iter()
.map(|d| d.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::Time(v) => v
.iter()
.map(|t| t.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::DateTime(v) => v
.iter()
.map(|dt| dt.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::Ints(v) => v
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::Decimals(v) => v
.iter()
.map(|n| format_f64(*n))
.collect::<Vec<_>>()
.join("\\"),
Value::U8(v) => format!("({} bytes)", v.len()),
Value::U16(v) => v
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::I16(v) => v
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::U32(v) => v
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::I32(v) => v
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::U64(v) => v
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::I64(v) => v
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>()
.join("\\"),
Value::F32(v) => v
.iter()
.map(|n| format!("{}", n))
.collect::<Vec<_>>()
.join("\\"),
Value::F64(v) => v
.iter()
.map(|n| format_f64(*n))
.collect::<Vec<_>>()
.join("\\"),
Value::Tags(v) => v
.iter()
.map(|t| format!("({:04X},{:04X})", t.group, t.element))
.collect::<Vec<_>>()
.join("\\"),
Value::Sequence(v) => format!("(Sequence with {} item(s))", v.len()),
Value::PixelData(PixelData::Native { bytes }) => {
format!("(PixelData, {} bytes)", bytes.len())
}
Value::PixelData(PixelData::Encapsulated { fragments, .. }) => {
format!("(PixelData, {} fragment(s))", fragments.len())
}
}
}
pub(crate) fn encoded_len(&self) -> usize {
match self {
Value::Empty => 0,
Value::Strings(v) => {
let total: usize = v.iter().map(|s| s.len()).sum();
total + v.len().saturating_sub(1)
}
Value::PersonNames(v) => {
let total: usize = v.iter().map(|p| p.to_string().len()).sum();
total + v.len().saturating_sub(1)
}
Value::Uid(s) => s.len(),
Value::Date(v) => v.len() * 8,
Value::Time(v) => v.len() * 14,
Value::DateTime(v) => v.len() * 26,
Value::Ints(v) => {
v.iter().map(|n| n.to_string().len()).sum::<usize>() + v.len().saturating_sub(1)
}
Value::Decimals(v) => {
v.iter().map(|n| format_f64(*n).len()).sum::<usize>() + v.len().saturating_sub(1)
}
Value::U8(v) => v.len(),
Value::U16(v) => v.len() * 2,
Value::I16(v) => v.len() * 2,
Value::U32(v) => v.len() * 4,
Value::I32(v) => v.len() * 4,
Value::U64(v) => v.len() * 8,
Value::I64(v) => v.len() * 8,
Value::F32(v) => v.len() * 4,
Value::F64(v) => v.len() * 8,
Value::Tags(v) => v.len() * 4,
Value::Sequence(_) => 0,
Value::PixelData(PixelData::Native { bytes }) => bytes.len(),
Value::PixelData(PixelData::Encapsulated { fragments, .. }) => {
fragments.iter().map(|f| f.len()).sum()
}
}
}
}
fn parse_u8_str(s: &str) -> DcmResult<u8> {
s.parse::<u8>()
.map_err(|_| DcmError::Other(format!("expected u8, got {:?}", s)))
}
fn parse_u16_str(s: &str) -> DcmResult<u16> {
s.parse::<u16>()
.map_err(|_| DcmError::Other(format!("expected u16, got {:?}", s)))
}
fn parse_u32_str(s: &str) -> DcmResult<u32> {
s.parse::<u32>()
.map_err(|_| DcmError::Other(format!("expected u32, got {:?}", s)))
}
fn format_f64(v: f64) -> String {
if v.fract() == 0.0 && v.abs() < 1e15 {
format!("{:.1}", v)
} else {
format!("{}", v)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn date_full_parse() {
let d = DicomDate::parse("20231215").unwrap();
assert_eq!(d.year, 2023);
assert_eq!(d.month, 12);
assert_eq!(d.day, 15);
}
#[test]
fn date_year_only() {
let d = DicomDate::parse("2023").unwrap();
assert_eq!(d.year, 2023);
assert_eq!(d.month, 0);
assert_eq!(d.day, 0);
}
#[test]
fn date_year_month() {
let d = DicomDate::parse("202312").unwrap();
assert_eq!(d.year, 2023);
assert_eq!(d.month, 12);
assert_eq!(d.day, 0);
}
#[test]
fn date_display_full() {
let d = DicomDate {
year: 2023,
month: 12,
day: 15,
};
assert_eq!(d.to_string(), "20231215");
}
#[test]
fn date_display_partial_year() {
let d = DicomDate {
year: 2023,
month: 0,
day: 0,
};
assert_eq!(d.to_string(), "2023");
}
#[test]
fn date_display_partial_year_month() {
let d = DicomDate {
year: 2023,
month: 12,
day: 0,
};
assert_eq!(d.to_string(), "202312");
}
#[test]
fn date_legacy_format() {
let d = DicomDate::from_da_str("2023.12.15").unwrap();
assert_eq!(d.year, 2023);
assert_eq!(d.month, 12);
assert_eq!(d.day, 15);
}
#[test]
fn date_invalid() {
assert!(DicomDate::parse("20231").is_err());
assert!(DicomDate::parse("2023121").is_err());
assert!(DicomDate::parse("abcdefgh").is_err());
}
#[test]
fn time_full_parse() {
let t = DicomTime::parse("143022.500000").unwrap();
assert_eq!(t.hour, 14);
assert_eq!(t.minute, 30);
assert_eq!(t.second, 22);
assert_eq!(t.fraction, 500000);
}
#[test]
fn time_partial_hour() {
let t = DicomTime::parse("14").unwrap();
assert_eq!(t.hour, 14);
assert_eq!(t.minute, 0);
assert_eq!(t.second, 0);
assert_eq!(t.fraction, 0);
}
#[test]
fn time_partial_hour_minute() {
let t = DicomTime::parse("1430").unwrap();
assert_eq!(t.hour, 14);
assert_eq!(t.minute, 30);
assert_eq!(t.second, 0);
}
#[test]
fn time_partial_no_fraction() {
let t = DicomTime::parse("143022").unwrap();
assert_eq!(t.hour, 14);
assert_eq!(t.minute, 30);
assert_eq!(t.second, 22);
assert_eq!(t.fraction, 0);
}
#[test]
fn time_fraction_short() {
let t = DicomTime::parse("143022.5").unwrap();
assert_eq!(t.fraction, 500000);
}
#[test]
fn time_display() {
let t = DicomTime {
hour: 14,
minute: 30,
second: 22,
fraction: 500000,
};
assert_eq!(t.to_string(), "143022.500000");
}
#[test]
fn time_display_no_fraction() {
let t = DicomTime {
hour: 14,
minute: 30,
second: 22,
fraction: 0,
};
assert_eq!(t.to_string(), "143022");
}
#[test]
fn datetime_full_parse() {
let dt = DicomDateTime::parse("20231215143022.000000+0530").unwrap();
assert_eq!(dt.date.year, 2023);
assert_eq!(dt.date.month, 12);
assert_eq!(dt.date.day, 15);
let t = dt.time.unwrap();
assert_eq!(t.hour, 14);
assert_eq!(t.minute, 30);
assert_eq!(t.second, 22);
assert_eq!(dt.offset_minutes, Some(330)); }
#[test]
fn datetime_negative_offset() {
let dt = DicomDateTime::parse("20231215143022.000000-0500").unwrap();
assert_eq!(dt.offset_minutes, Some(-300));
}
#[test]
fn datetime_no_time() {
let dt = DicomDateTime::parse("20231215").unwrap();
assert_eq!(dt.date.year, 2023);
assert!(dt.time.is_none());
assert!(dt.offset_minutes.is_none());
}
#[test]
fn datetime_display_roundtrip() {
let s = "20231215143022.500000+0530";
let dt = DicomDateTime::parse(s).unwrap();
assert_eq!(dt.to_string(), s);
}
#[test]
fn datetime_display_roundtrip_no_fraction() {
let s = "20231215143022+0530";
let dt = DicomDateTime::parse(s).unwrap();
assert_eq!(dt.to_string(), s);
}
#[test]
fn pn_simple() {
let pn = PersonName::parse("Eichelberg^Marco^^Dr.");
assert_eq!(pn.last_name(), "Eichelberg");
assert_eq!(pn.first_name(), "Marco");
assert_eq!(pn.middle_name(), "");
assert_eq!(pn.prefix(), "Dr.");
assert_eq!(pn.suffix(), "");
}
#[test]
fn pn_multi_component() {
let pn = PersonName::parse("Smith^John=\u{5C71}\u{7530}^\u{592A}\u{90CE}=\u{3084}\u{307E}\u{3060}^\u{305F}\u{308D}\u{3046}");
assert_eq!(pn.last_name(), "Smith");
assert_eq!(pn.first_name(), "John");
assert!(!pn.ideographic.is_empty());
assert!(!pn.phonetic.is_empty());
}
#[test]
fn pn_display_single_group() {
let pn = PersonName::parse("Smith^John");
assert_eq!(pn.to_string(), "Smith^John");
}
#[test]
fn pn_display_two_groups() {
let pn = PersonName::parse("Smith^John=SJ");
assert_eq!(pn.to_string(), "Smith^John=SJ");
}
#[test]
fn value_multiplicity() {
assert_eq!(Value::Empty.multiplicity(), 0);
assert_eq!(
Value::Strings(vec!["a".into(), "b".into()]).multiplicity(),
2
);
assert_eq!(Value::U16(vec![1, 2, 3]).multiplicity(), 3);
assert_eq!(Value::Uid("1.2.3".into()).multiplicity(), 1);
assert_eq!(Value::Sequence(vec![]).multiplicity(), 0);
}
#[test]
fn value_is_empty() {
assert!(Value::Empty.is_empty());
assert!(Value::Strings(vec![]).is_empty());
assert!(!Value::Strings(vec!["x".into()]).is_empty());
}
#[test]
fn value_as_string() {
let v = Value::Strings(vec!["hello".into(), "world".into()]);
assert_eq!(v.as_string(), Some("hello"));
assert_eq!(v.as_strings().unwrap().len(), 2);
}
#[test]
fn value_as_uid() {
let v = Value::Uid("1.2.840.10008.1.1".into());
assert_eq!(v.as_string(), Some("1.2.840.10008.1.1"));
}
#[test]
fn value_as_numeric() {
let v = Value::U16(vec![512]);
assert_eq!(v.as_u16(), Some(512));
let v = Value::U32(vec![65536]);
assert_eq!(v.as_u32(), Some(65536));
let v = Value::I32(vec![-1]);
assert_eq!(v.as_i32(), Some(-1));
let v = Value::F64(vec![2.78]);
assert_eq!(v.as_f64(), Some(2.78));
}
#[test]
fn value_to_display_string_strings() {
let v = Value::Strings(vec!["foo".into(), "bar".into()]);
assert_eq!(v.to_display_string(), "foo\\bar");
}
#[test]
fn value_to_display_string_u16() {
let v = Value::U16(vec![512, 256]);
assert_eq!(v.to_display_string(), "512\\256");
}
#[test]
fn value_to_display_string_sequence() {
let v = Value::Sequence(vec![]);
assert_eq!(v.to_display_string(), "(Sequence with 0 item(s))");
}
#[test]
fn value_as_bytes() {
let v = Value::U8(vec![1, 2, 3]);
assert_eq!(v.as_bytes(), Some(&[1u8, 2, 3][..]));
}
}