From 5645d3cb87be13c00d930b6f8ef62f5c4f6eb9e3 Mon Sep 17 00:00:00 2001
From: Pavel Trushkin <pavel@dcmsys.com>
Date: Mon, 10 Jun 2019 17:55:19 +0300
Subject: [PATCH] Added support for optional fractional part of a second in
 OFDateTime::setISOFormattedDateTime and OFTime::setISOFormattedTime

---
 ofstd/include/dcmtk/ofstd/ofdatime.h |   8 +-
 ofstd/include/dcmtk/ofstd/oftime.h   |   8 +-
 ofstd/libsrc/ofdatime.cc             |  60 ++++++----
 ofstd/libsrc/oftime.cc               | 163 ++++++++++++++++-----------
 ofstd/tests/tofdatim.cc              |  25 ++++
 5 files changed, 171 insertions(+), 93 deletions(-)

diff --git a/ofstd/include/dcmtk/ofstd/ofdatime.h b/ofstd/include/dcmtk/ofstd/ofdatime.h
index 4a974b4a9..371990e3f 100644
--- a/ofstd/include/dcmtk/ofstd/ofdatime.h
+++ b/ofstd/include/dcmtk/ofstd/ofdatime.h
@@ -205,10 +205,10 @@ class DCMTK_OFSTD_EXPORT OFDateTime
 
     /** set the date/time value to the given ISO formatted date/time string.
      *  The two ISO date/time formats supported by this function are
-     *  - "YYYY-MM-DD HH:MM[:SS [&ZZ:ZZ]]" (with arbitrary delimiters) and
-     *  - "YYYYMMDDHHMM[SS[&ZZZZ]]" (without delimiters, useful for DICOM datetime type)
-     *  where the brackets enclose optional parts. Please note that the optional fractional
-     *  part of a second ".FFFFFF" (see getISOFormattedDateTime()) is not yet supported.
+     *  - "YYYY-MM-DD HH:MM[:SS[.FFFFFF] [&ZZ:ZZ]]" (with arbitrary delimiters) and
+     *  - "YYYYMMDDHHMM[SS[.FFFFFF][&ZZZZ]]" (without delimiters, useful for DICOM datetime type)
+     *  where the brackets enclose optional parts. Optional fractional
+     *  part of a second ".FFFFFF" (see getISOFormattedDateTime()) is supported.
      *  @param formattedDateTime ISO formatted date/time value to be set
      *  @return OFTrue if input is valid and result variable has been set, OFFalse otherwise
      */
diff --git a/ofstd/include/dcmtk/ofstd/oftime.h b/ofstd/include/dcmtk/ofstd/oftime.h
index cfeb3d4c7..114aa59a6 100644
--- a/ofstd/include/dcmtk/ofstd/oftime.h
+++ b/ofstd/include/dcmtk/ofstd/oftime.h
@@ -230,10 +230,10 @@ class DCMTK_OFSTD_EXPORT OFTime
 
     /** set the time value to the given ISO formatted time string.
      *  The two ISO time formats supported by this function are
-     *  - "HH:MM[:SS [&ZZ:ZZ]]" (with arbitrary delimiters) and
-     *  - "HHMM[SS[&ZZZZ]]" (without delimiters)
-     *  where the brackets enclose optional parts. Please note that the optional fractional
-     *  part of a second ".FFFFFF" (see getISOFormattedTime()) is not yet supported.
+     *  - "HH:MM[:SS[.FFFFFF] [&ZZ:ZZ]]" (with arbitrary delimiters) and
+     *  - "HHMM[SS[.FFFFFF][&ZZZZ]]" (without delimiters)
+     *  where the brackets enclose optional parts. Optional fractional
+     *  part of a second ".FFFFFF" (see getISOFormattedTime()) is supported.
      *  @param formattedTime ISO formatted time value to be set
      *  @return OFTrue if input is valid and result variable has been set, OFFalse otherwise
      */
