Xclox
C++11 header-only cross-platform date and time library with an asynchronous NTP client
date.hpp
1 /*
2  * Copyright (c) 2024 Abdullatif Kalla.
3  *
4  * This source code is licensed under the MIT license found in the
5  * LICENSE.txt file in the root directory of this source tree.
6  */
7 
8 #ifndef XCLOX_DATE_HPP
9 #define XCLOX_DATE_HPP
10 
11 #include "internal.hpp"
12 
13 namespace xclox {
14 
15 namespace internal {
16 
17  inline Days ymdToDays(int year, int month, int day)
18  {
19  // Math from http://howardhinnant.github.io/date_algorithms.html
20  auto const y = static_cast<int>(year) - (month <= 2) + (year < 1); // if the year is negative; i.e. before common era, add one year.
21  auto const m = static_cast<unsigned>(month);
22  auto const d = static_cast<unsigned>(day);
23  auto const era = (y >= 0 ? y : y - 399) / 400;
24  auto const yoe = static_cast<unsigned>(y - era * 400); // [0, 399]
25  auto const doy = (153 * (m > 2 ? m - 3 : m + 9) + 2) / 5 + d - 1; // [0, 365]
26  auto const doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; // [0, 146096]
27 
28  return Days { era * 146097 + static_cast<long>(doe) - 719468 };
29  }
30 
31  inline void daysToYmd(Days dys, int* year, int* month, int* day)
32  {
33  // Math from http://howardhinnant.github.io/date_algorithms.html
34  auto const z = dys.count() + 719468;
35  auto const era = (z >= 0 ? z : z - 146096) / 146097;
36  auto const doe = static_cast<unsigned long>(z - era * 146097); // [0, 146096]
37  auto const yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365; // [0, 399]
38  auto const y = static_cast<Days::rep>(yoe) + era * 400;
39  auto const doy = doe - (365 * yoe + yoe / 4 - yoe / 100); // [0, 365]
40  auto const mp = (5 * doy + 2) / 153; // [0, 11]
41  auto const m = mp < 10 ? mp + 3 : mp - 9; // [1, 12]
42  auto const d = doy - (153 * mp + 2) / 5 + 1; // [1, 31]
43  if (year) {
44  *year = y + (m <= 2);
45  *year = *year - (*year < 1); // if the year is negative; i.e. before common era, subtract one year.;
46  }
47  if (month)
48  *month = static_cast<int>(m);
49  if (day)
50  *day = static_cast<int>(d);
51  }
52 
53 } // namespace internal
54 
84 class Date {
85 public:
91  using Days = internal::Days;
92  using Weeks = std::chrono::duration<long, std::ratio_multiply<std::ratio<7>, Days::period>>;
93 
95 
105  enum class Weekday {
106  Monday = 1,
107  Tuesday = 2,
108  Wednesday = 3,
109  Thursday = 4,
110  Friday = 5,
111  Saturday = 6,
112  Sunday = 7
113  };
114 
119  enum class Month {
120  January = 1,
121  February = 2,
122  March = 3,
123  April = 4,
124  May = 5,
125  June = 6,
126  July = 7,
127  August = 8,
128  September = 9,
129  October = 10,
130  November = 11,
131  December = 12
132  };
133 
135 
143  : m_year(0)
144  , m_month(0)
145  , m_day(0)
146  {
147  }
148 
150  Date(const Date& other) = default;
151 
153  Date(Date&& other) = default;
154 
156  explicit Date(const Days& days)
157  {
158  internal::daysToYmd(days, &m_year, &m_month, &m_day);
159  }
160 
162  explicit Date(int year, int month, int day)
163  : m_year(year)
164  , m_month(month)
165  , m_day(day)
166  {
167  }
168 
170  ~Date() = default;
171 
173 
180  Date& operator=(const Date& other) = default;
181 
183  Date& operator=(Date&& other) = default;
184 
186 
193  bool operator<(const Date& other) const
194  {
195  return this->year() < other.year() || (this->year() == other.year() && this->month() < other.month()) || (this->year() == other.year() && this->month() == other.month() && this->day() < other.day());
196  }
197 
199  bool operator<=(const Date& other) const
200  {
201  return this->operator<(other) || this->operator==(other);
202  }
203 
205  bool operator>(const Date& other) const
206  {
207  return this->year() > other.year() || (this->year() == other.year() && this->month() > other.month()) || (this->year() == other.year() && this->month() == other.month() && this->day() > other.day());
208  }
209 
211  bool operator>=(const Date& other) const
212  {
213  return this->operator>(other) || this->operator==(other);
214  }
215 
217  bool operator==(const Date& other) const
218  {
219  return this->year() == other.year() && this->month() == other.month() && this->day() == other.day();
220  }
221 
223  bool operator!=(const Date& other) const
224  {
225  return this->year() != other.year() || this->month() != other.month() || this->day() != other.day();
226  }
227 
229 
248  bool isValid() const
249  {
250  return m_year != 0 && (m_month > 0 && m_month < 13) && (m_day > 0 && m_day < (daysInMonthOfYear(m_year, m_month) + 1));
251  }
252 
254  void getYearMonthDay(int* year, int* month, int* day) const
255  {
256  if (year)
257  *year = m_year;
258  if (month)
259  *month = m_month;
260  if (day)
261  *day = m_day;
262  }
263 
265  int day() const
266  {
267  return m_day;
268  }
269 
271  int month() const
272  {
273  return m_month;
274  }
275 
280  int year() const
281  {
282  return m_year;
283  }
284 
286  int dayOfWeek() const
287  {
288  return (toDaysSinceEpoch() % 7 + 3) % 7 + 1;
289  }
290 
292  int dayOfYear() const
293  {
294  return toDaysSinceEpoch() - internal::ymdToDays(year(), 1, 1).count() + 1;
295  }
296 
298  int daysInMonth() const
299  {
300  return daysInMonthOfYear(year(), month());
301  }
302 
304  int daysInYear() const
305  {
306  return (isLeapYear() ? 366 : 365);
307  }
308 
310  bool isLeapYear() const
311  {
312  return isLeapYear(year());
313  }
314 
324  int weekOfYear(int* weekYear = nullptr) const
325  {
326  static auto getFirstWeekDate = [](int year) {
327  Date d(year, 1, 1);
328  return d.addDays((11 - d.dayOfWeek()) % 7 - 3);
329  };
330  int y = year();
331  Date currentDate = *this;
332  Date firstWeekDate = getFirstWeekDate(y);
333  if (currentDate < firstWeekDate) {
334  // If the given date is earlier than the start of the first week of the year that contains it, then the date belongs to the last week of the previous year.
335  --y;
336  firstWeekDate = getFirstWeekDate(y);
337  } else {
338  Date nextYearFirstWeekDate = getFirstWeekDate(y + 1);
339  if (currentDate >= nextYearFirstWeekDate) {
340  // If the given date is on or after the start of the first week of the next year, then the date belongs to the first week of the next year.
341  ++y;
342  firstWeekDate = nextYearFirstWeekDate;
343  }
344  }
345  int week = daysBetween(firstWeekDate, currentDate) / 7 + 1;
346  if (weekYear)
347  *weekYear = y;
348  return week;
349  }
350 
367  std::string dayOfWeekName(bool shortName = false) const
368  {
369  return shortName ? internal::getShortWeekdayName(dayOfWeek()) : internal::getLongWeekdayName(dayOfWeek());
370  }
371 
393  std::string monthName(bool shortName = false) const
394  {
395  return shortName ? internal::getShortMonthName(month()) : internal::getLongMonthName(month());
396  }
397 
399 
406  Date addDays(int days) const
407  {
408  int y, m, d;
409  internal::daysToYmd(Days(internal::ymdToDays(m_year, m_month, m_day) + Days(days)), &y, &m, &d);
410  return Date(y, m, d);
411  }
412 
414  Date subtractDays(int days) const
415  {
416  int y, m, d;
417  internal::daysToYmd(Days(internal::ymdToDays(m_year, m_month, m_day) - Days(days)), &y, &m, &d);
418  return Date(y, m, d);
419  }
420 
429  Date addMonths(int months) const
430  {
431  if (months < 0)
432  return subtractMonths(-months);
433 
434  const int totalMonths = m_month + months - 1;
435  const int newYear = m_year + (totalMonths / 12);
436  const int newMonth = (totalMonths % 12) + 1;
437  const int newDaysInMonth = daysInMonthOfYear(newYear, newMonth);
438  const int newDays = newDaysInMonth < m_day ? newDaysInMonth : m_day;
439 
440  return Date(newYear, newMonth, newDays);
441  }
442 
451  Date subtractMonths(int months) const
452  {
453  if (months < 0)
454  return addMonths(-months);
455 
456  const int newYear = m_year - (std::abs(m_month - months - 12) / 12);
457  const int newMonth = ((11 + m_month - (months % 12)) % 12) + 1;
458  const int newDaysInMonth = daysInMonthOfYear(newYear, newMonth);
459  const int newDays = newDaysInMonth < m_day ? newDaysInMonth : m_day;
460 
461  return Date(newYear, newMonth, newDays);
462  }
463 
465  Date addYears(int years) const
466  {
467  const int newYear = m_year + years;
468  return Date(newYear > 0 ? newYear : newYear - 1, m_month, m_day);
469  }
470 
472  Date subtractYears(int years) const
473  {
474  const int newYear = m_year - years;
475  return Date(newYear > 0 ? newYear : newYear - 1, m_month, m_day);
476  }
477 
479 
486  long toDaysSinceEpoch() const
487  {
488  return internal::ymdToDays(year(), month(), day()).count();
489  }
490 
493  {
494  return Days(toDaysSinceEpoch());
495  }
496 
503  long toJulianDay() const
504  {
505  return toDaysSinceEpoch() + 2440588;
506  }
507 
532  std::string toString(const std::string& format) const
533  {
534  if (!isValid())
535  return std::string();
536 
537  std::stringstream output;
538 
539  for (size_t pos = 0; pos < format.size(); ++pos) {
540  int y, m, d;
541  getYearMonthDay(&y, &m, &d);
542  y = std::abs(y);
543 
544  char currChar = format[pos];
545  const int charCount = internal::countIdenticalCharsFrom(pos, format);
546 
547  if (currChar == '#') {
548  output << (year() < 0 ? "-" : "+");
549  } else if (currChar == 'y') {
550  if (charCount == 1) {
551  output << y;
552  } else if (charCount == 2) {
553  y = y - ((y / 100) * 100);
554  output << std::setfill('0') << std::setw(2) << y;
555  } else if (charCount == 4) {
556  output << std::setfill('0') << std::setw(4) << y;
557  }
558  pos += charCount - 1; // skip all identical characters except the last.
559  } else if (currChar == 'M') {
560  if (charCount == 1) {
561  output << m;
562  } else if (charCount == 2) {
563  output << std::setfill('0') << std::setw(2) << m;
564  } else if (charCount == 3) {
565  output << monthName(true);
566  } else if (charCount == 4) {
567  output << monthName(false);
568  }
569  pos += charCount - 1; // skip all identical characters except the last.
570  } else if (currChar == 'd') {
571  if (charCount == 1) {
572  output << d;
573  } else if (charCount == 2) {
574  output << std::setfill('0') << std::setw(2) << d;
575  } else if (charCount == 3) {
576  output << dayOfWeekName(true);
577  } else if (charCount == 4) {
578  output << dayOfWeekName(false);
579  }
580  pos += charCount - 1; // skip all identical characters except the last.
581  } else if (currChar == 'E') {
582  output << (year() < 0 ? "BCE" : "CE");
583  } else {
584  output << currChar;
585  }
586  }
587 
588  return output.str();
589  }
590 
592 
597  static Date current()
598  {
599  return Date(std::chrono::duration_cast<Days>(std::chrono::system_clock::now().time_since_epoch()));
600  }
601 
603  static Date epoch()
604  {
605  return Date(Days(0));
606  }
607 
612  static Date fromString(const std::string& date, const std::string& format)
613  {
614  int _year = 1, _month = 1, _day = 1;
615 
616  for (size_t fmtPos = 0, datPos = 0; fmtPos < format.size() && datPos < date.size(); ++fmtPos) {
617  const size_t charCount = static_cast<size_t>(internal::countIdenticalCharsFrom(fmtPos, format));
618 
619  if (format[fmtPos] == '#') {
620  if (date[datPos] == '+') {
621  _year = 1;
622  ++datPos;
623  } else if (date[datPos] == '-') {
624  _year = -1;
625  ++datPos;
626  }
627  } else if (format[fmtPos] == 'y') {
628  if (charCount == 1) {
629  _year = _year * internal::readIntAndAdvancePos(date, datPos, 4);
630  } else if (charCount == 2) {
631  _year = _year * std::stoi(date.substr(datPos, charCount));
632  _year += 2000;
633  datPos += charCount;
634  } else if (charCount == 4) {
635  _year = _year * std::stoi(date.substr(datPos, charCount));
636  datPos += charCount;
637  }
638  fmtPos += charCount - 1; // skip all identical characters except the last.
639  } else if (format[fmtPos] == 'E') {
640  if (date.substr(datPos, 2) == "CE") {
641  _year = std::abs(_year);
642  datPos += 2;
643  } else if (date.substr(datPos, 3) == "BCE") {
644  _year = -std::abs(_year);
645  datPos += 3;
646  }
647  } else if (format[fmtPos] == 'M') {
648  if (charCount == 1) {
649  _month = internal::readIntAndAdvancePos(date, datPos, 4);
650  } else if (charCount == 2) {
651  _month = std::stoi(date.substr(datPos, charCount));
652  datPos += charCount;
653  } else if (charCount == 3) {
654  _month = internal::getShortMonthNumber(date.substr(datPos, charCount));
655  datPos += charCount;
656  } else if (charCount == 4) {
657  size_t newPos = datPos;
658  while (newPos < date.size() && std::isalpha(date[newPos]))
659  ++newPos;
660  _month = internal::getLongMonthNumber(date.substr(datPos, newPos - datPos));
661  datPos = newPos;
662  }
663  fmtPos += charCount - 1; // skip all identical characters except the last.
664  } else if (format[fmtPos] == 'd') {
665  if (charCount == 1) {
666  _day = internal::readIntAndAdvancePos(date, datPos, 2);
667  } else if (charCount == 2) {
668  _day = std::stoi(date.substr(datPos, charCount));
669  datPos += charCount;
670  } else if (charCount == 3) {
671  // lets the format string and the date string be in sync.
672  datPos += charCount;
673  } else if (charCount == 4) {
674  while (datPos < date.size() && std::isalpha(date[datPos]))
675  ++datPos;
676  }
677  fmtPos += charCount - 1; // skip all identical characters except the last.
678  } else {
679  // not a pattern, skip it in the date string.
680  ++datPos;
681  }
682  }
683 
684  return Date(_year, _month, _day);
685  }
686 
688  static Date fromJulianDay(long julianDay)
689  {
690  return Date(Days(julianDay - 2440588));
691  }
692 
707  static long daysBetween(const Date& from, const Date& to)
708  {
709  return to.toDaysSinceEpoch() - from.toDaysSinceEpoch();
710  }
711 
719  static long weeksBetween(const Date& from, const Date& to)
720  {
721  return daysBetween(from, to) / 7;
722  }
723 
725 
734  static bool isLeapYear(int year)
735  {
736  // no year 0 in the Gregorian calendar, the first year before the common era is -1 (year 1 BCE). So, -1, -5, -9 etc are leap years.
737  if (year < 1)
738  ++year;
739 
740  return (year % 4 == 0) && (year % 100 != 0 || year % 400 == 0);
741  }
742 
744  static int daysInMonthOfYear(int year, int month)
745  {
746  switch (month) {
747 
748  case 1:
749  return 31;
750  case 2:
751  return (isLeapYear(year) ? 29 : 28);
752  case 3:
753  return 31;
754  case 4:
755  return 30;
756  case 5:
757  return 31;
758  case 6:
759  return 30;
760  case 7:
761  return 31;
762  case 8:
763  return 31;
764  case 9:
765  return 30;
766  case 10:
767  return 31;
768  case 11:
769  return 30;
770  case 12:
771  return 31;
772  }
773 
774  return -1;
775  }
776 
777 private:
778  int m_year;
779  int m_month;
780  int m_day;
781 };
782 
793 std::ostream& operator<<(std::ostream& os, const Date& d)
794 {
795  os << d.toString("yyyy-MM-dd");
796  return os;
797 }
798 
803 std::istream& operator>>(std::istream& is, Date& d)
804 {
805  const int DateFormatWidth = 10;
806  char result[DateFormatWidth];
807  is.read(result, DateFormatWidth);
808  d = Date::fromString(std::string(result, DateFormatWidth), "yyyy-MM-dd");
809 
810  return is;
811 }
812 
814 
815 } // namespace xclox
816 
817 #endif // XCLOX_DATE_HPP
Date is an immutable class representing a date without a time zone in the ISO-8601 calendar system,...
Definition: date.hpp:84
std::string monthName(bool shortName=false) const
Returns the name of the month of this date.
Definition: date.hpp:393
bool operator<(const Date &other) const
Returns whether this date is earlier than other.
Definition: date.hpp:193
Date subtractDays(int days) const
Returns the result of subtracting days from this date as a new Date object.
Definition: date.hpp:414
int dayOfWeek() const
Returns the weekday of this date as a number between 1 and 7, which corresponds to the enumeration We...
Definition: date.hpp:286
bool operator>(const Date &other) const
Returns whether this date is later than other.
Definition: date.hpp:205
long toDaysSinceEpoch() const
Returns the number of elapsed days since the epoch "1970-01-01".
Definition: date.hpp:486
std::string toString(const std::string &format) const
Returns this date as a string, formatted according to the format string format.
Definition: date.hpp:532
Month
Type of month.
Definition: date.hpp:119
Date(Date &&other)=default
Move-constructs a Date object from other.
int daysInYear() const
Returns the number of days in the year of this date. It is either 365 or 366.
Definition: date.hpp:304
bool isValid() const
Returns whether this date object represents a valid date.
Definition: date.hpp:248
static bool isLeapYear(int year)
Returns whether year is a leap year.
Definition: date.hpp:734
internal::Days Days
Day duration.
Definition: date.hpp:91
void getYearMonthDay(int *year, int *month, int *day) const
Set the year, month, and day of this date in the parameters year, month, and day, respectively.
Definition: date.hpp:254
Date subtractYears(int years) const
Returns the result of subtracting years from this date as a new Date object.
Definition: date.hpp:472
int year() const
Returns the year of this date as a number.
Definition: date.hpp:280
std::chrono::duration< long, std::ratio_multiply< std::ratio< 7 >, Days::period > > Weeks
Week duration.
Definition: date.hpp:92
Date(const Days &days)
Constructs a Date object from days elapsed since the epoch "1970-01-01".
Definition: date.hpp:156
int daysInMonth() const
Returns the number of days in the month of this date. It ranges between 28 and 31.
Definition: date.hpp:298
static int daysInMonthOfYear(int year, int month)
Returns the number of days in month of year. It ranges between 28 and 31.
Definition: date.hpp:744
bool operator<=(const Date &other) const
Returns whether this date is earlier than other or equal to it.
Definition: date.hpp:199
Date addYears(int years) const
Returns the result of adding years to this date as a new Date object.
Definition: date.hpp:465
bool operator==(const Date &other) const
Returns whether this date is equal to other.
Definition: date.hpp:217
Date subtractMonths(int months) const
Returns the result of subtracting months from this date as a new Date object.
Definition: date.hpp:451
Weekday
Type of weekday.
Definition: date.hpp:105
Date addDays(int days) const
Returns the result of adding days to this date as a new Date object.
Definition: date.hpp:406
bool operator>=(const Date &other) const
Returns whether this date is later than other or equal to it.
Definition: date.hpp:211
static Date fromJulianDay(long julianDay)
Returns a Date object corresponding to the Julian day julianDay.
Definition: date.hpp:688
std::string dayOfWeekName(bool shortName=false) const
Returns the name of the weekday of this date.
Definition: date.hpp:367
static Date current()
Returns a Date object set to the current date obtained from the system clock.
Definition: date.hpp:597
Date(int year, int month, int day)
Constructs a Date object from the given year, month and day.
Definition: date.hpp:162
int month() const
Returns the month of the year of this date as a number between 1 and 12, which corresponds to the enu...
Definition: date.hpp:271
Date()
Constructs an invalid Date object with every field is set to zero.
Definition: date.hpp:142
Date & operator=(Date &&other)=default
Move assignment operator.
bool operator!=(const Date &other) const
Returns whether this date is different from other.
Definition: date.hpp:223
std::istream & operator>>(std::istream &is, Date &d)
Reads a date in ISO-8601 date format "yyyy-MM-dd" from stream is and stores it in date d.
Definition: date.hpp:803
static Date epoch()
Returns a Date object set to the epoch "1970-01-01".
Definition: date.hpp:603
int day() const
Returns the day of the month of this date as a number between 1 and 31.
Definition: date.hpp:265
Days toStdDurationSinceEpoch() const
Returns the elapsed time since the epoch "1970-01-01" as a Days duration.
Definition: date.hpp:492
static long weeksBetween(const Date &from, const Date &to)
Returns the number of weeks between from and to.
Definition: date.hpp:719
long toJulianDay() const
Returns the corresponding Julian Day Number (JDN) of this date.
Definition: date.hpp:503
static long daysBetween(const Date &from, const Date &to)
Returns the number of days between from and to.
Definition: date.hpp:707
static Date fromString(const std::string &date, const std::string &format)
Returns a Date object from the date string date according to the format string format.
Definition: date.hpp:612
Date(const Date &other)=default
Copy-constructs a Date object from other.
int weekOfYear(int *weekYear=nullptr) const
Returns the week of the year of this date, and optionally stores the year in weekYear.
Definition: date.hpp:324
int dayOfYear() const
Returns the day of year of this date as a number between 1 and 365 (1 to 366 on leap years).
Definition: date.hpp:292
~Date()=default
Default destructor.
Date addMonths(int months) const
Returns the result of adding months to this date as a new Date object.
Definition: date.hpp:429
std::ostream & operator<<(std::ostream &os, const Date &d)
Writes date d to stream os in ISO-8601 date format, "yyyy-MM-dd".
Definition: date.hpp:793
Date & operator=(const Date &other)=default
Copy assignment operator.
bool isLeapYear() const
Returns whether the year of this date is a leap year.
Definition: date.hpp:310