1 #include "./datetime.h"
2 
3 #include "../conversion/stringbuilder.h"
4 #include "../conversion/stringconversion.h"
5 
6 #include <iomanip>
7 #include <sstream>
8 #include <stdexcept>
9 
10 using namespace std;
11 
12 namespace CppUtilities {
13 
14 const int DateTime::m_daysPerYear = 365;
15 const int DateTime::m_daysPer4Years = 1461;
16 const int DateTime::m_daysPer100Years = 36524;
17 const int DateTime::m_daysPer400Years = 146097;
18 const int DateTime::m_daysTo1601 = 584388;
19 const int DateTime::m_daysTo1899 = 693593;
20 const int DateTime::m_daysTo10000 = 3652059;
21 const int DateTime::m_daysToMonth365[13] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };
22 const int DateTime::m_daysToMonth366[13] = { 0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366 };
23 const int DateTime::m_daysInMonth365[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
24 const int DateTime::m_daysInMonth366[12] = { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
25 
inRangeInclMax(num1 val,num2 min,num3 max)26 template <typename num1, typename num2, typename num3> constexpr bool inRangeInclMax(num1 val, num2 min, num3 max)
27 {
28     return (val) >= (min) && (val) <= (max);
29 }
30 
inRangeExclMax(num1 val,num2 min,num3 max)31 template <typename num1, typename num2, typename num3> constexpr bool inRangeExclMax(num1 val, num2 min, num3 max)
32 {
33     return (val) >= (min) && (val) < (max);
34 }
35 
36 /*!
37  * \class DateTime
38  * \brief Represents an instant in time, typically expressed as a date and time of day.
39  * \remarks
40  *  - Time values are measured in 100-nanosecond units called ticks,
41  *    and a particular date is the number of ticks since 12:00 midnight, January 1,
42  *    0001 A.D. (C.E.) in the Gregorian Calendar (excluding ticks that would be added by leap seconds).
43  *  - There is no time zone information associated. You need to keep track of the used time zone separately. That can
44  *    be done by keeping an additional TimeSpan around which represents the delta to GMT or by simply using GMT everywhere
45  *    in the program.
46  *  - When constructing an instance via DateTime::fromTimeStamp(), DateTime::fromChronoTimePoint() or DateTime::fromIsoStringLocal()
47  *    the time zone deltas are "baked into" the DateTime instance. For instance, the expression (DateTime::now() - DateTime::gmtNow())
48  *    returns one hour in Germany during winter time (and *not* zero although both instances represent the current time).
49  * \todo
50  * - Add method for parsing custom string formats.
51  * - Add method for printing to custom string formats.
52  * - Allow to determine the date part for each component at once to prevent multiple
53  *   invocations of getDatePart().
54  */
55 
56 /*!
57  * \brief Constructs a new DateTime object with the local time from the specified UNIX \a timeStamp.
58  */
fromTimeStamp(time_t timeStamp)59 DateTime DateTime::fromTimeStamp(time_t timeStamp)
60 {
61     if (timeStamp) {
62         struct tm *const timeinfo = localtime(&timeStamp);
63         return DateTime::fromDateAndTime(timeinfo->tm_year + 1900, timeinfo->tm_mon + 1, timeinfo->tm_mday, timeinfo->tm_hour, timeinfo->tm_min,
64             timeinfo->tm_sec < 60 ? timeinfo->tm_sec : 59, 0);
65     } else {
66         return DateTime();
67     }
68 }
69 
70 /*!
71  * \brief Parses the given C-style string as DateTime.
72  * \throws Throws a ConversionException if the specified \a str does not match the expected time format.
73  *
74  * The expected format is something like "2012-02-29 15:34:20.033" or "2012/02/29 15:34:20.033". The
75  * delimiters '-', ':' and '/' are exchangeable.
76  *
77  * \sa DateTime::fromIsoString()
78  */
fromString(const char * str)79 DateTime DateTime::fromString(const char *str)
80 {
81     int values[6] = { 0 };
82     int *const dayIndex = values + 2;
83     int *const secondsIndex = values + 5;
84     int *valueIndex = values;
85     int *const valuesEnd = values + 7;
86     double millisecondsFact = 100.0, milliseconds = 0.0;
87     for (const char *strIndex = str;; ++strIndex) {
88         const char c = *strIndex;
89         if (c <= '9' && c >= '0') {
90             if (valueIndex > secondsIndex) {
91                 milliseconds += (c - '0') * millisecondsFact;
92                 millisecondsFact /= 10;
93             } else {
94                 Detail::raiseAndAdd(*valueIndex, 10, c);
95             }
96         } else if ((c == '-' || c == ':' || c == '/') || (c == '.' && (valueIndex == secondsIndex))
97             || ((c == ' ' || c == 'T') && (valueIndex == dayIndex))) {
98             if (++valueIndex == valuesEnd) {
99                 break; // just ignore further values for now
100             }
101         } else if (c == '\0') {
102             break;
103         } else {
104             throw ConversionException(argsToString("Unexpected character \"", c, '\"'));
105         }
106     }
107     return DateTime::fromDateAndTime(values[0], values[1], *dayIndex, values[3], values[4], *secondsIndex, milliseconds);
108 }
109 
110 /*!
111  * \brief Parses the specified ISO date time denotation provided as C-style string.
112  * \returns Returns a pair where the first value is the parsed date time and the second value
113  *          the time zone designator (a time span which can be subtracted from the first value to get the UTC time).
114  * \remarks
115  * - Parsing durations and time intervals is *not* supported.
116  * - Truncated representations are *not* supported.
117  * - Standardised extensions (ISO 8601-2:2019) are *not* supported.
118  * \sa https://en.wikipedia.org/wiki/ISO_8601
119  */
fromIsoString(const char * str)120 std::pair<DateTime, TimeSpan> DateTime::fromIsoString(const char *str)
121 {
122     int values[9] = { 0 };
123     int *const yearIndex = values + 0;
124     int *const monthIndex = values + 1;
125     int *const dayIndex = values + 2;
126     int *const hourIndex = values + 3;
127     int *const secondsIndex = values + 5;
128     int *const miliSecondsIndex = values + 6;
129     int *const deltaHourIndex = values + 7;
130     int *const valuesEnd = values + 9;
131     int *valueIndex = values;
132     unsigned int remainingDigits = 4;
133     bool deltaNegative = false;
134     double millisecondsFact = 100.0, milliseconds = 0.0;
135     for (const char *strIndex = str;; ++strIndex) {
136         const char c = *strIndex;
137         if (c <= '9' && c >= '0') {
138             if (valueIndex == miliSecondsIndex) {
139                 milliseconds += (c - '0') * millisecondsFact;
140                 millisecondsFact /= 10;
141             } else {
142                 if (!remainingDigits) {
143                     if (++valueIndex == miliSecondsIndex || valueIndex >= valuesEnd) {
144                         throw ConversionException("Max. number of digits exceeded");
145                     }
146                     remainingDigits = 2;
147                 }
148                 *valueIndex *= 10;
149                 *valueIndex += c - '0';
150                 remainingDigits -= 1;
151             }
152         } else if (c == 'T') {
153             if (++valueIndex != hourIndex) {
154                 throw ConversionException("\"T\" expected before hour");
155             }
156             remainingDigits = 2;
157         } else if (c == '-') {
158             if (valueIndex < dayIndex) {
159                 ++valueIndex;
160             } else if (++valueIndex >= secondsIndex) {
161                 valueIndex = deltaHourIndex;
162                 deltaNegative = true;
163             } else {
164                 throw ConversionException("Unexpected \"-\" after day");
165             }
166             remainingDigits = 2;
167         } else if (c == '.') {
168             if (valueIndex != secondsIndex) {
169                 throw ConversionException("Unexpected \".\"");
170             } else {
171                 ++valueIndex;
172             }
173         } else if (c == ':') {
174             if (valueIndex < hourIndex) {
175                 throw ConversionException("Unexpected \":\" before hour");
176             } else if (valueIndex == secondsIndex) {
177                 throw ConversionException("Unexpected \":\" after second");
178             } else {
179                 ++valueIndex;
180             }
181             remainingDigits = 2;
182         } else if ((c == '+') && (++valueIndex >= secondsIndex)) {
183             valueIndex = deltaHourIndex;
184             deltaNegative = false;
185             remainingDigits = 2;
186         } else if ((c == 'Z') && (++valueIndex >= secondsIndex)) {
187             valueIndex = deltaHourIndex + 2;
188             remainingDigits = 2;
189         } else if (c == '\0') {
190             break;
191         } else {
192             throw ConversionException(argsToString("Unexpected \"", c, '\"'));
193         }
194     }
195     auto delta = TimeSpan::fromMinutes(*deltaHourIndex * 60 + values[8]);
196     if (deltaNegative) {
197         delta = TimeSpan(-delta.totalTicks());
198     }
199     if (valueIndex < monthIndex && !*monthIndex) {
200         *monthIndex = 1;
201     }
202     if (valueIndex < dayIndex && !*dayIndex) {
203         *dayIndex = 1;
204     }
205     return make_pair(DateTime::fromDateAndTime(*yearIndex, *monthIndex, *dayIndex, *hourIndex, values[4], *secondsIndex, milliseconds), delta);
206 }
207 
208 /*!
209  * \brief Returns the string representation of the current instance using the specified \a format.
210  * \remarks If \a noMilliseconds is true the date will be rounded to full seconds.
211  * \sa toIsoString() for ISO format
212  */
toString(string & result,DateTimeOutputFormat format,bool noMilliseconds) const213 void DateTime::toString(string &result, DateTimeOutputFormat format, bool noMilliseconds) const
214 {
215     if (format == DateTimeOutputFormat::Iso) {
216         result = toIsoString();
217         return;
218     }
219 
220     stringstream s(stringstream::in | stringstream::out);
221     s << setfill('0');
222 
223     if (format == DateTimeOutputFormat::IsoOmittingDefaultComponents) {
224         constexpr auto dateDelimiter = '-', timeDelimiter = ':';
225         const int components[] = { year(), month(), day(), hour(), minute(), second(), millisecond(), microsecond(), nanosecond() };
226         const int *const firstTimeComponent = components + 3;
227         const int *const firstFractionalComponent = components + 6;
228         const int *const lastComponent = components + 8;
229         const int *componentsEnd = noMilliseconds ? firstFractionalComponent : lastComponent + 1;
230         for (const int *i = componentsEnd - 1; i > components; --i) {
231             if (i >= firstTimeComponent && *i == 0) {
232                 componentsEnd = i;
233             } else if (i < firstTimeComponent && *i == 1) {
234                 componentsEnd = i;
235             }
236         }
237         for (const int *i = components; i != componentsEnd; ++i) {
238             if (i == firstTimeComponent) {
239                 s << 'T';
240             } else if (i == firstFractionalComponent) {
241                 s << '.';
242             }
243             if (i == components) {
244                 s << setw(4) << *i;
245             } else if (i < firstFractionalComponent) {
246                 if (i < firstTimeComponent) {
247                     s << dateDelimiter;
248                 } else if (i > firstTimeComponent) {
249                     s << timeDelimiter;
250                 }
251                 s << setw(2) << *i;
252             } else if (i < lastComponent) {
253                 s << setw(3) << *i;
254             } else {
255                 s << *i / TimeSpan::nanosecondsPerTick;
256             }
257         }
258         result = s.str();
259         return;
260     }
261 
262     if (format == DateTimeOutputFormat::DateTimeAndWeekday || format == DateTimeOutputFormat::DateTimeAndShortWeekday)
263         s << printDayOfWeek(dayOfWeek(), format == DateTimeOutputFormat::DateTimeAndShortWeekday) << ' ';
264     if (format == DateTimeOutputFormat::DateOnly || format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday
265         || format == DateTimeOutputFormat::DateTimeAndShortWeekday)
266         s << setw(4) << year() << '-' << setw(2) << month() << '-' << setw(2) << day();
267     if (format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday
268         || format == DateTimeOutputFormat::DateTimeAndShortWeekday)
269         s << " ";
270     if (format == DateTimeOutputFormat::TimeOnly || format == DateTimeOutputFormat::DateAndTime || format == DateTimeOutputFormat::DateTimeAndWeekday
271         || format == DateTimeOutputFormat::DateTimeAndShortWeekday) {
272         s << setw(2) << hour() << ':' << setw(2) << minute() << ':' << setw(2) << second();
273         int ms = millisecond();
274         if (!noMilliseconds && ms > 0) {
275             s << '.' << setw(3) << ms;
276         }
277     }
278     result = s.str();
279 }
280 
281 /*!
282  * \brief Returns the string representation of the current instance in the ISO format with custom delimiters,
283  *        eg. 2016/08/29T21-32-31.588539814+02:00 with '/' as \a dateDelimiter and '-' as \a timeDelimiter.
284  */
toIsoStringWithCustomDelimiters(TimeSpan timeZoneDelta,char dateDelimiter,char timeDelimiter,char timeZoneDelimiter) const285 string DateTime::toIsoStringWithCustomDelimiters(TimeSpan timeZoneDelta, char dateDelimiter, char timeDelimiter, char timeZoneDelimiter) const
286 {
287     stringstream s(stringstream::in | stringstream::out);
288     s << setfill('0');
289     s << setw(4) << year() << dateDelimiter << setw(2) << month() << dateDelimiter << setw(2) << day() << 'T' << setw(2) << hour() << timeDelimiter
290       << setw(2) << minute() << timeDelimiter << setw(2) << second();
291     const int milli(millisecond());
292     const int micro(microsecond());
293     const int nano(nanosecond());
294     if (milli || micro || nano) {
295         s << '.' << setw(3) << milli;
296         if (micro || nano) {
297             s << setw(3) << micro;
298             if (nano) {
299                 s << nano / TimeSpan::nanosecondsPerTick;
300             }
301         }
302     }
303     if (!timeZoneDelta.isNull()) {
304         if (timeZoneDelta.isNegative()) {
305             s << '-';
306             timeZoneDelta = TimeSpan(-timeZoneDelta.totalTicks());
307         } else {
308             s << '+';
309         }
310         s << setw(2) << timeZoneDelta.hours() << timeZoneDelimiter << setw(2) << timeZoneDelta.minutes();
311     }
312     return s.str();
313 }
314 
315 /*!
316  * \brief Returns the string representation of the current instance in the ISO format,
317  *        eg. 2016-08-29T21:32:31.588539814+02:00.
318  */
toIsoString(TimeSpan timeZoneDelta) const319 string DateTime::toIsoString(TimeSpan timeZoneDelta) const
320 {
321     return toIsoStringWithCustomDelimiters(timeZoneDelta);
322 }
323 
324 /*!
325  * \brief Returns the string representation as C-style string for the given day of week.
326  *
327  * If \a abbreviation is true, only the first three letters of the string will
328  * be returned.
329  * \sa DayOfWeek
330  */
printDayOfWeek(DayOfWeek dayOfWeek,bool abbreviation)331 const char *DateTime::printDayOfWeek(DayOfWeek dayOfWeek, bool abbreviation)
332 {
333     if (abbreviation) {
334         switch (dayOfWeek) {
335         case DayOfWeek::Monday:
336             return "Mon";
337         case DayOfWeek::Tuesday:
338             return "Tue";
339         case DayOfWeek::Wednesday:
340             return "Wed";
341         case DayOfWeek::Thursday:
342             return "Thu";
343         case DayOfWeek::Friday:
344             return "Fri";
345         case DayOfWeek::Saturday:
346             return "Sat";
347         case DayOfWeek::Sunday:
348             return "Sun";
349         }
350     } else {
351         switch (dayOfWeek) {
352         case DayOfWeek::Monday:
353             return "Monday";
354         case DayOfWeek::Tuesday:
355             return "Tuesday";
356         case DayOfWeek::Wednesday:
357             return "Wednesday";
358         case DayOfWeek::Thursday:
359             return "Thursday";
360         case DayOfWeek::Friday:
361             return "Friday";
362         case DayOfWeek::Saturday:
363             return "Saturday";
364         case DayOfWeek::Sunday:
365             return "Sunday";
366         }
367     }
368     return "";
369 }
370 
371 #if defined(PLATFORM_UNIX) && !defined(PLATFORM_MAC)
372 /*!
373  * \brief Returns a DateTime object that is set to the current date and time on this computer, expressed as the GMT time.
374  * \remarks Only available under UNIX-like systems supporting clock_gettime().
375  */
exactGmtNow()376 DateTime DateTime::exactGmtNow()
377 {
378     struct timespec t;
379     clock_gettime(CLOCK_REALTIME, &t);
380     return DateTime(DateTime::unixEpochStart().totalTicks() + static_cast<std::uint64_t>(t.tv_sec) * TimeSpan::ticksPerSecond
381         + static_cast<std::uint64_t>(t.tv_nsec) / 100);
382 }
383 #endif
384 
385 /*!
386  * \brief Converts the given date expressed in \a year, \a month and \a day to ticks.
387  */
dateToTicks(int year,int month,int day)388 std::uint64_t DateTime::dateToTicks(int year, int month, int day)
389 {
390     if (!inRangeInclMax(year, 1, 9999)) {
391         throw ConversionException("year is out of range");
392     }
393     if (!inRangeInclMax(month, 1, 12)) {
394         throw ConversionException("month is out of range");
395     }
396     const auto *const daysToMonth = reinterpret_cast<const int *>(isLeapYear(year) ? m_daysToMonth366 : m_daysToMonth365);
397     const int passedMonth = month - 1;
398     if (!inRangeInclMax(day, 1, daysToMonth[month] - daysToMonth[passedMonth])) {
399         throw ConversionException("day is out of range");
400     }
401     const auto passedYears = static_cast<unsigned int>(year - 1);
402     const auto passedDays = static_cast<unsigned int>(day - 1);
403     return (passedYears * m_daysPerYear + passedYears / 4 - passedYears / 100 + passedYears / 400
404                + static_cast<unsigned int>(daysToMonth[passedMonth]) + passedDays)
405         * TimeSpan::ticksPerDay;
406 }
407 
408 /*!
409  * \brief Converts the given time expressed in \a hour, \a minute, \a second and \a millisecond to ticks.
410  */
timeToTicks(int hour,int minute,int second,double millisecond)411 std::uint64_t DateTime::timeToTicks(int hour, int minute, int second, double millisecond)
412 {
413     if (!inRangeExclMax(hour, 0, 24)) {
414         throw ConversionException("hour is out of range");
415     }
416     if (!inRangeExclMax(minute, 0, 60)) {
417         throw ConversionException("minute is out of range");
418     }
419     if (!inRangeExclMax(second, 0, 60)) {
420         throw ConversionException("second is out of range");
421     }
422     if (!inRangeExclMax(millisecond, 0.0, 1000.0)) {
423         throw ConversionException("millisecond is out of range");
424     }
425     return static_cast<std::uint64_t>(hour * TimeSpan::ticksPerHour) + static_cast<std::uint64_t>(minute * TimeSpan::ticksPerMinute)
426         + static_cast<std::uint64_t>(second * TimeSpan::ticksPerSecond) + static_cast<std::uint64_t>(millisecond * TimeSpan::ticksPerMillisecond);
427 }
428 
429 /*!
430  * \brief Returns the specified date part.
431  * \sa DatePart
432  */
getDatePart(DatePart part) const433 int DateTime::getDatePart(DatePart part) const
434 {
435     const auto fullDays = static_cast<int>(m_ticks / TimeSpan::ticksPerDay);
436     const auto full400YearBlocks = fullDays / m_daysPer400Years;
437     const auto daysMinusFull400YearBlocks = fullDays - full400YearBlocks * m_daysPer400Years;
438     auto full100YearBlocks = daysMinusFull400YearBlocks / m_daysPer100Years;
439     if (full100YearBlocks == 4) {
440         full100YearBlocks = 3;
441     }
442     const auto daysMinusFull100YearBlocks = daysMinusFull400YearBlocks - full100YearBlocks * m_daysPer100Years;
443     const auto full4YearBlocks = daysMinusFull100YearBlocks / m_daysPer4Years;
444     const auto daysMinusFull4YearBlocks = daysMinusFull100YearBlocks - full4YearBlocks * m_daysPer4Years;
445     auto full1YearBlocks = daysMinusFull4YearBlocks / m_daysPerYear;
446     if (full1YearBlocks == 4) {
447         full1YearBlocks = 3;
448     }
449     if (part == DatePart::Year) {
450         return full400YearBlocks * 400 + full100YearBlocks * 100 + full4YearBlocks * 4 + full1YearBlocks + 1;
451     }
452     const auto restDays = daysMinusFull4YearBlocks - full1YearBlocks * m_daysPerYear;
453     if (part == DatePart::DayOfYear) { // day
454         return restDays + 1;
455     }
456     const auto *const daysToMonth = (full1YearBlocks == 3 && (full4YearBlocks != 24 || full100YearBlocks == 3)) ? m_daysToMonth366 : m_daysToMonth365;
457     auto month = 1;
458     while (restDays >= daysToMonth[month]) {
459         ++month;
460     }
461     if (part == DatePart::Month) {
462         return month;
463     } else if (part == DatePart::Day) {
464         return restDays - daysToMonth[month - 1] + 1;
465     }
466     return 0;
467 }
468 
469 } // namespace CppUtilities
470