From 12588c6928ebf202b3b7bd07a3606acd24914d3b Mon Sep 17 00:00:00 2001 From: Martchus Date: Tue, 30 Aug 2016 19:59:04 +0200 Subject: [PATCH] Improve chrono utils - Support parsing/generating ISO time stamp with time zone delta - Fix minor bugs - Improve tests --- CMakeLists.txt | 1 + chrono/datetime.cpp | 143 +++++++++++++++++++++++++++++++++++------- chrono/datetime.h | 13 ++++ chrono/format.h | 18 ++++++ chrono/timespan.cpp | 41 ++++++------ chrono/timespan.h | 12 +++- tests/chronotests.cpp | 28 ++++++--- 7 files changed, 198 insertions(+), 58 deletions(-) create mode 100644 chrono/format.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 8dc2a03..5da8d7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,6 +10,7 @@ set(HEADER_FILES chrono/datetime.h chrono/period.h chrono/timespan.h + chrono/format.h conversion/binaryconversion.h conversion/binaryconversionprivate.h conversion/conversionexception.h diff --git a/chrono/datetime.cpp b/chrono/datetime.cpp index 48f8ec0..93d5f07 100644 --- a/chrono/datetime.cpp +++ b/chrono/datetime.cpp @@ -76,30 +76,109 @@ DateTime DateTime::fromTimeStampGmt(time_t timeStamp) } /*! - * \brief Parses the given std::string \a str as DateTime. + * \brief Parses the given C-style string as DateTime. */ -DateTime DateTime::fromString(const string &str) +DateTime DateTime::fromString(const char *str) { - int values[7] = {0}; - int *i = values; - for(const auto &c : str) { - if(c >= '1' || c <= '0') { - *i *= 10; - *i += c - '1'; - } else if((c == '-' || c == ':' || c == '/') || (c == '.' && (i == values + 5))) { - ++i; + int values[6] = {0}; + int *const dayIndex = values + 2; + int *const secondsIndex = values + 5; + int *valueIndex = values; + int *const valuesEnd = values + 7; + double miliSecondsFact = 100.0, miliSeconds = 0.0; + for(const char *strIndex = str; ; ++strIndex) { + const char c = *strIndex; + if(c <= '9' && c >= '0') { + if(valueIndex > secondsIndex) { + miliSeconds += (c - '0') * miliSecondsFact; + miliSecondsFact /= 10; + } else { + *valueIndex *= 10; + *valueIndex += c - '0'; + } + } else if((c == '-' || c == ':' || c == '/') || (c == '.' && (valueIndex == secondsIndex)) || (c == ' ' && (valueIndex == dayIndex))) { + if(++valueIndex == valuesEnd) { + break; // just ignore further values for now + } + } else if(c == '\0') { + break; } else { - throw ConversionException(string("string contains unexpected character ") + c); + throw ConversionException(string("unexpected ") + c); } } - return DateTime::fromDateAndTime(values[0], values[1], values[2], values[3], values[4], values[5], 100.0 * values[6]); + return DateTime::fromDateAndTime(values[0], values[1], *dayIndex, values[3], values[4], *secondsIndex, miliSeconds); } /*! - * \brief Converts the value of the current DateTime object to its equivalent std::string representation - * according the given \a format. - * - * If \a noMilliseconds is true the date will be rounded to full seconds. + * \brief Parses the given ISO date time denotation provided as C-style string. + * \returns Returns a pair where the first value is the parsed UTC DateTime and the second value + * a TimeSpan which can be added to the first value to get the local DateTime. + * \remarks Not sure whether it is actually ISO conform, but it parses denotations like + * "2016-08-29T21:32:31.588539814+02:00". + */ +std::pair DateTime::fromIsoString(const char *str) +{ + int values[9] = {0}; + int *const dayIndex = values + 2; + int *const hourIndex = values + 3; + int *const secondsIndex = values + 5; + int *const miliSecondsIndex = values + 6; + int *const deltaHourIndex = values + 7; + int *valueIndex = values; + bool deltaNegative = false; + double miliSecondsFact = 100.0, miliSeconds = 0.0; + for(const char *strIndex = str; ; ++strIndex) { + const char c = *strIndex; + if(c <= '9' && c >= '0') { + if(valueIndex == miliSecondsIndex) { + miliSeconds += (c - '0') * miliSecondsFact; + miliSecondsFact /= 10; + } else { + *valueIndex *= 10; + *valueIndex += c - '0'; + } + } else if(c == 'T') { + if(++valueIndex != hourIndex) { + throw ConversionException("\"T\" expected before hour"); + } + } else if(c == '-') { + if(valueIndex < dayIndex) { + ++valueIndex; + } else { + throw ConversionException("unexpected \"-\" after day"); + } + } else if(c == '.') { + if(valueIndex != secondsIndex) { + throw ConversionException("unexpected \".\""); + } else { + ++valueIndex; + } + } else if(c == ':') { + if(valueIndex < hourIndex) { + throw ConversionException("unexpected \":\" before hour"); + } else if(valueIndex == secondsIndex) { + throw ConversionException("unexpected \":\" after second"); + } else { + ++valueIndex; + } + } else if((c == '+') && (++valueIndex == deltaHourIndex)) { + deltaNegative = false; + } else if((c == '-') && (++valueIndex == deltaHourIndex)) { + deltaNegative = true; + } else if(c == '\0') { + break; + } else { + throw ConversionException(string("unexpected \"") + c + '\"'); + } + } + deltaNegative && (*deltaHourIndex = -*deltaHourIndex); + return make_pair(DateTime::fromDateAndTime(values[0], values[1], *dayIndex, *hourIndex, values[4], *secondsIndex, miliSeconds), TimeSpan::fromMinutes(*deltaHourIndex * 60 + values[8])); +} + +/*! + * \brief Returns the string representation of the current instance using the specified \a format. + * \remarks If \a noMilliseconds is true the date will be rounded to full seconds. + * \sa toIsoString() for ISO format */ string DateTime::toString(DateTimeOutputFormat format, bool noMilliseconds) const { @@ -109,10 +188,9 @@ string DateTime::toString(DateTimeOutputFormat format, bool noMilliseconds) cons } /*! - * \brief Converts the value of the current DateTime object to its equivalent std::string representation - * according the given \a format. - * - * If \a noMilliseconds is true the date will be rounded to full seconds. + * \brief Returns the string representation of the current instance using the specified \a format. + * \remarks If \a noMilliseconds is true the date will be rounded to full seconds. + * \sa toIsoString() for ISO format */ void DateTime::toString(string &result, DateTimeOutputFormat format, bool noMilliseconds) const { @@ -120,12 +198,12 @@ void DateTime::toString(string &result, DateTimeOutputFormat format, bool noMill s << setfill('0'); if(format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndShortWeekday) - s << printDayOfWeek(dayOfWeek(), format == DateTimeOutputFormat::DateTimeAndShortWeekday) << " "; + s << printDayOfWeek(dayOfWeek(), format == DateTimeOutputFormat::DateTimeAndShortWeekday) << ' '; if(format == DateTimeOutputFormat::DateOnly || format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndShortWeekday) - s << setw(4) << year() << "-" << setw(2) << month() << "-" << setw(2) << day(); + s << setw(4) << year() << '-' << setw(2) << month() << '-' << setw(2) << day(); if(format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndShortWeekday) @@ -134,15 +212,32 @@ void DateTime::toString(string &result, DateTimeOutputFormat format, bool noMill || format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndShortWeekday) { - s << setw(2) << hour() << ":" << setw(2) << minute() << ":" << setw(2) << second(); + s << setw(2) << hour() << ':' << setw(2) << minute() << ':' << setw(2) << second(); int ms = millisecond(); if(!noMilliseconds && ms > 0) { - s << "." << ms; + s << '.' << setw(3) << ms; } } result = s.str(); } +/*! + * \brief Returns the string representation of the current instance in the ISO format, + * eg. 2016-08-29T21:32:31.588539814+02:00. + */ +string DateTime::toIsoString(TimeSpan timeZoneDelta) const +{ + stringstream s(stringstream::in | stringstream::out); + s << setfill('0'); + s << setw(4) << year() << '-' << setw(2) << month() << '-' << setw(2) << day() + << 'T' << setw(2) << hour() << ':' << setw(2) << minute() << ':' << setw(2) << second() << '.' << setw(3) << millisecond(); + if(!timeZoneDelta.isNull()) { + s << (timeZoneDelta.isNegative() ? '-' : '+'); + s << setw(2) << timeZoneDelta.hours() << ':' << setw(2) << timeZoneDelta.minutes(); + } + return s.str(); +} + /*! * \brief Returns the string representation as C-style string for the given day of week. * diff --git a/chrono/datetime.h b/chrono/datetime.h index 7c6a73c..7f96160 100644 --- a/chrono/datetime.h +++ b/chrono/datetime.h @@ -61,6 +61,8 @@ public: static DateTime fromTime(int hour = 0, int minute = 0, int second = 0, double millisecond = 0.0); static DateTime fromDateAndTime(int year = 1, int month = 1, int day = 1, int hour = 0, int minute = 0, int second = 0, double millisecond = 0.0); static DateTime fromString(const std::string &str); + static DateTime fromString(const char *str); + static std::pair fromIsoString(const char *str); static DateTime fromTimeStamp(time_t timeStamp); static DateTime fromTimeStampGmt(time_t timeStamp); @@ -81,6 +83,7 @@ public: constexpr bool isSameDay(const DateTime &other) const; std::string toString(DateTimeOutputFormat format = DateTimeOutputFormat::DateAndTime, bool noMilliseconds = false) const; void toString(std::string &result, DateTimeOutputFormat format = DateTimeOutputFormat::DateAndTime, bool noMilliseconds = false) const; + std::string toIsoString(TimeSpan delta) const; static const char *printDayOfWeek(DayOfWeek dayOfWeek, bool abbreviation = false); static constexpr DateTime eternity(); @@ -121,6 +124,8 @@ private: static const int m_daysInMonth366[12]; }; + + /*! * \brief Constructs a DateTime. */ @@ -162,6 +167,14 @@ inline DateTime DateTime::fromDateAndTime(int year, int month, int day, int hour return DateTime(); } +/*! + * \brief Parses the given std::string as DateTime. + */ +inline DateTime DateTime::fromString(const std::string &str) +{ + return fromString(str.data()); +} + /*! * \brief Gets the number of ticks which represent the value of the current instance. */ diff --git a/chrono/format.h b/chrono/format.h new file mode 100644 index 0000000..30de10d --- /dev/null +++ b/chrono/format.h @@ -0,0 +1,18 @@ +#ifndef CHRONO_FORMAT_H +#define CHRONO_FORMAT_H + +#include "./datetime.h" + +#include + +inline std::ostream &operator<< (std::ostream &out, const ChronoUtilities::DateTime &value) +{ + return out << value.toString(ChronoUtilities::DateTimeOutputFormat::DateAndTime, false); +} + +inline std::ostream &operator<< (std::ostream &out, const ChronoUtilities::TimeSpan &value) +{ + return out << value.toString(ChronoUtilities::TimeSpanOutputFormat::Normal, false); +} + +#endif // CHRONO_FORMAT_H diff --git a/chrono/timespan.cpp b/chrono/timespan.cpp index d1931c6..7913f40 100644 --- a/chrono/timespan.cpp +++ b/chrono/timespan.cpp @@ -18,32 +18,29 @@ using namespace ConversionUtilities; */ /*! - * \brief Parses the given std::string \a str as TimeSpan. + * \brief Parses the given C-style string as TimeSpan. */ -TimeSpan TimeSpan::fromString(const string &str) -{ - return TimeSpan::fromString(str, ':'); -} - -/*! - * \brief Parses the given std::string \a str as TimeSpan. - */ -TimeSpan TimeSpan::fromString(const string &str, char separator) +TimeSpan TimeSpan::fromString(const char *str, char separator) { vector parts; - string::size_type start = 0; - string::size_type end = str.find(separator, start); - while(true) { - parts.push_back(stringToNumber(str.substr(start, end - start))); - if(end == string::npos) { - break; - } - start = end + 1; - if(start >= str.size()) { - break; - } - end = str.find(separator, start); + size_t partsSize = 1; + for(const char *i = str; *i; ++i) { + *i == separator && ++partsSize; } + parts.reserve(partsSize); + + for(const char *i = str; ;) { + if(*i == separator) { + parts.emplace_back(stringToNumber(string(str, i))); + str = ++i; + } else if(*i == '\0') { + parts.emplace_back(stringToNumber(string(str, i))); + break; + } else { + ++i; + } + } + switch(parts.size()) { case 0: return TimeSpan(); diff --git a/chrono/timespan.h b/chrono/timespan.h index ef4ccde..12efb9b 100644 --- a/chrono/timespan.h +++ b/chrono/timespan.h @@ -37,8 +37,8 @@ public: static constexpr TimeSpan fromMinutes(double minutes); static constexpr TimeSpan fromHours(double hours); static constexpr TimeSpan fromDays(double days); - static TimeSpan fromString(const std::string &str); - static TimeSpan fromString(const std::string &str, char separator); + static TimeSpan fromString(const std::string &str, char separator = ':'); + static TimeSpan fromString(const char *str, char separator); static constexpr TimeSpan negativeInfinity(); static constexpr TimeSpan infinity(); @@ -134,6 +134,14 @@ constexpr inline TimeSpan TimeSpan::fromDays(double days) return TimeSpan(static_cast(days * static_cast(m_ticksPerDay))); } +/*! + * \brief Parses the given std::string as TimeSpan. + */ +inline TimeSpan TimeSpan::fromString(const std::string &str, char separator) +{ + return TimeSpan::fromString(str.data(), separator); +} + /*! * \brief Constructs a new instace of the TimeSpan class with the minimal number of ticks. */ diff --git a/tests/chronotests.cpp b/tests/chronotests.cpp index 777bbd7..3972f84 100644 --- a/tests/chronotests.cpp +++ b/tests/chronotests.cpp @@ -1,6 +1,7 @@ #include "../chrono/datetime.h" #include "../chrono/timespan.h" #include "../chrono/period.h" +#include "../chrono/format.h" #include "../conversion/conversionexception.h" #include @@ -42,20 +43,20 @@ CPPUNIT_TEST_SUITE_REGISTRATION(ChronoTests); void ChronoTests::testDateTime() { // test year(), month(), ... - auto test1 = DateTime::fromDateAndTime(2012, 2, 29, 15, 34, 20, 33.0); - CPPUNIT_ASSERT(test1.year() == 2012); - CPPUNIT_ASSERT(test1.month() == 2); - CPPUNIT_ASSERT(test1.day() == 29); - CPPUNIT_ASSERT(test1.minute() == 34); - CPPUNIT_ASSERT(test1.second() == 20); - CPPUNIT_ASSERT(test1.millisecond() == 33); + const auto test1 = DateTime::fromDateAndTime(2012, 2, 29, 15, 34, 20, 33.0); + CPPUNIT_ASSERT_EQUAL(2012, test1.year()); + CPPUNIT_ASSERT_EQUAL(2, test1.month()); + CPPUNIT_ASSERT_EQUAL(29, test1.day()); + CPPUNIT_ASSERT_EQUAL(34, test1.minute()); + CPPUNIT_ASSERT_EQUAL(20, test1.second()); + CPPUNIT_ASSERT_EQUAL(33, test1.millisecond()); CPPUNIT_ASSERT(test1.dayOfWeek() == DayOfWeek::Wednesday); - CPPUNIT_ASSERT(test1.dayOfYear() == (31 + 29)); + CPPUNIT_ASSERT_EQUAL((31 + 29), test1.dayOfYear()); CPPUNIT_ASSERT(test1.isLeapYear()); - CPPUNIT_ASSERT(test1.toString(DateTimeOutputFormat::DateTimeAndShortWeekday) == "Wed 2012-02-29 15:34:20.33"); + CPPUNIT_ASSERT_EQUAL(string("Wed 2012-02-29 15:34:20.033"), test1.toString(DateTimeOutputFormat::DateTimeAndShortWeekday)); // test fromTimeStamp() - auto test2 = DateTime::fromTimeStampGmt(1453840331); + const auto test2 = DateTime::fromTimeStampGmt(1453840331); CPPUNIT_ASSERT(test2.toString(DateTimeOutputFormat::DateTimeAndShortWeekday) == "Tue 2016-01-26 20:32:11"); // test whether ConversionException() is thrown when invalid values are specified @@ -63,6 +64,13 @@ void ChronoTests::testDateTime() CPPUNIT_ASSERT_THROW(DateTime::fromDateAndTime(2012, 2, 29, 15, 61, 20, 33), ConversionException); CPPUNIT_ASSERT_THROW(DateTime::fromDateAndTime(2012, 4, 31, 15, 0, 20, 33), ConversionException); CPPUNIT_ASSERT_THROW(DateTime::fromDateAndTime(2012, 3, 31, 15, 0, 61, 33), ConversionException); + + // test fromString()/toString() + CPPUNIT_ASSERT_EQUAL(test1, DateTime::fromString("2012-02-29 15:34:20.033")); + CPPUNIT_ASSERT_EQUAL(string("2012-02-29 15:34:20.033"), test1.toString(DateTimeOutputFormat::DateAndTime, false)); + CPPUNIT_ASSERT_THROW(TimeSpan::fromString("2012-02-29 15:34:34:20.033"), ConversionException); + const auto test3 = DateTime::fromIsoString("2016-08-29T21:32:31.125+02:00"); + CPPUNIT_ASSERT_EQUAL(string("2016-08-29T21:32:31.125+02:00"), test3.first.toIsoString(test3.second)); } /*!