chrono utils: Improve handling micro- and nanoseconds

DateTime and TimeSpan have an accuracy of 100 nanoseconds.

This commit ensures this is also handled in toString() and
fromString() methods and adds relevant convenience methods.
This commit is contained in:
Martchus 2017-08-23 23:04:22 +02:00
parent 9c8bb44843
commit 357ede4ee7
6 changed files with 145 additions and 27 deletions

View File

@ -127,8 +127,8 @@ set(META_APP_AUTHOR "Martchus")
set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}") set(META_APP_URL "https://github.com/${META_APP_AUTHOR}/${META_PROJECT_NAME}")
set(META_APP_DESCRIPTION "Common C++ classes and routines used by my applications such as argument parser, IO and conversion utilities") set(META_APP_DESCRIPTION "Common C++ classes and routines used by my applications such as argument parser, IO and conversion utilities")
set(META_VERSION_MAJOR 4) set(META_VERSION_MAJOR 4)
set(META_VERSION_MINOR 9) set(META_VERSION_MINOR 10)
set(META_VERSION_PATCH 2) set(META_VERSION_PATCH 0)
# find required 3rd party libraries # find required 3rd party libraries
include(3rdParty) include(3rdParty)

View File

@ -222,7 +222,19 @@ string DateTime::toIsoString(TimeSpan timeZoneDelta) const
stringstream s(stringstream::in | stringstream::out); stringstream s(stringstream::in | stringstream::out);
s << setfill('0'); s << setfill('0');
s << setw(4) << year() << '-' << setw(2) << month() << '-' << setw(2) << day() << 'T' << setw(2) << hour() << ':' << setw(2) << minute() << ':' s << setw(4) << year() << '-' << setw(2) << month() << '-' << setw(2) << day() << 'T' << setw(2) << hour() << ':' << setw(2) << minute() << ':'
<< setw(2) << second() << '.' << setw(3) << millisecond(); << setw(2) << second();
const int milli(millisecond());
const int micro(microsecond());
const int nano(nanosecond());
if (milli || micro || nano) {
s << '.' << setw(3) << milli;
if (micro || nano) {
s << setw(3) << micro;
if (nano) {
s << nano / TimeSpan::nanosecondsPerTick;
}
}
}
if (!timeZoneDelta.isNull()) { if (!timeZoneDelta.isNull()) {
s << (timeZoneDelta.isNegative() ? '-' : '+'); s << (timeZoneDelta.isNegative() ? '-' : '+');
s << setw(2) << timeZoneDelta.hours() << ':' << setw(2) << timeZoneDelta.minutes(); s << setw(2) << timeZoneDelta.hours() << ':' << setw(2) << timeZoneDelta.minutes();
@ -302,13 +314,13 @@ uint64 DateTime::dateToTicks(int year, int month, int day)
if (!inRangeInclMax(month, 1, 12)) { if (!inRangeInclMax(month, 1, 12)) {
throw ConversionException("month is out of range"); throw ConversionException("month is out of range");
} }
const int *daysToMonth = isLeapYear(year) ? m_daysToMonth366 : m_daysToMonth365; const auto *daysToMonth = reinterpret_cast<const unsigned int *>(isLeapYear(year) ? m_daysToMonth366 : m_daysToMonth365);
int passedMonth = month - 1; int passedMonth = month - 1;
if (!inRangeInclMax(day, 1, daysToMonth[month] - daysToMonth[passedMonth])) { if (!inRangeInclMax(day, 1, daysToMonth[month] - daysToMonth[passedMonth])) {
throw ConversionException("day is out of range"); throw ConversionException("day is out of range");
} }
int passedYears = year - 1; const auto passedYears = static_cast<unsigned int>(year - 1);
int passedDays = day - 1; const auto passedDays = static_cast<unsigned int>(day - 1);
return (passedYears * m_daysPerYear + passedYears / 4 - passedYears / 100 + passedYears / 400 + daysToMonth[passedMonth] + passedDays) return (passedYears * m_daysPerYear + passedYears / 4 - passedYears / 100 + passedYears / 400 + daysToMonth[passedMonth] + passedDays)
* TimeSpan::m_ticksPerDay; * TimeSpan::m_ticksPerDay;
} }
@ -330,8 +342,8 @@ uint64 DateTime::timeToTicks(int hour, int minute, int second, double millisecon
if (!inRangeExclMax(millisecond, 0.0, 1000.0)) { if (!inRangeExclMax(millisecond, 0.0, 1000.0)) {
throw ConversionException("millisecond is out of range"); throw ConversionException("millisecond is out of range");
} }
return (hour * TimeSpan::m_ticksPerHour) + (minute * TimeSpan::m_ticksPerMinute) + (second * TimeSpan::m_ticksPerSecond) return static_cast<uint64>(hour * TimeSpan::m_ticksPerHour) + static_cast<uint64>(minute * TimeSpan::m_ticksPerMinute)
+ (uint64)(millisecond * (double)TimeSpan::m_ticksPerMillisecond); + static_cast<uint64>(second * TimeSpan::m_ticksPerSecond) + static_cast<uint64>(millisecond * TimeSpan::m_ticksPerMillisecond);
} }
/*! /*!

View File

@ -73,6 +73,8 @@ public:
constexpr int minute() const; constexpr int minute() const;
constexpr int second() const; constexpr int second() const;
constexpr int millisecond() const; constexpr int millisecond() const;
constexpr int microsecond() const;
constexpr int nanosecond() const;
constexpr bool isNull() const; constexpr bool isNull() const;
constexpr TimeSpan timeOfDay() const; constexpr TimeSpan timeOfDay() const;
bool isLeapYear() const; bool isLeapYear() const;
@ -278,6 +280,24 @@ constexpr inline int DateTime::millisecond() const
return m_ticks / TimeSpan::m_ticksPerMillisecond % 1000ul; return m_ticks / TimeSpan::m_ticksPerMillisecond % 1000ul;
} }
/*!
* \brief Gets the microsecond component of the date represented by this instance.
*/
constexpr int DateTime::microsecond() const
{
return m_ticks / TimeSpan::ticksPerMicrosecond % 1000ul;
}
/*!
* \brief Gets the nanosecond component of the date represented by this instance.
* \remarks The accuracy of the DateTime class is 100-nanoseconds. Hence the returned value
* will always have two zeros at the end (in decimal representation).
*/
constexpr int DateTime::nanosecond() const
{
return m_ticks % 10ul * TimeSpan::nanosecondsPerTick;
}
/*! /*!
* \brief Returns ture if the date represented by the current DateTime class is null. * \brief Returns ture if the date represented by the current DateTime class is null.
* \sa DateTime * \sa DateTime

View File

@ -61,12 +61,12 @@ TimeSpan TimeSpan::fromString(const char *str, char separator)
* \brief Converts the value of the current TimeSpan object to its equivalent std::string representation * \brief Converts the value of the current TimeSpan object to its equivalent std::string representation
* according the given \a format. * according the given \a format.
* *
* If \a noMilliseconds is true the time interval will be rounded to full seconds. * If \a fullSeconds is true the time interval will be rounded to full seconds.
*/ */
string TimeSpan::toString(TimeSpanOutputFormat format, bool noMilliseconds) const string TimeSpan::toString(TimeSpanOutputFormat format, bool fullSeconds) const
{ {
string result; string result;
toString(result, format, noMilliseconds); toString(result, format, fullSeconds);
return result; return result;
} }
@ -74,11 +74,11 @@ string TimeSpan::toString(TimeSpanOutputFormat format, bool noMilliseconds) cons
* \brief Converts the value of the current TimeSpan object to its equivalent std::string representation * \brief Converts the value of the current TimeSpan object to its equivalent std::string representation
* according the given \a format. * according the given \a format.
* *
* If \a noMilliseconds is true the time interval will be rounded to full seconds. * If \a fullSeconds is true the time interval will be rounded to full seconds.
* *
* The string representation will be stored in \a result. * The string representation will be stored in \a result.
*/ */
void TimeSpan::toString(string &result, TimeSpanOutputFormat format, bool noMilliseconds) const void TimeSpan::toString(string &result, TimeSpanOutputFormat format, bool fullSeconds) const
{ {
stringstream s(stringstream::in | stringstream::out); stringstream s(stringstream::in | stringstream::out);
TimeSpan positive(m_ticks); TimeSpan positive(m_ticks);
@ -88,36 +88,75 @@ void TimeSpan::toString(string &result, TimeSpanOutputFormat format, bool noMill
} }
switch (format) { switch (format) {
case TimeSpanOutputFormat::Normal: case TimeSpanOutputFormat::Normal:
s << setfill('0') << setw(2) << floor(positive.totalHours()) << ":" << setw(2) << positive.minutes() << ":" << setw(2) << positive.seconds() s << setfill('0') << setw(2) << floor(positive.totalHours()) << ":" << setw(2) << positive.minutes() << ":" << setw(2) << positive.seconds();
<< " "; if (!fullSeconds) {
const int milli(positive.milliseconds());
const int micro(positive.microseconds());
const int nano(positive.nanoseconds());
if (milli || micro || nano) {
s << '.' << setw(3) << milli;
if (micro || nano) {
s << setw(3) << micro;
if (nano) {
s << nano / TimeSpan::nanosecondsPerTick;
}
}
}
}
break; break;
case TimeSpanOutputFormat::WithMeasures: case TimeSpanOutputFormat::WithMeasures:
if (isNull()) { if (isNull()) {
s << "0 s "; result = "0 s";
return;
} else { } else {
if (positive.totalMilliseconds() < 1.0) { if (!fullSeconds && positive.totalMilliseconds() < 1.0) {
s << setprecision(2) << (m_ticks / 10.0) << " µs "; s << setprecision(2) << positive.totalMicroseconds() << " µs";
} else { } else {
bool needWhitespace = false;
if (const int days = positive.days()) { if (const int days = positive.days()) {
s << days << " d "; needWhitespace = true;
s << days << " d";
} }
if (const int hours = positive.hours()) { if (const int hours = positive.hours()) {
s << hours << " h "; if (needWhitespace)
s << ' ';
needWhitespace = true;
s << hours << " h";
} }
if (const int minutes = positive.minutes()) { if (const int minutes = positive.minutes()) {
s << minutes << " min "; if (needWhitespace)
s << ' ';
needWhitespace = true;
s << minutes << " min";
} }
if (const int seconds = positive.seconds()) { if (const int seconds = positive.seconds()) {
s << seconds << " s "; if (needWhitespace)
s << ' ';
needWhitespace = true;
s << seconds << " s";
} }
if (!noMilliseconds) { if (!fullSeconds) {
if (const int milliseconds = positive.milliseconds()) { if (const int milliseconds = positive.milliseconds()) {
s << milliseconds << " ms "; if (needWhitespace)
s << ' ';
needWhitespace = true;
s << milliseconds << " ms";
}
if (const int microseconds = positive.microseconds()) {
if (needWhitespace)
s << ' ';
needWhitespace = true;
s << microseconds << " µs";
}
if (const int nanoseconds = positive.nanoseconds()) {
if (needWhitespace)
s << ' ';
s << nanoseconds << " ns";
} }
} }
} }
} }
break; break;
} }
result = s.str().substr(0, static_cast<string::size_type>(s.tellp()) - 1); result = s.str();
} }

