cpp-utilities/chrono/datetime.cpp

409 lines
16 KiB
C++
Raw Normal View History

2015-09-06 20:19:09 +02:00
#include "./datetime.h"
2015-04-22 18:36:40 +02:00
#include "../conversion/stringbuilder.h"
2017-05-01 03:13:11 +02:00
#include "../conversion/stringconversion.h"
2015-04-22 18:36:40 +02:00
#include <iomanip>
2017-05-01 03:13:11 +02:00
#include <sstream>
2015-04-22 18:36:40 +02:00
#include <stdexcept>
using namespace std;
using namespace ChronoUtilities;
using namespace ConversionUtilities;
const int DateTime::m_daysPerYear = 365;
const int DateTime::m_daysPer4Years = 1461;
const int DateTime::m_daysPer100Years = 36524;
const int DateTime::m_daysPer400Years = 146097;
const int DateTime::m_daysTo1601 = 584388;
const int DateTime::m_daysTo1899 = 693593;
const int DateTime::m_daysTo10000 = 3652059;
2017-05-01 03:13:11 +02:00
const int DateTime::m_daysToMonth365[13] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
const int DateTime::m_daysToMonth366[13] = { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 };
const int DateTime::m_daysInMonth365[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
const int DateTime::m_daysInMonth366[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
2015-04-22 18:36:40 +02:00
2017-11-29 19:08:22 +01:00
template <typename num1, typename num2, typename num3> constexpr bool inRangeInclMax(num1 val, num2 min, num3 max)
2015-04-22 18:36:40 +02:00
{
return (val) >= (min) && (val) <= (max);
}
2017-11-29 19:08:22 +01:00
template <typename num1, typename num2, typename num3> constexpr bool inRangeExclMax(num1 val, num2 min, num3 max)
2015-04-22 18:36:40 +02:00
{
2017-05-01 03:13:11 +02:00
return (val) >= (min) && (val) < (max);
2015-04-22 18:36:40 +02:00
}
/*!
* \class ChronoUtilities::DateTime
* \brief Represents an instant in time, typically expressed as a date and time of day.
2016-02-15 20:29:52 +01:00
* \remarks
* - Time values are measured in 100-nanosecond units called ticks,
* and a particular date is the number of ticks since 12:00 midnight, January 1,
* 0001 A.D. (C.E.) in the GregorianCalendar calendar (excluding ticks that would
* be added by leap seconds).
* - There is no time zone information associated. Hence different time zones are
* not taken into account when comparing two instances. For instance, the
* expression (DateTime::now() - DateTime::gmtNow()) returns one hour in Germany during winter
* time (instead of zero).
* \todo
* - Add method for parsing custom string formats.
* - Add method for printing to custom string formats.
* - Allow to determine the date part for each compontent at once to prevent multiple
* invocations of getDatePart().
* - Make more methods constexpr.
2015-04-22 18:36:40 +02:00
*/
/*!
* \brief Constructs a new DateTime object with the local time from the specified UNIX \a timeStamp.
*/
DateTime DateTime::fromTimeStamp(time_t timeStamp)
{
2017-05-01 03:13:11 +02:00
if (timeStamp) {
2015-09-06 15:30:16 +02:00
struct tm *timeinfo = localtime(&timeStamp);
2017-05-01 03:13:11 +02:00
return DateTime::fromDateAndTime(timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday, timeinfo->tm_hour, timeinfo->tm_min,
timeinfo->tm_sec < 60 ? timeinfo->tm_sec : 59, 0);
2015-09-06 15:30:16 +02:00
} else {
return DateTime();
}
}
2016-01-27 00:14:20 +01:00
/*!
* \brief Constructs a new DateTime object with the GMT time from the specified UNIX \a timeStamp.
2016-01-27 00:14:20 +01:00
*/
DateTime DateTime::fromTimeStampGmt(time_t timeStamp)
{
2017-06-25 15:09:16 +02:00
return DateTime(DateTime::unixEpochStart().totalTicks() + static_cast<uint64>(timeStamp) * TimeSpan::m_ticksPerSecond);
2016-01-27 00:14:20 +01:00
}
2015-04-22 18:36:40 +02:00
/*!
* \brief Parses the given C-style string as DateTime.
* \throws Throws a ConversionException if the specified \a str does not match the expected time format.
*
* The expected format is something like "2012-02-29 15:34:20.033" or "2012/02/29 15:34:20.033". The
* delimiters '-', ':' and '/' are exchangeable.
*
* \sa DateTime::fromIsoString()
2015-04-22 18:36:40 +02:00
*/
DateTime DateTime::fromString(const char *str)
2015-04-22 18:36:40 +02:00
{
2017-05-01 03:13:11 +02:00
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;
2017-05-01 03:13:11 +02:00
for (const char *strIndex = str;; ++strIndex) {
const char c = *strIndex;
2017-05-01 03:13:11 +02:00
if (c <= '9' && c >= '0') {
if (valueIndex > secondsIndex) {
miliSeconds += (c - '0') * miliSecondsFact;
miliSecondsFact /= 10;
} else {
*valueIndex *= 10;
*valueIndex += c - '0';
}
2017-05-01 03:13:11 +02:00
} else if ((c == '-' || c == ':' || c == '/') || (c == '.' && (valueIndex == secondsIndex)) || (c == ' ' && (valueIndex == dayIndex))) {
if (++valueIndex == valuesEnd) {
break; // just ignore further values for now
}
2017-05-01 03:13:11 +02:00
} else if (c == '\0') {
break;
2015-04-22 18:36:40 +02:00
} else {
2017-05-01 03:13:11 +02:00
throw ConversionException(argsToString("unexpected ", c));
2015-04-22 18:36:40 +02:00
}
}
return DateTime::fromDateAndTime(values[0], values[1], *dayIndex, values[3], values[4], *secondsIndex, miliSeconds);
2015-04-22 18:36:40 +02:00
}
/*!
2016-09-01 15:42:25 +02:00
* \brief Parses the specified ISO date time denotation provided as C-style string.
* \returns Returns a pair where the first value is the parsed date time and the second value
* a time span which can be subtracted from the first value to get the UTC time.
2018-01-29 16:24:39 +01:00
* \remarks Not all variants allowed by ISO 8601 are supported right now, eg. delimiters can not
* be omitted.
* The common form (something like "2016-08-29T21:32:31.588539814+02:00") is supported of course.
* \sa https://en.wikipedia.org/wiki/ISO_8601
*/
std::pair<DateTime, TimeSpan> DateTime::fromIsoString(const char *str)
{
2017-05-01 03:13:11 +02:00
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;
2017-05-01 03:13:11 +02:00
for (const char *strIndex = str;; ++strIndex) {
const char c = *strIndex;
2017-05-01 03:13:11 +02:00
if (c <= '9' && c >= '0') {
if (valueIndex == miliSecondsIndex) {
miliSeconds += (c - '0') * miliSecondsFact;
miliSecondsFact /= 10;
} else {
*valueIndex *= 10;
*valueIndex += c - '0';
}
2017-05-01 03:13:11 +02:00
} else if (c == 'T') {
if (++valueIndex != hourIndex) {
throw ConversionException("\"T\" expected before hour");
}
2017-05-01 03:13:11 +02:00
} else if (c == '-') {
if (valueIndex < dayIndex) {
++valueIndex;
2018-01-29 16:24:39 +01:00
} else if (++valueIndex == deltaHourIndex) {
deltaNegative = true;
} else {
throw ConversionException("unexpected \"-\" after day");
}
2017-05-01 03:13:11 +02:00
} else if (c == '.') {
if (valueIndex != secondsIndex) {
throw ConversionException("unexpected \".\"");
} else {
++valueIndex;
}
2017-05-01 03:13:11 +02:00
} else if (c == ':') {
if (valueIndex < hourIndex) {
throw ConversionException("unexpected \":\" before hour");
2017-05-01 03:13:11 +02:00
} else if (valueIndex == secondsIndex) {
throw ConversionException("unexpected \":\" after second");
} else {
++valueIndex;
}
2017-05-01 03:13:11 +02:00
} else if ((c == '+') && (++valueIndex == deltaHourIndex)) {
deltaNegative = false;
2018-01-29 16:24:39 +01:00
} else if ((c == 'Z') && (++valueIndex == deltaHourIndex)) {
valueIndex += 2;
2017-05-01 03:13:11 +02:00
} else if (c == '\0') {
break;
} else {
2017-05-01 03:13:11 +02:00
throw ConversionException(argsToString("unexpected \"", c, '\"'));
}
}
2018-01-29 16:24:39 +01:00
auto delta(TimeSpan::fromMinutes(*deltaHourIndex * 60 + values[8]));
if (deltaNegative) {
delta = TimeSpan(-delta.totalTicks());
}
return make_pair(DateTime::fromDateAndTime(values[0], values[1], *dayIndex, *hourIndex, values[4], *secondsIndex, miliSeconds), delta);
}
/*!
* \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
2015-04-22 18:36:40 +02:00
*/
string DateTime::toString(DateTimeOutputFormat format, bool noMilliseconds) const
{
string result;
toString(result, format, noMilliseconds);
return result;
}
/*!
* \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
2015-04-22 18:36:40 +02:00
*/
void DateTime::toString(string &result, DateTimeOutputFormat format, bool noMilliseconds) const
{
stringstream s(stringstream::in | stringstream::out);
s << setfill('0');
2017-05-01 03:13:11 +02:00
if (format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndShortWeekday)
s << printDayOfWeek(dayOfWeek(), format == DateTimeOutputFormat::DateTimeAndShortWeekday) << ' ';
2017-05-01 03:13:11 +02:00
if (format == DateTimeOutputFormat::DateOnly || format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday
|| format == DateTimeOutputFormat::DateTimeAndShortWeekday)
s << setw(4) << year() << '-' << setw(2) << month() << '-' << setw(2) << day();
2017-05-01 03:13:11 +02:00
if (format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday
|| format == DateTimeOutputFormat::DateTimeAndShortWeekday)
2015-04-22 18:36:40 +02:00
s << " ";
2017-05-01 03:13:11 +02:00
if (format == DateTimeOutputFormat::TimeOnly || format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday
|| format == DateTimeOutputFormat::DateTimeAndShortWeekday) {
s << setw(2) << hour() << ':' << setw(2) << minute() << ':' << setw(2) << second();
2015-04-22 18:36:40 +02:00
int ms = millisecond();
2017-05-01 03:13:11 +02:00
if (!noMilliseconds && ms > 0) {
s << '.' << setw(3) << ms;
2015-04-22 18:36:40 +02:00
}
}
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');
2017-05-01 03:13:11 +02:00
s << setw(4) << year() << '-' << setw(2) << month() << '-' << setw(2) << day() << 'T' << setw(2) << hour() << ':' << setw(2) << minute() << ':'
<< 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;
}
}
}
2017-05-01 03:13:11 +02:00
if (!timeZoneDelta.isNull()) {
2018-01-29 16:24:39 +01:00
if (timeZoneDelta.isNegative()) {
s << '-';
timeZoneDelta = TimeSpan(-timeZoneDelta.totalTicks());
} else {
s << '+';
}
s << setw(2) << timeZoneDelta.hours() << ':' << setw(2) << timeZoneDelta.minutes();
}
return s.str();
}
2015-04-22 18:36:40 +02:00
/*!
2016-01-18 23:41:30 +01:00
* \brief Returns the string representation as C-style string for the given day of week.
2015-04-22 18:36:40 +02:00
*
* If \a abbreviation is true, only the first three letters of the string will
* be returned.
* \sa DayOfWeek
*/
const char *DateTime::printDayOfWeek(DayOfWeek dayOfWeek, bool abbreviation)
{
2017-05-01 03:13:11 +02:00
if (abbreviation) {
switch (dayOfWeek) {
2015-04-22 18:36:40 +02:00
case DayOfWeek::Monday:
return "Mon";
case DayOfWeek::Tuesday:
return "Tue";
case DayOfWeek::Wednesday:
return "Wed";
case DayOfWeek::Thursday:
return "Thu";
case DayOfWeek::Friday:
return "Fri";
case DayOfWeek::Saturday:
return "Sat";
case DayOfWeek::Sunday:
return "Sun";
}
} else {
2017-05-01 03:13:11 +02:00
switch (dayOfWeek) {
2015-04-22 18:36:40 +02:00
case DayOfWeek::Monday:
return "Monday";
case DayOfWeek::Tuesday:
return "Tuesday";
case DayOfWeek::Wednesday:
return "Wednesday";
case DayOfWeek::Thursday:
return "Thursday";
case DayOfWeek::Friday:
return "Friday";
case DayOfWeek::Saturday:
return "Saturday";
case DayOfWeek::Sunday:
return "Sunday";
}
}
return "";
}
#if defined(PLATFORM_UNIX) && !defined(PLATFORM_MAC)
/*!
* \brief Returns a DateTime object that is set to the current date and time on this computer, expressed as the GMT time.
2017-11-29 19:11:01 +01:00
* \remarks Only available under UNIX-like systems supporting clock_gettime().
*/
DateTime DateTime::exactGmtNow()
{
struct timespec t;
clock_gettime(CLOCK_REALTIME, &t);
2017-05-01 03:13:11 +02:00
return DateTime(
2017-06-25 15:09:16 +02:00
DateTime::unixEpochStart().totalTicks() + static_cast<uint64>(t.tv_sec) * TimeSpan::m_ticksPerSecond + static_cast<uint64>(t.tv_nsec) / 100);
}
#endif
2015-04-22 18:36:40 +02:00
/*!
2016-01-18 23:41:30 +01:00
* \brief Converts the given date expressed in \a year, \a month and \a day to ticks.
2015-04-22 18:36:40 +02:00
*/
uint64 DateTime::dateToTicks(int year, int month, int day)
{
2017-06-25 15:09:16 +02:00
if (!inRangeInclMax(year, 1, 9999)) {
2015-04-22 18:36:40 +02:00
throw ConversionException("year is out of range");
}
2017-06-25 15:09:16 +02:00
if (!inRangeInclMax(month, 1, 12)) {
throw ConversionException("month is out of range");
}
2017-11-29 19:08:22 +01:00
const auto *const daysToMonth = reinterpret_cast<const int *>(isLeapYear(year) ? m_daysToMonth366 : m_daysToMonth365);
const int passedMonth = month - 1;
2017-06-25 15:09:16 +02:00
if (!inRangeInclMax(day, 1, daysToMonth[month] - daysToMonth[passedMonth])) {
throw ConversionException("day is out of range");
}
const auto passedYears = static_cast<unsigned int>(year - 1);
const auto passedDays = static_cast<unsigned int>(day - 1);
2017-09-22 00:23:02 +02:00
return (passedYears * m_daysPerYear + passedYears / 4 - passedYears / 100 + passedYears / 400
+ static_cast<unsigned int>(daysToMonth[passedMonth]) + passedDays)
2017-06-25 15:09:16 +02:00
* TimeSpan::m_ticksPerDay;
2015-04-22 18:36:40 +02:00
}
/*!
2016-01-18 23:41:30 +01:00
* \brief Converts the given time expressed in \a hour, \a minute, \a second and \a millisecond to ticks.
2015-04-22 18:36:40 +02:00
*/
uint64 DateTime::timeToTicks(int hour, int minute, int second, double millisecond)
{
2017-05-01 03:13:11 +02:00
if (!inRangeExclMax(hour, 0, 24)) {
2015-04-22 18:36:40 +02:00
throw ConversionException("hour is out of range");
}
2017-05-01 03:13:11 +02:00
if (!inRangeExclMax(minute, 0, 60)) {
2015-04-22 18:36:40 +02:00
throw ConversionException("minute is out of range");
}
2017-05-01 03:13:11 +02:00
if (!inRangeExclMax(second, 0, 60)) {
2015-04-22 18:36:40 +02:00
throw ConversionException("second is out of range");
}
2017-05-01 03:13:11 +02:00
if (!inRangeExclMax(millisecond, 0.0, 1000.0)) {
2015-04-22 18:36:40 +02:00
throw ConversionException("millisecond is out of range");
}
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);
2015-04-22 18:36:40 +02:00
}
/*!
2016-01-18 23:41:30 +01:00
* \brief Returns the specified date part.
2015-04-22 18:36:40 +02:00
* \sa DatePart
*/
int DateTime::getDatePart(DatePart part) const
{
2017-11-29 19:08:22 +01:00
const int fullDays = m_ticks / TimeSpan::m_ticksPerDay;
const int full400YearBlocks = fullDays / m_daysPer400Years;
const int daysMinusFull400YearBlocks = fullDays - full400YearBlocks * m_daysPer400Years;
2015-04-22 18:36:40 +02:00
int full100YearBlocks = daysMinusFull400YearBlocks / m_daysPer100Years;
2017-05-01 03:13:11 +02:00
if (full100YearBlocks == 4) {
2015-04-22 18:36:40 +02:00
full100YearBlocks = 3;
}
2017-11-29 19:08:22 +01:00
const int daysMinusFull100YearBlocks = daysMinusFull400YearBlocks - full100YearBlocks * m_daysPer100Years;
const int full4YearBlocks = daysMinusFull100YearBlocks / m_daysPer4Years;
const int daysMinusFull4YearBlocks = daysMinusFull100YearBlocks - full4YearBlocks * m_daysPer4Years;
2015-04-22 18:36:40 +02:00
int full1YearBlocks = daysMinusFull4YearBlocks / m_daysPerYear;
2017-05-01 03:13:11 +02:00
if (full1YearBlocks == 4) {
2015-04-22 18:36:40 +02:00
full1YearBlocks = 3;
}
2017-05-01 03:13:11 +02:00
if (part == DatePart::Year) {
2015-04-22 18:36:40 +02:00
return full400YearBlocks * 400 + full100YearBlocks * 100 + full4YearBlocks * 4 + full1YearBlocks + 1;
}
2017-11-29 19:08:22 +01:00
const int restDays = daysMinusFull4YearBlocks - full1YearBlocks * m_daysPerYear;
2017-05-01 03:13:11 +02:00
if (part == DatePart::DayOfYear) { // day
2015-04-22 18:36:40 +02:00
return restDays + 1;
}
2017-11-29 19:08:22 +01:00
const int *const daysToMonth = (full1YearBlocks == 3 && (full4YearBlocks != 24 || full100YearBlocks == 3)) ? m_daysToMonth366 : m_daysToMonth365;
2015-04-22 18:36:40 +02:00
int month = 1;
2017-05-01 03:13:11 +02:00
while (restDays >= daysToMonth[month]) {
2015-04-22 18:36:40 +02:00
++month;
}
2017-05-01 03:13:11 +02:00
if (part == DatePart::Month) {
2015-04-22 18:36:40 +02:00
return month;
2017-05-01 03:13:11 +02:00
} else if (part == DatePart::Day) {
2015-04-22 18:36:40 +02:00
return restDays - daysToMonth[month - 1] + 1;
}
return 0;
}