Improve chrono utils

- Support parsing/generating ISO time stamp with
  time zone delta
- Fix minor bugs
- Improve tests
This commit is contained in:
Martchus 2016-08-30 19:59:04 +02:00
parent d6b08b8ed9
commit 12588c6928
7 changed files with 198 additions and 58 deletions

View File

@ -10,6 +10,7 @@ set(HEADER_FILES
chrono/datetime.h chrono/datetime.h
chrono/period.h chrono/period.h
chrono/timespan.h chrono/timespan.h
chrono/format.h
conversion/binaryconversion.h conversion/binaryconversion.h
conversion/binaryconversionprivate.h conversion/binaryconversionprivate.h
conversion/conversionexception.h conversion/conversionexception.h

View File

@ -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 values[6] = {0};
int *i = values; int *const dayIndex = values + 2;
for(const auto &c : str) { int *const secondsIndex = values + 5;
if(c >= '1' || c <= '0') { int *valueIndex = values;
*i *= 10; int *const valuesEnd = values + 7;
*i += c - '1'; double miliSecondsFact = 100.0, miliSeconds = 0.0;
} else if((c == '-' || c == ':' || c == '/') || (c == '.' && (i == values + 5))) { for(const char *strIndex = str; ; ++strIndex) {
++i; 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 { } 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 * \brief Parses the given ISO date time denotation provided as C-style string.
* according the given \a format. * \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.
* If \a noMilliseconds is true the date will be rounded to full seconds. * \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, TimeSpan> 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 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 * \brief Returns the string representation of the current instance using the specified \a format.
* according the given \a format. * \remarks If \a noMilliseconds is true the date will be rounded to full seconds.
* * \sa toIsoString() for ISO format
* If \a noMilliseconds is true the date will be rounded to full seconds.
*/ */
void DateTime::toString(string &result, DateTimeOutputFormat format, bool noMilliseconds) const 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'); s << setfill('0');
if(format == DateTimeOutputFormat::DateTimeAndWeekday if(format == DateTimeOutputFormat::DateTimeAndWeekday
|| format == DateTimeOutputFormat::DateTimeAndShortWeekday) || format == DateTimeOutputFormat::DateTimeAndShortWeekday)
s << printDayOfWeek(dayOfWeek(), format == DateTimeOutputFormat::DateTimeAndShortWeekday) << " "; s << printDayOfWeek(dayOfWeek(), format == DateTimeOutputFormat::DateTimeAndShortWeekday) << ' ';
if(format == DateTimeOutputFormat::DateOnly if(format == DateTimeOutputFormat::DateOnly
|| format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateAndTime
|| format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndWeekday
|| format == DateTimeOutputFormat::DateTimeAndShortWeekday) || 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 if(format == DateTimeOutputFormat::DateAndTime
|| format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndWeekday
|| format == DateTimeOutputFormat::DateTimeAndShortWeekday) || format == DateTimeOutputFormat::DateTimeAndShortWeekday)
@ -134,15 +212,32 @@ void DateTime::toString(string &result, DateTimeOutputFormat format, bool noMill
|| format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateAndTime
|| format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndWeekday
|| format == DateTimeOutputFormat::DateTimeAndShortWeekday) { || 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(); int ms = millisecond();
if(!noMilliseconds && ms > 0) { if(!noMilliseconds && ms > 0) {
s << "." << ms; s << '.' << setw(3) << ms;
} }
} }
result = s.str(); 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. * \brief Returns the string representation as C-style string for the given day of week.
* *

View File

@ -61,6 +61,8 @@ public:
static DateTime fromTime(int hour = 0, int minute = 0, int second = 0, double millisecond = 0.0); 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 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 std::string &str);
static DateTime fromString(const char *str);
static std::pair<DateTime, TimeSpan> fromIsoString(const char *str);
static DateTime fromTimeStamp(time_t timeStamp); static DateTime fromTimeStamp(time_t timeStamp);
static DateTime fromTimeStampGmt(time_t timeStamp); static DateTime fromTimeStampGmt(time_t timeStamp);
@ -81,6 +83,7 @@ public:
constexpr bool isSameDay(const DateTime &other) const; constexpr bool isSameDay(const DateTime &other) const;
std::string toString(DateTimeOutputFormat format = DateTimeOutputFormat::DateAndTime, bool noMilliseconds = false) 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; 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 const char *printDayOfWeek(DayOfWeek dayOfWeek, bool abbreviation = false);
static constexpr DateTime eternity(); static constexpr DateTime eternity();
@ -121,6 +124,8 @@ private:
static const int m_daysInMonth366[12]; static const int m_daysInMonth366[12];
}; };
/*! /*!
* \brief Constructs a DateTime. * \brief Constructs a DateTime.
*/ */
@ -162,6 +167,14 @@ inline DateTime DateTime::fromDateAndTime(int year, int month, int day, int hour
return DateTime(); 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. * \brief Gets the number of ticks which represent the value of the current instance.
*/ */

18
chrono/format.h Normal file
View File

@ -0,0 +1,18 @@
#ifndef CHRONO_FORMAT_H
#define CHRONO_FORMAT_H
#include "./datetime.h"
#include <ostream>
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

View File

@ -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) TimeSpan TimeSpan::fromString(const char *str, char separator)
{
return TimeSpan::fromString(str, ':');
}
/*!
* \brief Parses the given std::string \a str as TimeSpan.
*/
TimeSpan TimeSpan::fromString(const string &str, char separator)
{ {
vector<double> parts; vector<double> parts;
string::size_type start = 0; size_t partsSize = 1;
string::size_type end = str.find(separator, start); for(const char *i = str; *i; ++i) {
while(true) { *i == separator && ++partsSize;
parts.push_back(stringToNumber<double>(str.substr(start, end - start)));
if(end == string::npos) {
break;
}
start = end + 1;
if(start >= str.size()) {
break;
}
end = str.find(separator, start);
} }
parts.reserve(partsSize);
for(const char *i = str; ;) {
if(*i == separator) {
parts.emplace_back(stringToNumber<double>(string(str, i)));
str = ++i;
} else if(*i == '\0') {
parts.emplace_back(stringToNumber<double>(string(str, i)));
break;
} else {
++i;
}
}
switch(parts.size()) { switch(parts.size()) {
case 0: case 0:
return TimeSpan(); return TimeSpan();

View File

@ -37,8 +37,8 @@ public:
static constexpr TimeSpan fromMinutes(double minutes); static constexpr TimeSpan fromMinutes(double minutes);
static constexpr TimeSpan fromHours(double hours); static constexpr TimeSpan fromHours(double hours);
static constexpr TimeSpan fromDays(double days); 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 negativeInfinity();
static constexpr TimeSpan infinity(); static constexpr TimeSpan infinity();
@ -134,6 +134,14 @@ constexpr inline TimeSpan TimeSpan::fromDays(double days)
return TimeSpan(static_cast<int64>(days * static_cast<double>(m_ticksPerDay))); return TimeSpan(static_cast<int64>(days * static_cast<double>(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. * \brief Constructs a new instace of the TimeSpan class with the minimal number of ticks.
*/ */

View File

@ -1,6 +1,7 @@
#include "../chrono/datetime.h" #include "../chrono/datetime.h"
#include "../chrono/timespan.h" #include "../chrono/timespan.h"
#include "../chrono/period.h" #include "../chrono/period.h"
#include "../chrono/format.h"
#include "../conversion/conversionexception.h" #include "../conversion/conversionexception.h"
#include <cppunit/extensions/HelperMacros.h> #include <cppunit/extensions/HelperMacros.h>
@ -42,20 +43,20 @@ CPPUNIT_TEST_SUITE_REGISTRATION(ChronoTests);
void ChronoTests::testDateTime() void ChronoTests::testDateTime()
{ {
// test year(), month(), ... // test year(), month(), ...
auto test1 = DateTime::fromDateAndTime(2012, 2, 29, 15, 34, 20, 33.0); const auto test1 = DateTime::fromDateAndTime(2012, 2, 29, 15, 34, 20, 33.0);
CPPUNIT_ASSERT(test1.year() == 2012); CPPUNIT_ASSERT_EQUAL(2012, test1.year());
CPPUNIT_ASSERT(test1.month() == 2); CPPUNIT_ASSERT_EQUAL(2, test1.month());
CPPUNIT_ASSERT(test1.day() == 29); CPPUNIT_ASSERT_EQUAL(29, test1.day());
CPPUNIT_ASSERT(test1.minute() == 34); CPPUNIT_ASSERT_EQUAL(34, test1.minute());
CPPUNIT_ASSERT(test1.second() == 20); CPPUNIT_ASSERT_EQUAL(20, test1.second());
CPPUNIT_ASSERT(test1.millisecond() == 33); CPPUNIT_ASSERT_EQUAL(33, test1.millisecond());
CPPUNIT_ASSERT(test1.dayOfWeek() == DayOfWeek::Wednesday); 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.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() // 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"); CPPUNIT_ASSERT(test2.toString(DateTimeOutputFormat::DateTimeAndShortWeekday) == "Tue 2016-01-26 20:32:11");
// test whether ConversionException() is thrown when invalid values are specified // 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, 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, 4, 31, 15, 0, 20, 33), ConversionException);
CPPUNIT_ASSERT_THROW(DateTime::fromDateAndTime(2012, 3, 31, 15, 0, 61, 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));
} }
/*! /*!