diff --git a/ofstd/libsrc/ofdatime.cc b/ofstd/libsrc/ofdatime.cc
index acda440bf..56384c63b 100644
--- a/ofstd/libsrc/ofdatime.cc
+++ b/ofstd/libsrc/ofdatime.cc
@@ -203,31 +203,47 @@ OFBool OFDateTime::setCurrentDateTime()
 
 OFBool OFDateTime::setISOFormattedDateTime(const OFString &formattedDateTime)
 {
-    OFBool result = OFFalse;
     const size_t length = formattedDateTime.length();
-    const size_t firstSep = formattedDateTime.find_first_not_of("0123456789");
-    const OFBool separators = (firstSep != OFString_npos);
-    /* check for supported formats: "YYYYMMDDHHMM[SS]" or "YYYYMMDDHHMMSS&ZZZZ" */
-    if (((((length == 12) || (length == 14)) && !separators) ||
-        ((length == 19) && (firstSep == 14) && ((formattedDateTime[14] == '+') || (formattedDateTime[14] == '-')))))
-    {
-        if (Date.setISOFormattedDate(formattedDateTime.substr(0, 8)) && Time.setISOFormattedTime(formattedDateTime.substr(8)))
-            result = OFTrue;
+
+    size_t tm_pos = 0;
+    size_t date_digits = 0;
+    size_t date_size = 0;
+
+    /* find end of YYYYMMDD or YYYY-MM-DD */
+    while (tm_pos < length && date_digits < 8) {
+        if (isdigit(formattedDateTime.at(tm_pos)))
+            date_digits++;
+        tm_pos++;
     }
-    /* "YYYY-MM-DD HH:MM[:SS]" or "YYYY-MM-DD HH:MM:SS &ZZ:ZZ" */
-    else if ((length >= 16) && separators)
-    {
-        if (Date.setISOFormattedDate(formattedDateTime.substr(0, 10)))
-        {
-            size_t pos = 10;
-            /* search for first digit of the time value (skip arbitrary separators) */
-            while ((pos < length) && !isdigit(OFstatic_cast(unsigned char, formattedDateTime.at(pos))))
-                ++pos;
-            if ((pos < length) && Time.setISOFormattedTime(formattedDateTime.substr(pos)))
-                result = OFTrue;
-        }
+
+    /* check if we found enough number of digits for date and symbols for time to continue parsing */
+    if (date_digits < 8 || tm_pos == length)
+        return OFFalse;
+
+    date_size = tm_pos;
+
+    if (formattedDateTime.at(tm_pos) == 'T') {
+        /* dateTtime, skip T */
+        tm_pos++;
+    } else {
+        /* skip heading spaces from time */
+        while (tm_pos < length && formattedDateTime.at(tm_pos) == ' ')
+            tm_pos++;
     }
-    return result;
+
+    /* check if we have enough symbols for time to continue parsing */
+    if (tm_pos == length)
+        return OFFalse;
+
+    /* parse ISO formatted date */
+    if (!Date.setISOFormattedDate(formattedDateTime.substr(0, date_size)))
+        return OFFalse;
+
+    /* parse ISO formatted time */
+    if (!Time.setISOFormattedTime(formattedDateTime.substr(tm_pos)))
+        return OFFalse;
+
+    return OFTrue;
 }
 
 
diff --git a/ofstd/libsrc/oftime.cc b/ofstd/libsrc/oftime.cc
index 75099d2b9..57ac65c68 100644
--- a/ofstd/libsrc/oftime.cc
+++ b/ofstd/libsrc/oftime.cc
@@ -19,7 +19,6 @@
  *
  */
 
-
 #include "dcmtk/config/osconfig.h"
 
 #define INCLUDE_CSTDIO
@@ -389,77 +388,115 @@ OFBool OFTime::setCurrentTime(const time_t &tt)
 
 OFBool OFTime::setISOFormattedTime(const OFString &formattedTime)
 {
-    OFBool status = OFFalse;
+    unsigned int hours, minutes;
+    double seconds, timezone;
+
+    size_t pos = 0;
     const size_t length = formattedTime.length();
-    const size_t firstSep = formattedTime.find_first_not_of("0123456789");
-    const OFBool separators = (firstSep != OFString_npos);
-    unsigned int hours, minutes, seconds;
-    /* check for supported formats: HHMM */
-    if ((length == 4) && !separators)
-    {
-        /* extract "HH" and "MM" components from time string */
-        if (sscanf(formattedTime.c_str(), "%02u%02u", &hours, &minutes) == 2)
-            status = setTime(hours, minutes, 0 /*seconds*/);
+    OFBool delim_must_present = OFFalse;
+    char delimiter = ':';
+
+    #define FTOA_TODIGIT(c)      ((c) - '0')
+
+    if ((length - pos) < 2 || !isdigit(formattedTime[pos]) || !isdigit(formattedTime[pos + 1])) {
+        /* no digits available to get hours */
+        return OFFalse;
     }
-    /* HH:MM */
-    else if ((length == 5) && separators)
-    {
-        /* extract "HH" and "MM" components from time string */
-        if (sscanf(formattedTime.c_str(), "%02u%*c%02u", &hours, &minutes) == 2)
-            status = setTime(hours, minutes, 0 /*seconds*/);
+    hours = FTOA_TODIGIT(formattedTime[pos]) * 10 + FTOA_TODIGIT(formattedTime[pos + 1]);
+
+    /* step to minutes or delimiter */
+    pos += 2;
+    if ((length - pos) < 1) {
+        /* length is not enough for ':' (possible) delimiter */
+        return OFFalse;
     }
-    /* HHMMSS */
-    else if ((length == 6) && !separators)
-    {
-        /* extract "HH", "MM" and "SS" components from time string */
-        if (sscanf(formattedTime.c_str(), "%02u%02u%02u", &hours, &minutes, &seconds) == 3)
-            status = setTime(hours, minutes, seconds);
+    /* skip delimiter ':' if it found here */
+    if (formattedTime[pos] == ':' || formattedTime[pos] == '-') {
+        /* after first ':' between HH and MM we require it everywhere */
+        delim_must_present = OFTrue;
+        delimiter = formattedTime[pos];
+        pos++;
     }
-    /* HH:MM:SS */
-    else if ((length == 8) && separators)
-    {
-        /* extract "HH", "MM" and "SS" components from time string */
-        if (sscanf(formattedTime.c_str(), "%02u%*c%02u%*c%02u", &hours, &minutes, &seconds) == 3)
-            status = setTime(hours, minutes, seconds);
+    if ((length - pos) < 2 || !isdigit(formattedTime[pos]) || !isdigit(formattedTime[pos + 1])) {
+        /* no digits available to get minutes */
+        return OFFalse;
     }
-    /* HHMMSS&ZZZZ */
-    else if ((length == 11) && (firstSep == 6) && ((formattedTime[6] == '+') || (formattedTime[6] == '-')))
-    {
-        int tzHours;
-        unsigned int tzMinutes;
-        /* extract "HH", "MM", "SS" and "&ZZZZ" components from time string */
-        if (sscanf(formattedTime.c_str(), "%02u%02u%02u%03d%02u", &hours, &minutes, &seconds, &tzHours, &tzMinutes) == 5)
-        {
-            const double timeZone = (tzHours < 0) ? tzHours - OFstatic_cast(double, tzMinutes) / 60
-                                                  : tzHours + OFstatic_cast(double, tzMinutes) / 60;
-            status = setTime(hours, minutes, seconds, timeZone);
+    minutes = FTOA_TODIGIT(formattedTime[pos]) * 10 + FTOA_TODIGIT(formattedTime[pos + 1]);
+
+    /* step to seconds or delimiter */
+    pos += 2;
+    if ((length - pos) < 1) {
+        /* HHMM or HH:MM is okay */
+        return setTime(hours, minutes, seconds);
+    }
+    /* skip one allowed ':' if it found here */
+    if (formattedTime[pos] == delimiter) {
+        pos++;
+    } else if (delim_must_present) {
+        return OFFalse;
+    }
+
+    if ((length - pos) < 2 || !isdigit(formattedTime[pos]) || !isdigit(formattedTime[pos + 1])) {
+        /* no digits available to get seconds */
+        return OFFalse;
+    }
+    seconds = FTOA_TODIGIT(formattedTime[pos]) * 10.0 + FTOA_TODIGIT(formattedTime[pos + 1]);
+
+    /* step to fractional part of seconds or time zone */
+    pos += 2;
+
+    if ((length - pos) >= 1 && formattedTime[pos] == '.') {
+        pos++;
+
+        size_t fp_start = pos, fp_size = 0;
+
+        /* we are on beginning of the fractional part of seconds */
+        while ( pos < length && isdigit(formattedTime[pos]) ) {
+            fp_size++;
+            pos++;
+        }
+        /* Part 05, sect 6.2. The FFFFFF component, if present, shall contain 1 to 6 digits. */
+        if (fp_size == 0 || fp_size > 6) {
+            return OFFalse;
+        }
+        double fp_divider = 0.1;
+        for (size_t fpi = fp_start; fpi < pos; fpi++, fp_divider /= 10.0) {
+            seconds += fp_divider * FTOA_TODIGIT(formattedTime[fpi]);
         }
     }
-    /* HH:MM:SS &ZZ:ZZ */
-    else if ((length >= 14) && separators)
-    {
-        /* first, extract "HH", "MM" and "SS" components from time string */
-        if (sscanf(formattedTime.c_str(), "%02u%*c%02u%*c%02u", &hours, &minutes, &seconds) == 3)
-        {
-            size_t pos = 8;
-            /* then search for the first digit of the time zone value (skip arbitrary separators) */
-            while ((pos < length) && !isdigit(OFstatic_cast(unsigned char, formattedTime.at(pos))))
-                ++pos;
-            if (pos < length)
-            {
-                /* and finally, extract the time zone component from the time string */
-                int tzHours;
-                unsigned int tzMinutes;
-                if (sscanf(formattedTime.c_str() + pos - 1, "%03d%*c%02u", &tzHours, &tzMinutes) == 2)
-                {
-                    const double timeZone = (tzHours < 0) ? tzHours - OFstatic_cast(double, tzMinutes) / 60
-                                                          : tzHours + OFstatic_cast(double, tzMinutes) / 60;
-                    status = setTime(hours, minutes, seconds, timeZone);
-                }
-            }
+
+    if ((length - pos) >= 1) {
+        /* extract "&ZZZZ" or "&ZZ:ZZ" components from time string */
+
+        if (delim_must_present) {
+            while (pos < length && formattedTime[pos] == ' ')
+                pos++;
         }
+
+        if ((length - pos) < 1) {
+            /* length is not enough for '&' delimiter */
+            return OFFalse;
+        }
+
+        if (((formattedTime[pos] != '+') && (formattedTime[pos] != '-')))
+            return OFFalse;
+
+        int tzHours;
+        unsigned int tzMinutes;
+
+        if (delim_must_present) {
+            if (sscanf(formattedTime.c_str() + pos, "%03d%*c%02u", &tzHours, &tzMinutes) != 2)
+                return OFFalse;
+        } else {
+            if (sscanf(formattedTime.c_str() + pos, "%03d%02u", &tzHours, &tzMinutes) != 2)
+                return OFFalse;
+        }
+        const double timeZone = (tzHours < 0) ? tzHours - OFstatic_cast(double, tzMinutes) / 60
+                                            : tzHours + OFstatic_cast(double, tzMinutes) / 60;
+        return setTime(hours, minutes, seconds, timeZone);
     }
-    return status;
+
+    return setTime(hours, minutes, seconds);
 }
 
 
diff --git a/ofstd/tests/tofdatim.cc b/ofstd/tests/tofdatim.cc
index f18250cc1..174f804d9 100644
--- a/ofstd/tests/tofdatim.cc
+++ b/ofstd/tests/tofdatim.cc
@@ -105,6 +105,17 @@ OFTEST(ofstd_OFTime)
     OFCHECK_EQUAL(time1.getTimeInSeconds(OFTrue /*useTimeZone*/, OFFalse /*normalize*/), 13599);
     OFCHECK(time1.setTimeInHours(99, 0, OFTrue /*normalized*/));
     OFCHECK_EQUAL(time1.getTimeInHours(OFTrue /*useTimeZone*/, OFFalse /*normalize*/), 3);
+    /* the "seconds" part is mandatory if fractional part is present */
+    OFCHECK(!time1.setISOFormattedTime("10:15.9"));
+    OFCHECK(time1.setISOFormattedTime("10:15:30.9"));
+    OFCHECK_EQUAL(time1.getSecond(), 30.9);
+    OFCHECK(time1.setISOFormattedTime("101530.987654"));
+    OFCHECK_EQUAL(time1.getHour(), 10);
+    OFCHECK_EQUAL(time1.getMinute(), 15);
+    OFCHECK_EQUAL(time1.getSecond(), 30.987654);
+    OFCHECK(time1.setISOFormattedTime("10:15:30.102030 +01:30"));
+    OFCHECK_EQUAL(time1.getTimeZone(), +1.50);
+    OFCHECK_EQUAL(time1.getSecond(), 30.10203);
 }
 
 
@@ -152,6 +163,20 @@ OFTEST(ofstd_OFDateTime)
     /* the "seconds" part is mandatory if time zone is present */
     OFCHECK(!dateTime2.setISOFormattedDateTime("2000-12-31 10:15 -02:30"));
     OFCHECK(!dateTime2.setISOFormattedDateTime("200012311015+0100"));
+    /* the "seconds" part is mandatory if fractional part is present */
+    OFCHECK(!dateTime1.setISOFormattedDateTime("200012311015.9"));
+    /* fractional part must contain digits only */
+    OFCHECK(!dateTime1.setISOFormattedDateTime("20001231101530.9B2"));
+    OFCHECK(dateTime1.setISOFormattedDateTime("20001231101530.9"));
+    OFCHECK(dateTime1.getISOFormattedDateTime(tmpString, OFTrue /*showSeconds*/, OFTrue /*showFraction*/,
+        OFTrue /*showTimeZone*/, OFFalse /*showDelimiter*/, "" /*dateTimeSeparator*/, "" /*timeZoneSeparator*/));
+    OFCHECK_EQUAL(tmpString, "20001231101530.900000+0000");
+    /* maximal length is 6 digits */
+    OFCHECK(!dateTime1.setISOFormattedDateTime("20001231101530.0000001"));
+    OFCHECK(dateTime1.setISOFormattedDateTime("20001231101530.123456-0130"));
+    OFCHECK(dateTime1.getISOFormattedDateTime(tmpString, OFTrue /*showSeconds*/, OFTrue /*showFraction*/,
+        OFTrue /*showTimeZone*/, OFTrue /*showDelimiter*/, " " /*dateTimeSeparator*/, " " /*timeZoneSeparator*/));
+    OFCHECK_EQUAL(tmpString, "2000-12-31 10:15:30.123456 -01:30");
 }
 
 
