Support units in `TimeSpan::fromString()`

This commit is contained in:
Martchus 2023-11-23 20:20:07 +01:00
parent a425363eac
commit 11de58141b
2 changed files with 90 additions and 12 deletions

View File

@ -2,8 +2,10 @@
#include "./timespan.h"
#include "../conversion/stringbuilder.h"
#include "../conversion/stringconversion.h"
#include <charconv>
#include <cmath>
#include <iomanip>
#include <sstream>
@ -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<double, 4>();
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<double>(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]);
}
}

View File

@ -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);