Xclox
C++11 header-only cross-platform date and time library with an asynchronous NTP client
datetime.h
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 #include "xclox/datetime.hpp"
9 
10 using namespace xclox;
11 using namespace std::chrono;
12 
13 TEST_SUITE("DateTime")
14 {
15  constexpr seconds NtpDeltaSeconds { 2208988800 };
16  constexpr seconds UnixRolloverSeconds { 2147483647 };
17 
18  auto compare = [](const DateTime& dt, int year, int month, int day, int hour, int minute, int second, int milli, int micro, int nano) {
19  return dt.year() == year && dt.month() == month && dt.day() == day
20  && dt.hour() == hour && dt.minute() == minute && dt.second() == second
21  && dt.millisecond() == milli && dt.microsecond() == micro && dt.nanosecond() == nano;
22  };
23 
24  TEST_CASE("constructible")
25  {
26  SUBCASE("default")
27  {
28  DateTime dt;
29 
30  CHECK(compare(dt, 0, 0, 0, 0, 0, 0, 0, 0, 0));
31  }
32  SUBCASE("values")
33  {
34  SUBCASE("before unix rollover")
35  {
36  DateTime dt1(Date(2038, 1, 19), Time(3, 14, 6, Time::Nanoseconds(999999999)));
37  DateTime dt2(UnixRolloverSeconds - seconds(1) + nanoseconds(999999999));
38  DateTime dt3(system_clock::time_point(UnixRolloverSeconds - seconds(1) + microseconds(999999)));
39 
40  CHECK(compare(dt1, 2038, 1, 19, 3, 14, 6, 999, 999999, 999999999));
41  CHECK(compare(dt2, 2038, 1, 19, 3, 14, 6, 999, 999999, 999999999));
42  CHECK(compare(dt3, 2038, 1, 19, 3, 14, 6, 999, 999999, 999999000));
43  }
44  SUBCASE("unix rollover")
45  {
46  DateTime dt1(Date(2038, 1, 19), Time(3, 14, 7));
47  DateTime dt2(UnixRolloverSeconds);
48  DateTime dt3(system_clock::time_point { UnixRolloverSeconds });
49 
50  CHECK(compare(dt1, 2038, 1, 19, 3, 14, 7, 0, 0, 0));
51  CHECK(compare(dt2, 2038, 1, 19, 3, 14, 7, 0, 0, 0));
52  CHECK(compare(dt3, 2038, 1, 19, 3, 14, 7, 0, 0, 0));
53  }
54  SUBCASE("after unix rollover")
55  {
56  DateTime dt1(Date(2038, 1, 19), Time(3, 14, 8, Time::Milliseconds(500)));
57  DateTime dt2(UnixRolloverSeconds + milliseconds(1500));
58  DateTime dt3(system_clock::time_point(UnixRolloverSeconds + milliseconds(1500)));
59 
60  CHECK(compare(dt1, 2038, 1, 19, 3, 14, 8, 500, 500000, 500000000));
61  CHECK(compare(dt2, 2038, 1, 19, 3, 14, 8, 500, 500000, 500000000));
62  CHECK(compare(dt3, 2038, 1, 19, 3, 14, 8, 500, 500000, 500000000));
63  }
64  SUBCASE("before unix epoch")
65  {
66  DateTime dt1(Date(1969, 12, 31), Time(23, 59, 59, Time::Nanoseconds(123456789)));
67  DateTime dt2(nanoseconds(-876543211));
68  DateTime dt3(system_clock::time_point(microseconds(-876544)));
69 
70  CHECK(compare(dt1, 1969, 12, 31, 23, 59, 59, 123, 123456, 123456789));
71  CHECK(compare(dt2, 1969, 12, 31, 23, 59, 59, 123, 123456, 123456789));
72  CHECK(compare(dt3, 1969, 12, 31, 23, 59, 59, 123, 123456, 123456000));
73  }
74  SUBCASE("unix epoch")
75  {
76  DateTime dt1(Date(1970, 1, 1), Time(0, 0, 0));
77  DateTime dt2(seconds(0));
78  DateTime dt3(system_clock::time_point(seconds(0)));
79 
80  CHECK(compare(dt1, 1970, 1, 1, 0, 0, 0, 0, 0, 0));
81  CHECK(compare(dt2, 1970, 1, 1, 0, 0, 0, 0, 0, 0));
82  CHECK(compare(dt3, 1970, 1, 1, 0, 0, 0, 0, 0, 0));
83  }
84  SUBCASE("after unix epoch")
85  {
86  DateTime dt1(Date(1970, 1, 1), Time(0, 0, 0, Time::Nanoseconds(1)));
87  DateTime dt2(nanoseconds(1));
88  DateTime dt3(system_clock::time_point(microseconds(1)));
89 
90  CHECK(compare(dt1, 1970, 1, 1, 0, 0, 0, 0, 0, 1));
91  CHECK(compare(dt2, 1970, 1, 1, 0, 0, 0, 0, 0, 1));
92  CHECK(compare(dt3, 1970, 1, 1, 0, 0, 0, 0, 1, 1000));
93  }
94  SUBCASE("before ntp epoch")
95  {
96  DateTime dt1(Date(1899, 12, 31), Time(23, 59, 59, Time::Nanoseconds(999999999)));
97  DateTime dt2(-NtpDeltaSeconds - nanoseconds(1));
98  DateTime dt3(system_clock::time_point(-NtpDeltaSeconds - microseconds(1)));
99 
100  CHECK(compare(dt1, 1899, 12, 31, 23, 59, 59, 999, 999999, 999999999));
101  CHECK(compare(dt2, 1899, 12, 31, 23, 59, 59, 999, 999999, 999999999));
102  CHECK(compare(dt3, 1899, 12, 31, 23, 59, 59, 999, 999999, 999999000));
103  }
104  SUBCASE("ntp epoch")
105  {
106  DateTime dt1(Date(1900, 1, 1), Time(0, 0, 0));
107  DateTime dt2(-NtpDeltaSeconds);
108  DateTime dt3(system_clock::time_point(-NtpDeltaSeconds));
109 
110  CHECK(compare(dt1, 1900, 1, 1, 0, 0, 0, 0, 0, 0));
111  CHECK(compare(dt2, 1900, 1, 1, 0, 0, 0, 0, 0, 0));
112  CHECK(compare(dt3, 1900, 1, 1, 0, 0, 0, 0, 0, 0));
113  }
114  SUBCASE("after ntp epoch")
115  {
116  DateTime dt1(Date(1900, 1, 1), Time(0, 0, 0, DateTime::Nanoseconds(1)));
117  DateTime dt2(-NtpDeltaSeconds + nanoseconds(1));
118  DateTime dt3(system_clock::time_point(-NtpDeltaSeconds + microseconds(1)));
119 
120  CHECK(compare(dt1, 1900, 1, 1, 0, 0, 0, 0, 0, 1));
121  CHECK(compare(dt2, 1900, 1, 1, 0, 0, 0, 0, 0, 1));
122  CHECK(compare(dt3, 1900, 1, 1, 0, 0, 0, 0, 1, 1000));
123  }
124  }
125  }
126 
127  TEST_CASE("validatable")
128  {
129  CHECK(DateTime().isValid() == false);
130  CHECK(DateTime(Date(), Time()).isValid() == false);
131  CHECK(DateTime(Date(2001, 2, 3), Time(Time::Hours(-1))).isValid() == false);
132  CHECK(DateTime(Date(0, 1, 2), Time(Time::Hours(1))).isValid() == false);
133  CHECK(DateTime(Date(2006, 5, 4), Time(3, 2, 1)).isValid() == true);
134  CHECK(DateTime(microseconds(-1)).isValid() == true);
135  CHECK(DateTime(system_clock::time_point(microseconds(-1))).isValid() == true);
136  //
137  // additional tests.
138  //
139  CHECK(DateTime(system_clock::now().time_since_epoch()).isValid() == true);
140  CHECK(DateTime(system_clock::now()).isValid() == true);
141  CHECK(DateTime::current().isValid() == true);
142  CHECK(DateTime::epoch().isValid() == true);
143  }
144 
145  TEST_CASE("comparable")
146  {
147  CHECK(DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))) < DateTime(Date(2017, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))));
148  CHECK(DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))) <= DateTime(Date(2017, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))));
149  CHECK(DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))) <= DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))));
150  CHECK(DateTime(Date(2012, 9, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))) > DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))));
151  CHECK(DateTime(Date(2012, 3, 27), Time(9, 55, 21, Time::Nanoseconds(123456789))) >= DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))));
152  CHECK(DateTime(Date(2015, 3, 27), Time(1, 55, 21, Time::Nanoseconds(123456789))) >= DateTime(Date(2015, 3, 27), Time(1, 55, 21, Time::Nanoseconds(123456789))));
153  CHECK(DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))) == DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))));
154  CHECK(DateTime(Date(2017, 12, 6), Time(16, 32, 4, Time::Nanoseconds(987654321))) != DateTime(Date(2012, 3, 27), Time(8, 55, 21, Time::Nanoseconds(123456789))));
155  }
156 
157  TEST_CASE("copyable")
158  {
159  DateTime dt1;
160  DateTime dt2(Date(2001, 2, 3), Time(4, 5, 6));
161  DateTime dt3(Date(2006, 5, 4), Time(3, 2, 1));
162 
163  SUBCASE("construction")
164  {
165  DateTime dt4(dt1);
166  DateTime dt5(dt2);
167  DateTime dt6(std::move(dt3));
168 
169  CHECK(dt1 == dt4);
170  CHECK(dt2 == dt5);
171  CHECK(dt3 == dt6);
172  }
173  SUBCASE("assignment")
174  {
175  DateTime dt4 = dt1;
176  DateTime dt5 = dt2;
177  DateTime dt6 = std::move(dt3);
178 
179  CHECK(dt1 == dt4);
180  CHECK(dt2 == dt5);
181  CHECK(dt3 == dt6);
182  }
183  }
184 
185  TEST_CASE("current datetime")
186  {
187  CHECK(abs(duration_cast<milliseconds>(DateTime::current().toStdTimePoint() - system_clock::now()).count()) < 100);
188  }
189 
190  TEST_CASE("epoch")
191  {
192  CHECK(DateTime::epoch() == DateTime(Date(1970, 1, 1), Time(0, 0, 0)));
193  }
194 
195  TEST_CASE("addition & subtraction")
196  {
197  SUBCASE("add duration")
198  {
199  // Adding time durations which don't affect the date part.
200  CHECK(DateTime(Date(2045, 3, 27), Time(16, 2, 3, 4)).addDuration(DateTime::Minutes(10)) == DateTime(Date(2045, 3, 27), Time(16, 12, 3, 4)));
201  CHECK(DateTime(Date(2045, 3, 27), Time(1, 2, 3, 4)).addDuration(DateTime::Hours(3)) == DateTime(Date(2045, 3, 27), Time(4, 2, 3, 4)));
202  // Adding time durations which affect the date part.
203  CHECK(DateTime(Date(2045, 3, 27), Time(16, 2, 3, 4)).addDuration(DateTime::Hours(10)) == DateTime(Date(2045, 3, 28), Time(2, 2, 3, 4)));
204  // Adding date durations which only affect the date part.
205  CHECK(DateTime(Date(2045, 3, 27), Time(1, 2, 3, 4)).addDuration(DateTime::Days(3)) == DateTime(Date(2045, 3, 30), Time(1, 2, 3, 4)));
206  // Adding a negative duration
207  CHECK(DateTime(Date(2045, 3, 27), Time(1, 2, 3, 4)).addDuration(-DateTime::Days(3)) == DateTime(Date(2045, 3, 24), Time(1, 2, 3, 4)));
208  }
209  SUBCASE("subtract duration")
210  {
211  // Subtracting time durations which don't affect the date part.
212  CHECK(DateTime(Date(2045, 3, 27), Time(16, 2, 3, 4)).subtractDuration(DateTime::Minutes(10)) == DateTime(Date(2045, 3, 27), Time(15, 52, 3, 4)));
213  CHECK(DateTime(Date(2045, 3, 27), Time(7, 2, 3, 4)).subtractDuration(DateTime::Hours(3)) == DateTime(Date(2045, 3, 27), Time(4, 2, 3, 4)));
214  // Subtracting time durations which affect the date part.
215  CHECK(DateTime(Date(2045, 3, 27), Time(6, 2, 3, 4)).subtractDuration(DateTime::Hours(10)) == DateTime(Date(2045, 3, 26), Time(20, 2, 3, 4)));
216  // Subtracting date durations which only affect the date part.
217  CHECK(DateTime(Date(2045, 3, 27), Time(1, 2, 3, 4)).subtractDuration(DateTime::Days(3)) == DateTime(Date(2045, 3, 24), Time(1, 2, 3, 4)));
218  // Subtracting a negative duration
219  CHECK(DateTime(Date(2045, 3, 27), Time(1, 2, 3, 4)).subtractDuration(-DateTime::Days(3)) == DateTime(Date(2045, 3, 30), Time(1, 2, 3, 4)));
220  }
221  SUBCASE("operators")
222  {
223  CHECK((DateTime(Date(2017, 3, 27), Time(22, 19, 53, 4)) - DateTime(Date(2017, 3, 26), Time(22, 12, 53, 4))) == DateTime::Days(1) + DateTime::Minutes(7));
224  CHECK((DateTime(Date(2017, 12, 29), Time(23, 12, 53, 4)) + DateTime::Hours(35)) == DateTime(Date(2017, 12, 31), Time(10, 12, 53, 4)));
225  CHECK((DateTime(Date(2017, 12, 31), Time(10, 12, 53, 4)) - DateTime::Hours(35)) == DateTime(Date(2017, 12, 29), Time(23, 12, 53, 4)));
226  }
227  }
228 
229  TEST_CASE("formatting")
230  {
231  SUBCASE("empty format")
232  {
233  CHECK(DateTime(Date(2024, 2, 15), Time(2, 3, 1, 4)).toString("") == "");
234  }
235  SUBCASE("era of year")
236  {
237  SUBCASE("positive(+) or negative(-) sign")
238  {
239  CHECK(DateTime(Date(-1, 2, 3), Time(4, 5, 6)).toString("#") == "-");
240  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("#") == "+");
241  }
242  SUBCASE("CE / BCE")
243  {
244  CHECK(DateTime(Date(-1, 2, 3), Time(4, 5, 6)).toString("E") == "BCE");
245  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("E") == "CE");
246  }
247  }
248  SUBCASE("year")
249  {
250  SUBCASE("minimum number of digits (1, 9999)")
251  {
252  CHECK(DateTime(Date(-1, 2, 3), Time(4, 5, 6)).toString("y") == "1");
253  CHECK(DateTime(Date(11, 2, 3), Time(4, 5, 6)).toString("y") == "11");
254  }
255  SUBCASE("two digits (00, 99)")
256  {
257  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("yy") == "01");
258  CHECK(DateTime(Date(-1, 2, 3), Time(4, 5, 6)).toString("yy") == "01");
259  CHECK(DateTime(Date(123, 2, 3), Time(4, 5, 6)).toString("yy") == "23");
260  CHECK(DateTime(Date(-1234, 2, 3), Time(4, 5, 6)).toString("yy") == "34");
261  }
262  SUBCASE("four digits (0000, 9999)")
263  {
264  CHECK(DateTime(Date(-1, 2, 3), Time(4, 5, 6)).toString("yyyy") == "0001");
265  CHECK(DateTime(Date(12345, 2, 3), Time(4, 5, 6)).toString("yyyy") == "2345");
266  }
267  }
268  SUBCASE("month")
269  {
270  SUBCASE("minimum number of digits (1, 12)")
271  {
272  CHECK(DateTime(Date(1, 1, 3), Time(4, 5, 6)).toString("M") == "1");
273  }
274  SUBCASE("two digits (01, 12)")
275  {
276  CHECK(DateTime(Date(1, 1, 3), Time(4, 5, 6)).toString("MM") == "01");
277  }
278  SUBCASE("short name (e.g. Feb)")
279  {
280  CHECK(DateTime(Date(1, 1, 3), Time(4, 5, 6)).toString("MMM") == "Jan");
281  }
282  SUBCASE("long name (e.g. February)")
283  {
284  CHECK(DateTime(Date(1, 1, 3), Time(4, 5, 6)).toString("MMMM") == "January");
285  }
286  }
287  SUBCASE("day")
288  {
289  SUBCASE("day of month as one digit or more (1, 31)")
290  {
291  CHECK(DateTime(Date(1, 1, 1), Time(4, 5, 6)).toString("d") == "1");
292  }
293  SUBCASE("day of month as two digits (00, 31)")
294  {
295  CHECK(DateTime(Date(1, 1, 1), Time(4, 5, 6)).toString("dd") == "01");
296  }
297  SUBCASE("day of week as short name (e.g. Fri)")
298  {
299  CHECK(DateTime(Date(2024, 2, 18), Time(4, 5, 6)).toString("ddd") == "Sun");
300  }
301  SUBCASE("day of week as long name (e.g. Friday)")
302  {
303  CHECK(DateTime(Date(2024, 2, 18), Time(4, 5, 6)).toString("dddd") == "Sunday");
304  }
305  }
306  SUBCASE("hour")
307  {
308  SUBCASE("24-hour clock")
309  {
310  SUBCASE("minimum number of digits (0, 23)")
311  {
312  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("h") == "4");
313  }
314  SUBCASE("two digits (00, 23)")
315  {
316  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("hh") == "04");
317  }
318  }
319  SUBCASE("12-hour clock")
320  {
321  SUBCASE("minimum number of digits (0, 23)")
322  {
323  CHECK(DateTime(Date(1, 2, 3), Time(0, 5, 6)).toString("H") == "12");
324  CHECK(DateTime(Date(1, 2, 3), Time(12, 5, 6)).toString("H") == "12");
325  CHECK(DateTime(Date(1, 2, 3), Time(13, 5, 6)).toString("H") == "1");
326  }
327  SUBCASE("two digits (00, 23)")
328  {
329  CHECK(DateTime(Date(1, 2, 3), Time(14, 5, 6)).toString("HH") == "02");
330  }
331  }
332  SUBCASE("before/after noon indicator")
333  {
334  Date d(1, 2, 3);
335  DateTime dt1(d, Time(0, 0, 0));
336  DateTime dt2(d, Time(11, 59, 59));
337  DateTime dt3(d, Time(12, 0, 0));
338  DateTime dt4(d, Time(23, 59, 59));
339 
340  SUBCASE("am / pm")
341  {
342  CHECK(dt1.toString("a") == "am");
343  CHECK(dt2.toString("a") == "am");
344  CHECK(dt3.toString("a") == "pm");
345  CHECK(dt4.toString("a") == "pm");
346  }
347  SUBCASE("AM / PM")
348  {
349  CHECK(dt1.toString("A") == "AM");
350  CHECK(dt2.toString("A") == "AM");
351  CHECK(dt3.toString("A") == "PM");
352  CHECK(dt4.toString("A") == "PM");
353  }
354  }
355  }
356  SUBCASE("minute")
357  {
358  SUBCASE("minimum number of digits (0, 59)")
359  {
360  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("m") == "5");
361  }
362  SUBCASE("two digits (00, 59)")
363  {
364  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("mm") == "05");
365  }
366  }
367  SUBCASE("second")
368  {
369  SUBCASE("minimum number of digits (0, 59)")
370  {
371  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("s") == "6");
372  }
373  SUBCASE("two digits (00, 59)")
374  {
375  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6)).toString("ss") == "06");
376  }
377  }
378  SUBCASE("subsecond")
379  {
380  DateTime dt1(Date(1, 2, 3), Time(4, 5, 6, 0));
381  DateTime dt2(Date(1, 2, 3), Time(4, 5, 6, DateTime::Nanoseconds(123456780)));
382 
383  SUBCASE("one-digit subsecond (0, 9)")
384  {
385  CHECK(dt1.toString("f") == "0");
386  CHECK(dt2.toString("f") == "1");
387  }
388  SUBCASE("nine-digit subsecond (000000000, 999999999)")
389  {
390  CHECK(dt1.toString("fffffffff") == "000000000");
391  CHECK(dt2.toString("fffffffff") == "123456780");
392  }
393  }
394  SUBCASE("unrecognized flags are preserved")
395  {
396  const auto& text = "- GNU'S NOT UNIX!";
397  CHECK(DateTime(Date(1, 2, 3), Time(4, 5, 6, DateTime::Nanoseconds(987654321))).toString(text) == text);
398  }
399  SUBCASE("combinations of format specifiers")
400  {
401  DateTime dt(Date(2024, 2, 18), Time(21, 46, 7, DateTime::Nanoseconds(987654321)));
402  CHECK_EQ(dt.toString(" # E y-yy-yyyy M-MM-MMM-MMMM d-dd-ddd-dddd h-hh-m-mm-s-ss f-ff-fff-ffff-fffff-ffffff-fffffff-ffffffff-fffffffff a A "),
403  " + CE 2024-24-2024 2-02-Feb-February 18-18-Sun-Sunday 21-21-46-46-7-07 9-98-987-9876-98765-987654-9876543-98765432-987654321 pm PM ");
404  }
405  SUBCASE("flags with unrecognized length are preserved")
406  {
407  DateTime dt(Date(2024, 2, 18), Time(21, 46, 7, DateTime::Nanoseconds(987654321)));
408  CHECK(dt.toString(" yyy yyyyy ddddd MMMMM hhh mmm sss ffffffffff ") == " yyy yyyyy ddddd MMMMM hhh mmm sss ffffffffff ");
409  }
410  SUBCASE("default format")
411  {
412  CHECK(DateTime(Date(2024, 2, 18), Time(21, 46, 7, DateTime::Nanoseconds(987654321))).toString() == "2024-02-18 21:46:07");
413  }
414  SUBCASE("invalid date or time")
415  {
416  CHECK(DateTime().toString("d/M/yyyy, hh:mm:ss.fffffffff") == "");
417  CHECK(DateTime(Date(0, 0, 0), Time(-4, -5, 66)).toString("#y-M-d h:m:s.f") == "");
418  CHECK(DateTime(Date(1, -2, -3), Time(4, 5, 6)).toString("E yy-MMM-ddd h-mm-ss A") == "");
419  CHECK(DateTime(Date(0, 2, 3), Time(4, 5, 6, -9)).toString("@ yyyy-MM-dd hh:mm:ss.fff a") == "");
420  }
421  //
422  // additional tests.
423  //
424  // ISO format
425  CHECK(DateTime(Date(2024, 2, 18), Time(21, 46, 7, DateTime::Nanoseconds(987654321))).toString("yyyy-MM-ddThh:mm:ss") == "2024-02-18T21:46:07");
426  // Web format
427  CHECK(DateTime(Date(2024, 2, 18), Time(21, 46, 7, DateTime::Nanoseconds(987654321))).toString("ddd, dd MMM yyyy hh:mm:ss") == "Sun, 18 Feb 2024 21:46:07");
428  //
429  const auto& format = "yyyy-MM-dd hh:mm:ss.fffffffff";
430  // NTP epoch
431  CHECK(DateTime(-NtpDeltaSeconds - seconds(1)).toString(format) == "1899-12-31 23:59:59.000000000");
432  CHECK(DateTime(-NtpDeltaSeconds - nanoseconds(1)).toString(format) == "1899-12-31 23:59:59.999999999");
433  CHECK(DateTime(-NtpDeltaSeconds).toString(format) == "1900-01-01 00:00:00.000000000");
434  CHECK(DateTime(-NtpDeltaSeconds + nanoseconds(1)).toString(format) == "1900-01-01 00:00:00.000000001");
435  CHECK(DateTime(-NtpDeltaSeconds + seconds(1)).toString(format) == "1900-01-01 00:00:01.000000000");
436  // UNIX epoch
437  CHECK(DateTime(-seconds(1)).toString(format) == "1969-12-31 23:59:59.000000000");
438  CHECK(DateTime(-nanoseconds(1)).toString(format) == "1969-12-31 23:59:59.999999999");
439  CHECK(DateTime(seconds(0)).toString(format) == "1970-01-01 00:00:00.000000000");
440  CHECK(DateTime(nanoseconds(1)).toString(format) == "1970-01-01 00:00:00.000000001");
441  CHECK(DateTime(seconds(1)).toString(format) == "1970-01-01 00:00:01.000000000");
442  // UNIX rollover
443  CHECK(DateTime(UnixRolloverSeconds - seconds(1)).toString(format) == "2038-01-19 03:14:06.000000000");
444  CHECK(DateTime(UnixRolloverSeconds - nanoseconds(1)).toString(format) == "2038-01-19 03:14:06.999999999");
445  CHECK(DateTime(UnixRolloverSeconds).toString(format) == "2038-01-19 03:14:07.000000000");
446  CHECK(DateTime(UnixRolloverSeconds + nanoseconds(1)).toString(format) == "2038-01-19 03:14:07.000000001");
447  CHECK(DateTime(UnixRolloverSeconds + seconds(1)).toString(format) == "2038-01-19 03:14:08.000000000");
448  }
449 
450  TEST_CASE("parsing")
451  {
452  SUBCASE("year")
453  {
454  SUBCASE("minimum number of digits (1, 9999)")
455  {
456  SUBCASE("single digit")
457  {
458  CHECK(DateTime::fromString("1", "y") == DateTime(Date(1, 1, 1), Time(0, 0, 0)));
459  }
460  SUBCASE("multiple digits")
461  {
462  CHECK(DateTime::fromString("123", "y") == DateTime(Date(123, 1, 1), Time(0, 0, 0)));
463  }
464  SUBCASE("limited to four digits")
465  {
466  CHECK(DateTime::fromString("123456789", "y") == DateTime(Date(1234, 1, 1), Time(0, 0, 0)));
467  }
468  }
469  SUBCASE("year of era as two digits (00, 99)")
470  {
471  CHECK(DateTime::fromString("987654", "yy") == DateTime(Date(98, 1, 1), Time(0, 0, 0)));
472  }
473  SUBCASE("year as four digits (0000, 9999)")
474  {
475  CHECK(DateTime::fromString("123456789", "yyyy") == DateTime(Date(1234, 1, 1), Time(0, 0, 0)));
476  }
477  }
478  SUBCASE("era of year")
479  {
480  SUBCASE("positive(+) or negative(-) sign")
481  {
482  CHECK(DateTime::fromString("-123", "#y") == DateTime(Date(-123, 1, 1), Time(0, 0, 0)));
483  CHECK(DateTime::fromString("+123", "#y") == DateTime(Date(123, 1, 1), Time(0, 0, 0)));
484  SUBCASE("located after the year")
485  {
486  CHECK(DateTime::fromString("19-", "y#") == DateTime(Date(-19, 1, 1), Time(0, 0, 0)));
487  }
488  }
489  SUBCASE("CE / BCE")
490  {
491  CHECK(DateTime::fromString("1BCE", "yE") == DateTime(Date(-1, 1, 1), Time(0, 0, 0)));
492  CHECK(DateTime::fromString("987CE", "yE") == DateTime(Date(987, 1, 1), Time(0, 0, 0)));
493  SUBCASE("located before the year")
494  {
495  CHECK(DateTime::fromString("BCE1", "Ey") == DateTime(Date(-1, 1, 1), Time(0, 0, 0)));
496  CHECK(DateTime::fromString("CE987", "Ey") == DateTime(Date(987, 1, 1), Time(0, 0, 0)));
497  }
498  }
499  }
500  SUBCASE("unrecognized characters are skipped according to the format")
501  {
502  CHECK(DateTime::fromString("(1-1999 2-08) [!]{@$%^&*}", "(1-yyyy 2-MM) [!]{@$%^&*}") == DateTime(Date(1999, 8, 1), Time(0, 0, 0)));
503  }
504  SUBCASE("month")
505  {
506  SUBCASE("minimum number of digits (1, 12)")
507  {
508  CHECK(DateTime::fromString("12345", "yyyyM") == DateTime(Date(1234, 5, 1), Time(0, 0, 0)));
509  SUBCASE("greedy when there is one more digit")
510  {
511  CHECK(DateTime::fromString("19991123", "yyyyM") == DateTime(Date(1999, 11, 1), Time(0, 0, 0)));
512  }
513  }
514  SUBCASE("two digits (01, 12)")
515  {
516  CHECK(DateTime::fromString("990123", "yyMM") == DateTime(Date(99, 1, 1), Time(0, 0, 0)));
517  }
518  SUBCASE("short name (e.g. Feb)")
519  {
520  CHECK(internal::getShortMonthNumber("abc") == 13);
521  CHECK(DateTime::fromString("1999 Sep", "yyyy MMM") == DateTime(Date(1999, 9, 1), Time(0, 0, 0)));
522  SUBCASE("case insensitive")
523  {
524  CHECK(DateTime::fromString("1999 sEp", "yyyy MMM") == DateTime(Date(1999, 9, 1), Time(0, 0, 0)));
525  }
526  SUBCASE("the second pattern takes effect in case of multiple occurences")
527  {
528  CHECK(DateTime::fromString("Oct, dec - 1999", "MMM, MMM - y") == DateTime(Date(1999, 12, 1), Time(0, 0, 0)));
529  }
530  }
531  SUBCASE("long name (e.g. February)")
532  {
533  CHECK(internal::getLongMonthNumber("abcdef") == 13);
534  CHECK(DateTime::fromString("1999 December", "yyyy MMMM") == DateTime(Date(1999, 12, 1), Time(0, 0, 0)));
535  SUBCASE("case insensitive")
536  {
537  CHECK(DateTime::fromString("1999 decEmbeR", "yyyy MMMM") == DateTime(Date(1999, 12, 1), Time(0, 0, 0)));
538  }
539  SUBCASE("followed by an interfering expression")
540  {
541  CHECK(DateTime::fromString("deCEmber BCE 9", "MMMM E y") == DateTime(Date(-9, 12, 1), Time(0, 0, 0)));
542  }
543  }
544  }
545  SUBCASE("day")
546  {
547  SUBCASE("day of month")
548  {
549  CHECK(DateTime::fromString("1999-9-9", "yyyy-M-d") == DateTime(Date(1999, 9, 9), Time(0, 0, 0)));
550  }
551  SUBCASE("day of week as short name (e.g. Fri)")
552  {
553  CHECK(internal::search(internal::getShortWeekdayNameArray(), "mon") == 0);
554  CHECK(internal::search(internal::getShortWeekdayNameArray(), "abc") == 7);
555  CHECK(DateTime::fromString("Sun, 2024-2-25", "ddd, yyyy-M-d") == DateTime(Date(2024, 2, 25), Time(0, 0, 0)));
556  }
557  SUBCASE("day of week as long name (e.g. Friday)")
558  {
559  CHECK(internal::search(internal::getLongWeekdayNameArray(), "MONDAY") == 0);
560  CHECK(internal::search(internal::getLongWeekdayNameArray(), "a") == 7);
561  CHECK(DateTime::fromString("Sunday, 2024-2-25", "dddd, yyyy-M-d") == DateTime(Date(2024, 2, 25), Time(0, 0, 0)));
562  }
563  }
564  SUBCASE("hour")
565  {
566  SUBCASE("24-hour clock")
567  {
568  CHECK(DateTime::fromString("2024-2-25 1", "yyyy-M-d h") == DateTime(Date(2024, 2, 25), Time(1, 0, 0)));
569  }
570  SUBCASE("12-hour clock")
571  {
572  CHECK(DateTime::fromString("1 12:00:00 am", "y H:m:s a") == DateTime(Date(1, 1, 1), Time(0, 0, 0)));
573  CHECK(DateTime::fromString("1 12:59:59 AM", "y H:m:s A") == DateTime(Date(1, 1, 1), Time(0, 59, 59)));
574  CHECK(DateTime::fromString("1 11:00:00 pm", "y HH:mm:ss a") == DateTime(Date(1, 1, 1), Time(23, 0, 0)));
575  CHECK(DateTime::fromString("1 11:59:59 PM", "y HH:mm:ss A") == DateTime(Date(1, 1, 1), Time(23, 59, 59)));
576  }
577  }
578  SUBCASE("minute")
579  {
580  CHECK(DateTime::fromString("2024-2-25 1", "yyyy-M-d m") == DateTime(Date(2024, 2, 25), Time(0, 1, 0)));
581  }
582  SUBCASE("second")
583  {
584  CHECK(DateTime::fromString("2024-2-25 1", "yyyy-M-d s") == DateTime(Date(2024, 2, 25), Time(0, 0, 1)));
585  }
586  SUBCASE("subsecond")
587  {
588  SUBCASE("single digit (0, 9)")
589  {
590  CHECK(DateTime::fromString("2024-2-25 1.5", "yyyy-M-d s.f") == DateTime(Date(2024, 2, 25), Time(0, 0, 1, 500)));
591  }
592  SUBCASE("multiple digits (000000000, 999999999)")
593  {
594  CHECK(DateTime::fromString("1 1.123456780", "y s.fffffffff") == DateTime(Date(1, 1, 1), Time(0, 0, 1, Time::Nanoseconds(123456780))));
595  }
596  }
597  SUBCASE("flags with unrecognized length are ignored")
598  {
599  CHECK(DateTime::fromString("1999-08-07 01:02:03", "##yyyy-MM-dd hh:mm:ss").isValid() == false);
600  CHECK(DateTime::fromString("1999-08-07 01:02:03", "yyyyy-MM-dd hh:mm:ss").isValid() == false);
601  CHECK(DateTime::fromString("1999-08-07 01:02:03", "yyyy-MMMMMM-dd hh:mm:ss").isValid() == false);
602  CHECK(DateTime::fromString("1999-08-07 01:02:03", "yyyy-MM-ddddd hh:mm:ss").isValid() == false);
603  CHECK(DateTime::fromString("1999-08-07 01:02:03", "yyyy-MM-dd hhh:mm:ss").isValid() == false);
604  CHECK(DateTime::fromString("1999-08-07 01:02:03", "yyyy-MM-dd hh:mmm:ss").isValid() == false);
605  CHECK(DateTime::fromString("1999-08-07 01:02:03", "yyyy-MM-dd hh:mm:sss").isValid() == false);
606  CHECK(DateTime::fromString("1999-08-07 01:02:03.123456789", "yyyy-MM-dd hh:mm:ss.ffffffffff").isValid() == false);
607  CHECK(DateTime::fromString("1999-08-07 01:02:03.0 CE", "yyyy-MM-dd hh:mm:ss.f EE").isValid() == false);
608  CHECK(DateTime::fromString("1999-08-07 01:02:03.0 am", "yyyy-MM-dd hh:mm:ss.f aa").isValid() == false);
609  CHECK(DateTime::fromString("1999-08-07 01:02:03.0 AM", "yyyy-MM-dd hh:mm:ss.f AA").isValid() == false);
610  }
611  SUBCASE("missing data yields invalid objects")
612  {
613  CHECK(DateTime::fromString("abcde", "y").isValid() == false);
614  CHECK(DateTime::fromString("1", "y M").isValid() == false);
615  CHECK(DateTime::fromString("1", "y MMM").isValid() == false);
616  CHECK(DateTime::fromString("1 abc", "y MMM").isValid() == false);
617  CHECK(DateTime::fromString("1", "y d").isValid() == false);
618  CHECK(DateTime::fromString("1", "y ddd").isValid() == false);
619  CHECK(DateTime::fromString("1 abc", "y ddd").isValid() == false);
620  CHECK(DateTime::fromString("1", "y h").isValid() == false);
621  CHECK(DateTime::fromString("1", "y H").isValid() == false);
622  CHECK(DateTime::fromString("1", "y m").isValid() == false);
623  CHECK(DateTime::fromString("1", "y s").isValid() == false);
624  CHECK(DateTime::fromString("1", "y f").isValid() == false);
625  CHECK(DateTime::fromString("1", "y a").isValid() == false);
626  CHECK(DateTime::fromString("1", "y A").isValid() == false);
627  CHECK(DateTime::fromString("1", "y E").isValid() == false);
628  CHECK(DateTime::fromString("1", "y #").isValid() == false);
629  }
630  SUBCASE("unrecognized identical characters are skipped, so whitespaces may be used as placeholders")
631  {
632  CHECK(DateTime::fromString("y:1999 / M:08 / d:07 6", " :yyyy / :MM / :dd h") == DateTime(Date(1999, 8, 7), Time(6, 0, 0)));
633  }
634  SUBCASE("default format")
635  {
636  CHECK(DateTime::fromString("2024-02-02 01:33:06") == DateTime(Date(2024, 2, 2), Time(1, 33, 6)));
637  }
638  SUBCASE("empty format") // doc
639  {
640  CHECK(DateTime::fromString("2024-02-02 01:33:06", "").isValid() == false);
641  }
642  }
643 
644  TEST_CASE("Julian day conversion")
645  {
646  SUBCASE("to")
647  {
648  CHECK(DateTime(Date(-4714, 11, 24), Time(12, 0, 0)).toJulianDay() == doctest::Approx(0).epsilon(0.0000000001));
649  CHECK(DateTime(Date(-4714, 11, 26), Time(0, 0, 0)).toJulianDay() == doctest::Approx(1.5).epsilon(0.0000000001));
650  CHECK(DateTime(Date(2017, 12, 31), Time(00, 9, 35)).toJulianDay() == doctest::Approx(2458118.506655093).epsilon(0.0000000001));
651  }
652  SUBCASE("from")
653  {
654  CHECK(DateTime::fromJulianDay(0) == DateTime(Date(-4714, 11, 24), Time(12, 0, 0)));
655  CHECK(DateTime::fromJulianDay(1.5) == DateTime(Date(-4714, 11, 26), Time(0, 0, 0)));
656  CHECK(DateTime::fromJulianDay(2458118.506655093) == DateTime(Date(2017, 12, 31), Time(0, 9, 35)));
657  }
658  }
659 
660  TEST_CASE("diffing")
661  {
662  CHECK(DateTime::nanosecondsBetween(DateTime(Date(2017, 1, 15), Time(12, 45, 36, DateTime::Nanoseconds(123001013))), DateTime(Date(2017, 1, 15), Time(12, 45, 37))) == 876998987);
663  CHECK(DateTime::microsecondsBetween(DateTime(Date(2017, 1, 15), Time(12, 45, 36, DateTime::Microseconds(123001))), DateTime(Date(2017, 1, 15), Time(12, 45, 37))) == 876999);
664  CHECK(DateTime::millisecondsBetween(DateTime(Date(2017, 1, 15), Time(12, 45, 36, 123)), DateTime(Date(2017, 1, 15), Time(12, 45, 37))) == 877);
665  CHECK(DateTime::secondsBetween(DateTime(Date(2002, 1, 1), Time(15, 45, 36)), DateTime(Date(2002, 1, 2), Time(15, 2, 37))) == 83821);
666  CHECK(DateTime::minutesBetween(DateTime(Date(2000, 1, 2), Time(23, 45, 36)), DateTime(Date(2000, 1, 2), Time(12, 2, 36))) == 703);
667  CHECK(DateTime::hoursBetween(DateTime(Date(1998, 1, 1), Time(23, 45, 36)), DateTime(Date(1998, 1, 2), Time(12, 2, 36))) == 12);
668  CHECK(DateTime::daysBetween(DateTime(Date(1970, 1, 1), Time(23, 2, 36)), DateTime(Date(1971, 1, 1), Time(23, 2, 36))) == 365);
669  }
670 
671  TEST_CASE("serialization & deserialization")
672  {
673  DateTime dt;
674  std::stringstream ss;
675  ss << DateTime(Date(2014, 11, 9), Time(2, 44, 21, 987));
676  ss >> dt;
677  CHECK(dt == DateTime(Date(2014, 11, 9), Time(2, 44, 21, 987)));
678  }
679 } // TEST_SUITE
DateTime is an immutable class representing a datetime without a time zone in the ISO-8601 calendar s...
Definition: datetime.hpp:47
static long long secondsBetween(const DateTime &from, const DateTime &to)
Returns the number of seconds between from and to.
Definition: datetime.hpp:775
static long hoursBetween(const DateTime &from, const DateTime &to)
Returns the number of hours between from and to.
Definition: datetime.hpp:787
int year() const
Returns the year as a number. There is no year 0. Negative numbers indicate years before 1 CE; that i...
Definition: datetime.hpp:280
long microsecond() const
Returns the microsecond of second (0, 999999).
Definition: datetime.hpp:238
static DateTime fromJulianDay(double julianDay)
Returns a DateTime object corresponding to the Julian day julianDay. See toJulianDay() for informatio...
Definition: datetime.hpp:742
long nanosecond() const
Returns the nanosecond of second (0, 999999999).
Definition: datetime.hpp:232
int day() const
Returns the day of month (1, 31).
Definition: datetime.hpp:268
std::string toString(const std::string &format="yyyy-MM-dd hh:mm:ss") const
Returns the datetime as a string formatted according to the format string format.
Definition: datetime.hpp:655
static DateTime epoch()
Returns a DateTime object set to the epoch "1970-1-1T00:00:00".
Definition: datetime.hpp:680
static DateTime fromString(const std::string &datetime, const std::string &format="yyyy-MM-dd hh:mm:ss")
Returns a DateTime object from the string datetime formatted according to the format string format.
Definition: datetime.hpp:689
static long long millisecondsBetween(const DateTime &from, const DateTime &to)
Returns the number of milliseconds between from and to.
Definition: datetime.hpp:769
int minute() const
Returns the minute of hour (0, 59).
Definition: datetime.hpp:256
Time::Nanoseconds Nanoseconds
Nanosecond duration.
Definition: datetime.hpp:56
Time::Hours Hours
Hour duration.
Definition: datetime.hpp:61
Time::Minutes Minutes
Minute duration.
Definition: datetime.hpp:60
static long long microsecondsBetween(const DateTime &from, const DateTime &to)
Returns the number of microseconds between from and to.
Definition: datetime.hpp:763
int millisecond() const
Returns the millisecond of second (0, 999).
Definition: datetime.hpp:244
static long minutesBetween(const DateTime &from, const DateTime &to)
Returns the number of minutes between from and to.
Definition: datetime.hpp:781
Date::Days Days
Day duration.
Definition: datetime.hpp:62
static DateTime current()
Returns a DateTime object set to the current datetime obtained from the system clock.
Definition: datetime.hpp:674
static long long nanosecondsBetween(const DateTime &from, const DateTime &to)
Returns the number of nanoseconds between from and to.
Definition: datetime.hpp:757
int second() const
Returns the second of minute (0, 59).
Definition: datetime.hpp:250
static long daysBetween(const DateTime &from, const DateTime &to)
Returns the number of days between from and to.
Definition: datetime.hpp:793
Time::Microseconds Microseconds
Microsecond duration.
Definition: datetime.hpp:57
int hour() const
Returns the hour of day (0, 23).
Definition: datetime.hpp:262
int month() const
Returns the month of year (1, 12), which corresponds to the enumeration Month.
Definition: datetime.hpp:274
Date is an immutable class representing a date without a time zone in the ISO-8601 calendar system,...
Definition: date.hpp:84
Time is an immutable time class representing a time without a time zone in the ISO-8601 calendar syst...
Definition: time.hpp:46
std::chrono::milliseconds Milliseconds
Millisecond duration.
Definition: time.hpp:58
std::chrono::nanoseconds Nanoseconds
Nanosecond duration.
Definition: time.hpp:56
std::chrono::hours Hours
Hour duration.
Definition: time.hpp:61