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/period.h
chrono/timespan.h
chrono/format.h
conversion/binaryconversion.h
conversion/binaryconversionprivate.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 *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, 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
{
@ -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.
*

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 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<DateTime, TimeSpan> 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.
*/

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)
{
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<double> parts;
string::size_type start = 0;
string::size_type end = str.find(separator, start);
while(true) {
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);
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<double>(string(str, i)));
str = ++i;
} else if(*i == '\0') {
parts.emplace_back(stringToNumber<double>(string(str, i)));
break;
} else {
++i;
}
}
switch(parts.size()) {
case 0:
return TimeSpan();

View File

@ -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<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.
*/

View File

@ -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 <cppunit/extensions/HelperMacros.h>
@ -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));
}
/*!