View File

@ -42,12 +42,15 @@ public:
static constexpr TimeSpan infinity(); static constexpr TimeSpan infinity();
constexpr int64 totalTicks() const; constexpr int64 totalTicks() const;
constexpr double totalMicroseconds() const;
constexpr double totalMilliseconds() const; constexpr double totalMilliseconds() const;
constexpr double totalSeconds() const; constexpr double totalSeconds() const;
constexpr double totalMinutes() const; constexpr double totalMinutes() const;
constexpr double totalHours() const; constexpr double totalHours() const;
constexpr double totalDays() const; constexpr double totalDays() const;
constexpr int nanoseconds() const;
constexpr int microseconds() const;
constexpr int milliseconds() const; constexpr int milliseconds() const;
constexpr int seconds() const; constexpr int seconds() const;
constexpr int minutes() const; constexpr int minutes() const;
@ -65,14 +68,16 @@ public:
TimeSpan &operator+=(const TimeSpan &other); TimeSpan &operator+=(const TimeSpan &other);
TimeSpan &operator-=(const TimeSpan &other); TimeSpan &operator-=(const TimeSpan &other);
std::string toString(TimeSpanOutputFormat format = TimeSpanOutputFormat::Normal, bool noMilliseconds = false) const; std::string toString(TimeSpanOutputFormat format = TimeSpanOutputFormat::Normal, bool fullSeconds = false) const;
void toString(std::string &result, TimeSpanOutputFormat format = TimeSpanOutputFormat::Normal, bool noMilliseconds = false) const; void toString(std::string &result, TimeSpanOutputFormat format = TimeSpanOutputFormat::Normal, bool fullSeconds = false) const;
constexpr bool isNull() const; constexpr bool isNull() const;
constexpr bool isNegative() const; constexpr bool isNegative() const;
constexpr bool isNegativeInfinity() const; constexpr bool isNegativeInfinity() const;
constexpr bool isInfinity() const; constexpr bool isInfinity() const;
// TODO: make those public constants signed in next major release and remove private ones then // TODO: make those public constants signed in next major release and remove private ones then
static constexpr int64 nanosecondsPerTick = 100uL;
static constexpr int64 ticksPerMicrosecond = 10uL;
static constexpr uint64 ticksPerMillisecond = 10000uL; static constexpr uint64 ticksPerMillisecond = 10000uL;
static constexpr uint64 ticksPerSecond = 10000000uL; static constexpr uint64 ticksPerSecond = 10000000uL;
static constexpr uint64 ticksPerMinute = 600000000uL; static constexpr uint64 ticksPerMinute = 600000000uL;
@ -178,6 +183,14 @@ constexpr inline int64 TimeSpan::totalTicks() const
return m_ticks; return m_ticks;
} }
/*!
* \brief Gets the value of the current TimeSpan class expressed in whole and fractional microseconds.
*/
constexpr double TimeSpan::totalMicroseconds() const
{
return static_cast<double>(m_ticks) / static_cast<double>(ticksPerMicrosecond);
}
/*! /*!
* \brief Gets the value of the current TimeSpan class expressed in whole and fractional milliseconds. * \brief Gets the value of the current TimeSpan class expressed in whole and fractional milliseconds.
*/ */
@ -218,6 +231,24 @@ constexpr inline double TimeSpan::totalDays() const
return static_cast<double>(m_ticks) / static_cast<double>(m_ticksPerDay); return static_cast<double>(m_ticks) / static_cast<double>(m_ticksPerDay);
} }
/*!
* \brief Gets the nanoseconds component of the time interval represented by the current TimeSpan class.
* \remarks The accuracy of the TimeSpan class is 100-nanoseconds. Hence the returned value
* will always have two zeros at the end (in decimal representation).
*/
constexpr int TimeSpan::nanoseconds() const
{
return m_ticks % 10l * TimeSpan::nanosecondsPerTick;
}
/*!
* \brief Gets the microseconds component of the time interval represented by the current TimeSpan class.
*/
constexpr int TimeSpan::microseconds() const
{
return (m_ticks / ticksPerMicrosecond) % 1000l;
}
/*! /*!
* \brief Gets the miliseconds component of the time interval represented by the current TimeSpan class. * \brief Gets the miliseconds component of the time interval represented by the current TimeSpan class.
*/ */

