diff --git a/chrono/timespan.cpp b/chrono/timespan.cpp index 7499baa..86e6b32 100644 --- a/chrono/timespan.cpp +++ b/chrono/timespan.cpp @@ -2,8 +2,10 @@ #include "./timespan.h" +#include "../conversion/stringbuilder.h" #include "../conversion/stringconversion.h" +#include #include #include #include @@ -31,9 +33,13 @@ namespace CppUtilities { * \brief Parses the given C-style string as TimeSpan. * \throws Throws a ConversionException if the specified \a str does not match the expected format. * - * The expected format is "days:hours:minutes:seconds", eg. "5:31:4.521" for 5 hours, 31 minutes + * The expected format is "days:hours:minutes:seconds", e.g. "5:31:4.521" for 5 hours, 31 minutes * and 4.521 seconds. So parts at the front can be omitted and the parts can be fractions. The - * colon can be changed by specifying another \a separator. + * colon can be changed by specifying another \a separator. White-spaces before and after parts + * are ignored. + * + * It is also possible to specify one or more values with a unit, e.g. "2w 1d 5h 1m 0.5s". + * The units "w" (weeks), "d" (days), "h" (hours), "m" (minutes) and "s" (seconds) are supported. */ TimeSpan TimeSpan::fromString(const char *str, char separator) { @@ -43,32 +49,98 @@ TimeSpan TimeSpan::fromString(const char *str, char separator) auto parts = std::array(); auto partsPresent = std::size_t(); + auto specificationsWithUnits = TimeSpan(); + for (const char *i = str;; ++i) { + // skip over white-spaces + if (*i == ' ' && i == str) { + str = i + 1; + continue; + } + + // consider non-separator and non-terminator characters as part to be interpreted as number if (*i != separator && *i != '\0') { continue; } - if (partsPresent >= 4) { - throw ConversionException("the time span specifications contains too many separators"); + + // allow only up to 4 parts (days, hours, minutes and seconds) + if (partsPresent == 4) { + throw ConversionException("too many separators/parts"); } - const auto part = std::string_view(str, i - str); - parts[partsPresent++] = part.empty() ? 0.0 : stringToNumber(part); + + // parse value of the part + auto part = 0.0; + auto valueWithUnit = TimeSpan(); + if (str != i) { + // parse value of the part as double + const auto res = std::from_chars(str, i, part); + if (res.ec != std::errc()) { + const auto part = std::string_view(str, i - str); + if (res.ec == std::errc::result_out_of_range) { + throw ConversionException(argsToString("part \"", part, "\" is too large")); + } else { + throw ConversionException(argsToString("part \"", part, "\" cannot be interpreted as floating point number")); + } + } + // handle remaining characters; detect a possibly present unit suffix + for (const char *suffix = res.ptr; suffix != i; ++suffix) { + if (*suffix == ' ') { + continue; + } + if (valueWithUnit.isNull()) { + switch (*suffix) { + case 'w': + valueWithUnit = TimeSpan::fromDays(7.0 * part); + continue; + case 'd': + valueWithUnit = TimeSpan::fromDays(part); + continue; + case 'h': + valueWithUnit = TimeSpan::fromHours(part); + continue; + case 'm': + valueWithUnit = TimeSpan::fromMinutes(part); + continue; + case 's': + valueWithUnit = TimeSpan::fromSeconds(part); + continue; + default: + ; + } + } + if (*suffix >= '0' && *suffix <= '9') { + str = i = suffix; + break; + } + throw ConversionException(argsToString("unexpected character \"", *suffix, '\"')); + } + } + + // set part value; add value with unit + if (valueWithUnit.isNull()) { + parts[partsPresent++] = part; + } else { + specificationsWithUnits += valueWithUnit; + } + + // expect next part starting after the separator or stop if terminator reached if (*i == separator) { str = i + 1; - } - if (*i == '\0') { + } else if (*i == '\0') { break; } } + // compute and return total value from specifications with units and parts switch (partsPresent) { case 1: - return TimeSpan::fromSeconds(parts.front()); + return specificationsWithUnits + TimeSpan::fromSeconds(parts.front()); case 2: - return TimeSpan::fromMinutes(parts.front()) + TimeSpan::fromSeconds(parts[1]); + return specificationsWithUnits + TimeSpan::fromMinutes(parts.front()) + TimeSpan::fromSeconds(parts[1]); case 3: - return TimeSpan::fromHours(parts.front()) + TimeSpan::fromMinutes(parts[1]) + TimeSpan::fromSeconds(parts[2]); + return specificationsWithUnits + TimeSpan::fromHours(parts.front()) + TimeSpan::fromMinutes(parts[1]) + TimeSpan::fromSeconds(parts[2]); default: - return TimeSpan::fromDays(parts.front()) + TimeSpan::fromHours(parts[1]) + TimeSpan::fromMinutes(parts[2]) + TimeSpan::fromSeconds(parts[3]); + return specificationsWithUnits + TimeSpan::fromDays(parts.front()) + TimeSpan::fromHours(parts[1]) + TimeSpan::fromMinutes(parts[2]) + TimeSpan::fromSeconds(parts[3]); } } diff --git a/tests/chronotests.cpp b/tests/chronotests.cpp index 0afd4d8..f75ceb7 100644 --- a/tests/chronotests.cpp +++ b/tests/chronotests.cpp @@ -323,6 +323,12 @@ void ChronoTests::testTimeSpan() CPPUNIT_ASSERT_EQUAL(TimeSpan::fromMinutes(5.5), TimeSpan::fromString("5:30")); CPPUNIT_ASSERT_EQUAL(TimeSpan::fromHours(7.0) + TimeSpan::fromMinutes(5.5), TimeSpan::fromString("7:5:30")); CPPUNIT_ASSERT_EQUAL(TimeSpan::fromDays(14.0), TimeSpan::fromString("14:::")); + CPPUNIT_ASSERT_EQUAL(TimeSpan::fromDays(14.0), TimeSpan::fromString("14d")); + CPPUNIT_ASSERT_EQUAL(TimeSpan::fromDays(14.0) + TimeSpan::fromHours(5.0), TimeSpan::fromString("14d 5h")); + CPPUNIT_ASSERT_EQUAL(TimeSpan::fromDays(14.0) + TimeSpan::fromMinutes(5.0), TimeSpan::fromString(" 14d 5m")); + CPPUNIT_ASSERT_EQUAL(TimeSpan::fromDays(14.0) + TimeSpan::fromMinutes(5.0) + TimeSpan::fromSeconds(24.5), TimeSpan::fromString("2 w 24.5s 5m ")); + CPPUNIT_ASSERT_EQUAL(TimeSpan::fromDays(14.0) + TimeSpan::fromSeconds(24.5), TimeSpan::fromString("2 w 24.5")); + CPPUNIT_ASSERT_EQUAL(TimeSpan::fromDays(14.0) + TimeSpan::fromString("1:2:3:4"), TimeSpan::fromString("2 w 1:2:3:4")); CPPUNIT_ASSERT_THROW(TimeSpan::fromString("2:34a:53:32.5"), ConversionException); CPPUNIT_ASSERT_THROW(TimeSpan::fromString("1:2:3:4:5"), ConversionException);