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_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_MINOR 9)
set(META_VERSION_PATCH 2)
set(META_VERSION_MINOR 10)
set(META_VERSION_PATCH 0)
# find required 3rd party libraries
include(3rdParty)

View File

@ -222,7 +222,19 @@ 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();
<< 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()) {
s << (timeZoneDelta.isNegative() ? '-' : '+');
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)) {
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;
if (!inRangeInclMax(day, 1, daysToMonth[month] - daysToMonth[passedMonth])) {
throw ConversionException("day is out of range");
}
int passedYears = year - 1;
int passedDays = day - 1;
const auto passedYears = static_cast<unsigned int>(year - 1);
const auto passedDays = static_cast<unsigned int>(day - 1);
return (passedYears * m_daysPerYear + passedYears / 4 - passedYears / 100 + passedYears / 400 + daysToMonth[passedMonth] + passedDays)
* 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)) {
throw ConversionException("millisecond is out of range");
}
return (hour * TimeSpan::m_ticksPerHour) + (minute * TimeSpan::m_ticksPerMinute) + (second * TimeSpan::m_ticksPerSecond)
+ (uint64)(millisecond * (double)TimeSpan::m_ticksPerMillisecond);
return static_cast<uint64>(hour * TimeSpan::m_ticksPerHour) + static_cast<uint64>(minute * TimeSpan::m_ticksPerMinute)
+ 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 second() const;
constexpr int millisecond() const;
constexpr int microsecond() const;
constexpr int nanosecond() const;
constexpr bool isNull() const;
constexpr TimeSpan timeOfDay() const;
bool isLeapYear() const;
@ -278,6 +280,24 @@ constexpr inline int DateTime::millisecond() const
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.
* \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
* 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;
toString(result, format, noMilliseconds);
toString(result, format, fullSeconds);
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
* 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.
*/
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);
TimeSpan positive(m_ticks);
@ -88,36 +88,75 @@ void TimeSpan::toString(string &result, TimeSpanOutputFormat format, bool noMill
}
switch (format) {
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;
case TimeSpanOutputFormat::WithMeasures:
if (isNull()) {
s << "0 s ";
result = "0 s";
return;
} else {
if (positive.totalMilliseconds() < 1.0) {
s << setprecision(2) << (m_ticks / 10.0) << " µs ";
if (!fullSeconds && positive.totalMilliseconds() < 1.0) {
s << setprecision(2) << positive.totalMicroseconds() << " µs";
} else {
bool needWhitespace = false;
if (const int days = positive.days()) {
s << days << " d ";
needWhitespace = true;
s << days << " d";
}
if (const int hours = positive.hours()) {
s << hours << " h ";
if (needWhitespace)
s << ' ';
needWhitespace = true;
s << hours << " h";
}
if (const int minutes = positive.minutes()) {
s << minutes << " min ";
if (needWhitespace)
s << ' ';
needWhitespace = true;
s << minutes << " min";
}
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()) {
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;
}
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();
constexpr int64 totalTicks() const;
constexpr double totalMicroseconds() const;
constexpr double totalMilliseconds() const;
constexpr double totalSeconds() const;
constexpr double totalMinutes() const;
constexpr double totalHours() const;
constexpr double totalDays() const;
constexpr int nanoseconds() const;
constexpr int microseconds() const;
constexpr int milliseconds() const;
constexpr int seconds() const;
constexpr int minutes() const;
@ -65,14 +68,16 @@ public:
TimeSpan &operator+=(const TimeSpan &other);
TimeSpan &operator-=(const TimeSpan &other);
std::string toString(TimeSpanOutputFormat format = TimeSpanOutputFormat::Normal, bool noMilliseconds = false) const;
void toString(std::string &result, 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 fullSeconds = false) const;
constexpr bool isNull() const;
constexpr bool isNegative() const;
constexpr bool isNegativeInfinity() const;
constexpr bool isInfinity() const;
// 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 ticksPerSecond = 10000000uL;
static constexpr uint64 ticksPerMinute = 600000000uL;
@ -178,6 +183,14 @@ constexpr inline int64 TimeSpan::totalTicks() const
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.
*/
@ -218,6 +231,24 @@ constexpr inline double TimeSpan::totalDays() const
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.
*/

View File

@ -90,6 +90,13 @@ void ChronoTests::testDateTime()
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_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)
#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("0 s"s, TimeSpan().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
CPPUNIT_ASSERT_THROW(TimeSpan::fromString("2:34a:53:32.5"), ConversionException);