View File

@ -90,6 +90,13 @@ void ChronoTests::testDateTime()
const auto test3 = DateTime::fromIsoString("2016-08-29T21:32:31.125+02:00"); const auto test3 = DateTime::fromIsoString("2016-08-29T21:32:31.125+02:00");
CPPUNIT_ASSERT_EQUAL("2016-08-29T21:32:31.125+02:00"s, test3.first.toIsoString(test3.second)); CPPUNIT_ASSERT_EQUAL("2016-08-29T21:32:31.125+02:00"s, test3.first.toIsoString(test3.second));
CPPUNIT_ASSERT_THROW(DateTime::fromString("#"), ConversionException); CPPUNIT_ASSERT_THROW(DateTime::fromString("#"), ConversionException);
// test accuracy (of 100 nanoseconds)
const auto test4 = DateTime::fromIsoString("2017-08-23T19:40:15.985077682+02:00");
CPPUNIT_ASSERT_EQUAL(15, test4.first.second());
CPPUNIT_ASSERT_EQUAL(985, test4.first.millisecond());
CPPUNIT_ASSERT_EQUAL(77, test4.first.microsecond());
CPPUNIT_ASSERT_EQUAL(600, test4.first.nanosecond());
CPPUNIT_ASSERT_EQUAL("2017-08-23T19:40:15.9850776+02:00"s, test4.first.toIsoString(test4.second));
// test now() and exactNow() (or at least whether both behave the same) // test now() and exactNow() (or at least whether both behave the same)
#if defined(PLATFORM_UNIX) #if defined(PLATFORM_UNIX)
@ -124,6 +131,15 @@ void ChronoTests::testTimeSpan()
CPPUNIT_ASSERT_EQUAL("-5 s"s, TimeSpan::fromSeconds(-5.0).toString(TimeSpanOutputFormat::WithMeasures, false)); CPPUNIT_ASSERT_EQUAL("-5 s"s, TimeSpan::fromSeconds(-5.0).toString(TimeSpanOutputFormat::WithMeasures, false));
CPPUNIT_ASSERT_EQUAL("0 s"s, TimeSpan().toString(TimeSpanOutputFormat::WithMeasures, false)); CPPUNIT_ASSERT_EQUAL("0 s"s, TimeSpan().toString(TimeSpanOutputFormat::WithMeasures, false));
CPPUNIT_ASSERT_EQUAL("5e+02 µs"s, TimeSpan::fromMilliseconds(0.5).toString(TimeSpanOutputFormat::WithMeasures, false)); CPPUNIT_ASSERT_EQUAL("5e+02 µs"s, TimeSpan::fromMilliseconds(0.5).toString(TimeSpanOutputFormat::WithMeasures, false));
// test accuracy (of 100 nanoseconds)
const auto test2 = TimeSpan::fromString("15.985077682");
CPPUNIT_ASSERT_EQUAL(15.9850776, test2.totalSeconds());
CPPUNIT_ASSERT_EQUAL(15, test2.seconds());
CPPUNIT_ASSERT_EQUAL(985, test2.milliseconds());
CPPUNIT_ASSERT_EQUAL(77, test2.microseconds());
CPPUNIT_ASSERT_EQUAL(600, test2.nanoseconds());
CPPUNIT_ASSERT_EQUAL("00:00:15.9850776"s, test2.toString());
CPPUNIT_ASSERT_EQUAL("15 s 985 ms 77 µs 600 ns"s, test2.toString(TimeSpanOutputFormat::WithMeasures));
// test whether ConversionException() is thrown when invalid values are specified // test whether ConversionException() is thrown when invalid values are specified
CPPUNIT_ASSERT_THROW(TimeSpan::fromString("2:34a:53:32.5"), ConversionException); CPPUNIT_ASSERT_THROW(TimeSpan::fromString("2:34a:53:32.5"), ConversionException);