Skip to content

Instantly share code, notes, and snippets.

@cbare
Created July 3, 2022 22:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cbare/6bda68b0512187a9689157bf5d5eb787 to your computer and use it in GitHub Desktop.
Save cbare/6bda68b0512187a9689157bf5d5eb787 to your computer and use it in GitHub Desktop.
Parse a Dicom formatted datetime string and return a Python datetime.
import datetime as dt
import pytest
import re
def parse_dicom_dt(dicom_dt):
"""
Parse a Dicom formatted datetime string and return a Python datetime.
The Dicom format is "YYYYMMDDHHMMSS.FFFFFF&ZZXX" described here:
https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html#table_6.2-1
"""
pattern = (
"(\d{14})" # YYYYMMDDHHMMSS
"(\.\d+)?" # .FFFFFF 6 digits of fractional seconds (optional)
"(?:([+-])(\d{2})(\d{2})?)?" # UTC offset (optional)
"$" # end of string
)
m = re.match(pattern, dicom_dt)
if m:
try:
# YYYYMMDDHHMMSS
d1 = dt.datetime.strptime(m.group(1), "%Y%m%d%H%M%S")
fractional_seconds = dt.timedelta(
seconds=float("0"+m.group(2)) if m.group(2) else 0
)
# UTC offset
offset_sign = -1 if m.group(3) == "-" else 1
offset_hrs = offset_sign * int(m.group(4) or "0")
offset_mins = offset_sign * int(m.group(5) or "0")
offset = dt.timedelta(hours=offset_hrs, minutes=offset_mins)
# Current advice is to use tz aware datetimes.
# See: https://docs.python.org/3/library/datetime.html
return (d1 + fractional_seconds - offset).replace(tzinfo=dt.timezone.utc)
except ValueError as e:
raise ValueError(f"Can't parse \"{dicom_dt}\" as a date.") from e
else:
raise ValueError(f"Can't parse \"{dicom_dt}\" as a date.")
EXAMPLES = {
"20220102112233.123456-0800": dt.datetime(2022, 1, 2, 11, 22, 33, 123456, tzinfo=dt.timezone.utc) + dt.timedelta(hours=8),
"20220102112233.01+1200": dt.datetime(2022, 1, 2, 11, 22, 33, 10000, tzinfo=dt.timezone.utc) + dt.timedelta(hours=-12),
"20220102112233+1200": dt.datetime(2022, 1, 2, 11, 22, 33, tzinfo=dt.timezone.utc) + dt.timedelta(hours=-12),
"20220102112233.1": dt.datetime(2022, 1, 2, 11, 22, 33, 100000, tzinfo=dt.timezone.utc),
"20220102112233": dt.datetime(2022, 1, 2, 11, 22, 33, tzinfo=dt.timezone.utc),
}
BAD_EXAMPLES = [
"",
"Boink!",
"20221399452233.123456-0800",
"202201021122Z3.123456-0800",
"20220102112233..123456-0800",
"20220102112233.123456@0800",
"20220102112233.123456-800",
"20220102112233.123456-12345",
]
def test_parse_dicom_dt():
for example, expected in EXAMPLES.items():
x = parse_dicom_dt(example)
assert x == expected, f"{x} not equal to {expected}"
def test_bad_dicom_dts():
for example in BAD_EXAMPLES:
with pytest.raises(Exception) as exc_info:
parse_dicom_dt(example)
print(exc_info.value)
if __name__ == "__main__":
test_parse_dicom_dt()
test_bad_dicom_dts()
@cbare
Copy link
Author

cbare commented Jul 5, 2022

This is a silly bit of code I wrote that is just slightly more flexible than using the strptime and strftime functions from the datetime standard library package along with the following format string:

DICOM_DATETIME_FORMAT = "%Y%m%d%H%M%S.%f%z"

I'm keeping this code around in case I find corner-cases not handled by that.

For details on Dicom's DateTime value representation "DT" see:
https://dicom.nema.org/medical/dicom/current/output/chtml/part05/sect_6.2.html#table_6.2-1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment