1/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2
3/*
4 Copyright (C) 2004, 2009 Ferdinando Ametrano
5 Copyright (C) 2006 Katiuscia Manzoni
6 Copyright (C) 2003 RiskMap srl
7 Copyright (C) 2015 Maddalena Zanzi
8 Copyright (c) 2015 Klaus Spanderen
9 Copyright (C) 2020 Leonardo Arcari
10 Copyright (C) 2020 Kline s.r.l.
11
12 This file is part of QuantLib, a free-software/open-source library
13 for financial quantitative analysts and developers - http://quantlib.org/
14
15 QuantLib is free software: you can redistribute it and/or modify it
16 under the terms of the QuantLib license. You should have received a
17 copy of the license along with this program; if not, please email
18 <quantlib-dev@lists.sf.net>. The license is also available online at
19 <http://quantlib.org/license.shtml>.
20
21 This program is distributed in the hope that it will be useful, but WITHOUT
22 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
23 FOR A PARTICULAR PURPOSE. See the license for more details.
24*/
25
26#include "dates.hpp"
27#include "utilities.hpp"
28#include <ql/time/date.hpp>
29#include <ql/time/timeunit.hpp>
30#include <ql/time/imm.hpp>
31#include <ql/time/ecb.hpp>
32#include <ql/time/asx.hpp>
33#include <ql/utilities/dataparsers.hpp>
34
35#include <sstream>
36#include <unordered_set>
37
38using namespace QuantLib;
39using namespace boost::unit_test_framework;
40
41void DateTest::ecbDates() {
42 BOOST_TEST_MESSAGE("Testing ECB dates...");
43
44 std::set<Date> knownDates = ECB::knownDates();
45 if (knownDates.empty())
46 BOOST_FAIL("empty EBC date vector");
47
48 Size n = ECB::nextDates(d: Date::minDate()).size();
49 if (n != knownDates.size())
50 BOOST_FAIL("nextDates(minDate) returns " << n <<
51 " instead of " << knownDates.size() << " dates");
52
53 std::set<Date>::const_iterator i;
54 Date previousEcbDate = Date::minDate(),
55 currentEcbDate, ecbDateMinusOne;
56 for (i=knownDates.begin(); i!=knownDates.end(); ++i) {
57
58 currentEcbDate = *i;
59 if (!ECB::isECBdate(d: currentEcbDate))
60 BOOST_FAIL(currentEcbDate << " fails isECBdate check");
61
62 ecbDateMinusOne = currentEcbDate-1;
63 if (ECB::isECBdate(d: ecbDateMinusOne))
64 BOOST_FAIL(ecbDateMinusOne << " fails isECBdate check");
65
66 if (ECB::nextDate(d: ecbDateMinusOne)!=currentEcbDate)
67 BOOST_FAIL("next EBC date following " << ecbDateMinusOne <<
68 " must be " << currentEcbDate);
69
70 if (ECB::nextDate(d: previousEcbDate)!=currentEcbDate)
71 BOOST_FAIL("next EBC date following " << previousEcbDate <<
72 " must be " << currentEcbDate);
73
74 previousEcbDate = currentEcbDate;
75 }
76
77 Date knownDate = *knownDates.begin();
78 ECB::removeDate(d: knownDate);
79 if (ECB::isECBdate(d: knownDate))
80 BOOST_FAIL("unable to remove an EBC date");
81 ECB::addDate(d: knownDate);
82 if (!ECB::isECBdate(d: knownDate))
83 BOOST_FAIL("unable to add an EBC date");
84}
85
86void DateTest::immDates() {
87 BOOST_TEST_MESSAGE("Testing IMM dates...");
88
89 const std::string IMMcodes[] = {
90 "F0", "G0", "H0", "J0", "K0", "M0", "N0", "Q0", "U0", "V0", "X0", "Z0",
91 "F1", "G1", "H1", "J1", "K1", "M1", "N1", "Q1", "U1", "V1", "X1", "Z1",
92 "F2", "G2", "H2", "J2", "K2", "M2", "N2", "Q2", "U2", "V2", "X2", "Z2",
93 "F3", "G3", "H3", "J3", "K3", "M3", "N3", "Q3", "U3", "V3", "X3", "Z3",
94 "F4", "G4", "H4", "J4", "K4", "M4", "N4", "Q4", "U4", "V4", "X4", "Z4",
95 "F5", "G5", "H5", "J5", "K5", "M5", "N5", "Q5", "U5", "V5", "X5", "Z5",
96 "F6", "G6", "H6", "J6", "K6", "M6", "N6", "Q6", "U6", "V6", "X6", "Z6",
97 "F7", "G7", "H7", "J7", "K7", "M7", "N7", "Q7", "U7", "V7", "X7", "Z7",
98 "F8", "G8", "H8", "J8", "K8", "M8", "N8", "Q8", "U8", "V8", "X8", "Z8",
99 "F9", "G9", "H9", "J9", "K9", "M9", "N9", "Q9", "U9", "V9", "X9", "Z9"
100 };
101
102 Date counter = { 1, January, 2000 };
103 Date last = { 1, January, 2040 };
104 Date imm;
105
106 while (counter<=last) {
107 imm = IMM::nextDate(d: counter, mainCycle: false);
108
109 // check that imm is greater than counter
110 if (imm<=counter)
111 BOOST_FAIL(imm.weekday() << " " << imm
112 << " is not greater than "
113 << counter.weekday() << " " << counter);
114
115 // check that imm is an IMM date
116 if (!IMM::isIMMdate(d: imm, mainCycle: false))
117 BOOST_FAIL(imm.weekday() << " " << imm
118 << " is not an IMM date (calculated from "
119 << counter.weekday() << " " << counter << ")");
120
121 // check that imm is <= to the next IMM date in the main cycle
122 if (imm>IMM::nextDate(d: counter, mainCycle: true))
123 BOOST_FAIL(imm.weekday() << " " << imm
124 << " is not less than or equal to the next future in the main cycle "
125 << IMM::nextDate(counter, true));
126
127 // check that for every date IMMdate is the inverse of IMMcode
128 if (IMM::date(immCode: IMM::code(immDate: imm), referenceDate: counter) != imm)
129 BOOST_FAIL(IMM::code(imm)
130 << " at calendar day " << counter
131 << " is not the IMM code matching " << imm);
132
133 // check that for every date the 120 IMM codes refer to future dates
134 for (int i=0; i<40; ++i) {
135 if (IMM::date(immCode: IMMcodes[i], referenceDate: counter)<counter)
136 BOOST_FAIL(IMM::date(IMMcodes[i], counter)
137 << " is wrong for " << IMMcodes[i]
138 << " at reference date " << counter);
139 }
140
141 counter = counter + 1;
142 }
143}
144
145void DateTest::asxDates() {
146 BOOST_TEST_MESSAGE("Testing ASX dates...");
147
148 const std::string ASXcodes[] = {
149 "F0", "G0", "H0", "J0", "K0", "M0", "N0", "Q0", "U0", "V0", "X0", "Z0",
150 "F1", "G1", "H1", "J1", "K1", "M1", "N1", "Q1", "U1", "V1", "X1", "Z1",
151 "F2", "G2", "H2", "J2", "K2", "M2", "N2", "Q2", "U2", "V2", "X2", "Z2",
152 "F3", "G3", "H3", "J3", "K3", "M3", "N3", "Q3", "U3", "V3", "X3", "Z3",
153 "F4", "G4", "H4", "J4", "K4", "M4", "N4", "Q4", "U4", "V4", "X4", "Z4",
154 "F5", "G5", "H5", "J5", "K5", "M5", "N5", "Q5", "U5", "V5", "X5", "Z5",
155 "F6", "G6", "H6", "J6", "K6", "M6", "N6", "Q6", "U6", "V6", "X6", "Z6",
156 "F7", "G7", "H7", "J7", "K7", "M7", "N7", "Q7", "U7", "V7", "X7", "Z7",
157 "F8", "G8", "H8", "J8", "K8", "M8", "N8", "Q8", "U8", "V8", "X8", "Z8",
158 "F9", "G9", "H9", "J9", "K9", "M9", "N9", "Q9", "U9", "V9", "X9", "Z9"
159 };
160
161 Date counter = { 1, January, 2000 };
162 Date last = { 1, January, 2040 };
163 Date asx;
164
165 while (counter <= last) {
166 asx = ASX::nextDate(d: counter, mainCycle: false);
167
168 // check that asx is greater than counter
169 if (asx <= counter)
170 BOOST_FAIL(asx.weekday() << " " << asx
171 << " is not greater than "
172 << counter.weekday() << " " << counter);
173
174 // check that asx is an ASX date
175 if (!ASX::isASXdate(d: asx, mainCycle: false))
176 BOOST_FAIL(asx.weekday() << " " << asx
177 << " is not an ASX date (calculated from "
178 << counter.weekday() << " " << counter << ")");
179
180 // check that asx is <= to the next ASX date in the main cycle
181 if (asx>ASX::nextDate(d: counter, mainCycle: true))
182 BOOST_FAIL(asx.weekday() << " " << asx
183 << " is not less than or equal to the next future in the main cycle "
184 << ASX::nextDate(counter, true));
185
186 // check that for every date ASXdate is the inverse of ASXcode
187 if (ASX::date(asxCode: ASX::code(asxDate: asx), referenceDate: counter) != asx)
188 BOOST_FAIL(ASX::code(asx)
189 << " at calendar day " << counter
190 << " is not the ASX code matching " << asx);
191
192 // check that for every date the 120 ASX codes refer to future dates
193 for (const auto& ASXcode : ASXcodes) {
194 if (ASX::date(asxCode: ASXcode, referenceDate: counter) < counter)
195 BOOST_FAIL(ASX::date(ASXcode, counter) << " is wrong for " << ASXcode
196 << " at reference date " << counter);
197 }
198
199 counter = counter + 1;
200 }
201}
202
203void DateTest::testConsistency() {
204
205 BOOST_TEST_MESSAGE("Testing dates...");
206
207 Date::serial_type minDate = Date::minDate().serialNumber()+1,
208 maxDate = Date::maxDate().serialNumber();
209
210 Date::serial_type dyold = Date(minDate-1).dayOfYear(),
211 dold = Date(minDate-1).dayOfMonth(),
212 mold = Date(minDate-1).month(),
213 yold = Date(minDate-1).year(),
214 wdold = Date(minDate-1).weekday();
215
216 for (Date::serial_type i=minDate; i<=maxDate; i++) {
217 Date t(i);
218 Date::serial_type serial = t.serialNumber();
219
220 // check serial number consistency
221 if (serial != i)
222 BOOST_FAIL("inconsistent serial number:\n"
223 << " original: " << i << "\n"
224 << " date: " << t << "\n"
225 << " serial number: " << serial);
226
227 Integer dy = t.dayOfYear(),
228 d = t.dayOfMonth(),
229 m = t.month(),
230 y = t.year(),
231 wd = t.weekday();
232
233 // check if skipping any date
234 if (!((dy == dyold+1) ||
235 (dy == 1 && dyold == 365 && !Date::isLeap(y: yold)) ||
236 (dy == 1 && dyold == 366 && Date::isLeap(y: yold))))
237 BOOST_FAIL("wrong day of year increment: \n"
238 << " date: " << t << "\n"
239 << " day of year: " << dy << "\n"
240 << " previous: " << dyold);
241 dyold = dy;
242
243 if (!((d == dold+1 && m == mold && y == yold) ||
244 (d == 1 && m == mold+1 && y == yold) ||
245 (d == 1 && m == 1 && y == yold+1)))
246 BOOST_FAIL("wrong day,month,year increment: \n"
247 << " date: " << t << "\n"
248 << " day,month,year: "
249 << d << "," << Integer(m) << "," << y << "\n"
250 << " previous: "
251 << dold<< "," << Integer(mold) << "," << yold);
252 dold = d; mold = m; yold = y;
253
254 // check month definition
255 if (m < 1 || m > 12)
256 BOOST_FAIL("invalid month: \n"
257 << " date: " << t << "\n"
258 << " month: " << Integer(m));
259
260 // check day definition
261 if (d < 1)
262 BOOST_FAIL("invalid day of month: \n"
263 << " date: " << t << "\n"
264 << " day: " << d);
265 if (!((m == 1 && d <= 31) ||
266 (m == 2 && d <= 28) ||
267 (m == 2 && d == 29 && Date::isLeap(y)) ||
268 (m == 3 && d <= 31) ||
269 (m == 4 && d <= 30) ||
270 (m == 5 && d <= 31) ||
271 (m == 6 && d <= 30) ||
272 (m == 7 && d <= 31) ||
273 (m == 8 && d <= 31) ||
274 (m == 9 && d <= 30) ||
275 (m == 10 && d <= 31) ||
276 (m == 11 && d <= 30) ||
277 (m == 12 && d <= 31)))
278 BOOST_FAIL("invalid day of month: \n"
279 << " date: " << t << "\n"
280 << " day: " << d);
281
282 // check weekday definition
283 if (!((Integer(wd) == Integer(wdold+1)) ||
284 (Integer(wd) == 1 && Integer(wdold) == 7)))
285 BOOST_FAIL("invalid weekday: \n"
286 << " date: " << t << "\n"
287 << " weekday: " << Integer(wd) << "\n"
288 << " previous: " << Integer(wdold));
289 wdold = wd;
290
291 // create the same date with a different constructor
292 Date s(d,Month(m),y);
293 // check serial number consistency
294 serial = s.serialNumber();
295 if (serial != i)
296 BOOST_FAIL("inconsistent serial number:\n"
297 << " date: " << t << "\n"
298 << " serial number: " << i << "\n"
299 << " cloned date: " << s << "\n"
300 << " serial number: " << serial);
301 }
302
303}
304
305void DateTest::isoDates() {
306 BOOST_TEST_MESSAGE("Testing ISO dates...");
307 std::string input_date("2006-01-15");
308 Date d = DateParser::parseISO(str: input_date);
309 if (d.dayOfMonth() != 15 ||
310 d.month() != January ||
311 d.year() != 2006) {
312 BOOST_FAIL("Iso date failed\n"
313 << " input date: " << input_date << "\n"
314 << " day of month: " << d.dayOfMonth() << "\n"
315 << " month: " << d.month() << "\n"
316 << " year: " << d.year());
317 }
318}
319
320void DateTest::parseDates() {
321 BOOST_TEST_MESSAGE("Testing parsing of dates...");
322
323 std::string input_date("2006-01-15");
324 Date d = DateParser::parseFormatted(str: input_date, fmt: "%Y-%m-%d");
325 if (d != Date(15, January, 2006)) {
326 BOOST_FAIL("date parsing failed\n"
327 << " input date: " << input_date << "\n"
328 << " parsed date: " << d);
329 }
330
331 input_date = "12/02/2012";
332 d = DateParser::parseFormatted(str: input_date, fmt: "%m/%d/%Y");
333 if (d != Date(2, December, 2012)) {
334 BOOST_FAIL("date parsing failed\n"
335 << " input date: " << input_date << "\n"
336 << " parsed date: " << d);
337 }
338 d = DateParser::parseFormatted(str: input_date, fmt: "%d/%m/%Y");
339 if (d != Date(12, February, 2012)) {
340 BOOST_FAIL("date parsing failed\n"
341 << " input date: " << input_date << "\n"
342 << " parsed date: " << d);
343 }
344
345 input_date = "20011002";
346 d = DateParser::parseFormatted(str: input_date, fmt: "%Y%m%d");
347 if (d != Date(2, October, 2001)) {
348 BOOST_FAIL("date parsing failed\n"
349 << " input date: " << input_date << "\n"
350 << " parsed date: " << d);
351 }
352}
353
354void DateTest::intraday() {
355#ifdef QL_HIGH_RESOLUTION_DATE
356
357 BOOST_TEST_MESSAGE("Testing intraday information of dates...");
358
359 const Date d1 = Date(12, February, 2015, 10, 45, 12, 1234, 76253);
360
361 BOOST_CHECK_MESSAGE(d1.year() == 2015, "failed to reproduce year");
362 BOOST_CHECK_MESSAGE(d1.month() == February, "failed to reproduce month");
363 BOOST_CHECK_MESSAGE(d1.dayOfMonth() == 12, "failed to reproduce day");
364 BOOST_CHECK_MESSAGE(d1.hours() == 10, "failed to reproduce hour of day");
365 BOOST_CHECK_MESSAGE(d1.minutes() == 45,
366 "failed to reproduce minute of hour");
367 BOOST_CHECK_MESSAGE(d1.seconds() == 13,
368 "failed to reproduce second of minute");
369
370 if (Date::ticksPerSecond() == 1000)
371 BOOST_CHECK_MESSAGE(d1.fractionOfSecond() == 0.234,
372 "failed to reproduce fraction of second");
373 else if (Date::ticksPerSecond() >= 1000000)
374 BOOST_CHECK_MESSAGE(d1.fractionOfSecond() == (234000 + 76253)/1000000.0,
375 "failed to reproduce fraction of second");
376
377 if (Date::ticksPerSecond() >= 1000)
378 BOOST_CHECK_MESSAGE(d1.milliseconds() == 234 + 76,
379 "failed to reproduce number of milliseconds");
380
381 if (Date::ticksPerSecond() >= 1000000)
382 BOOST_CHECK_MESSAGE(d1.microseconds() == 253,
383 "failed to reproduce number of microseconds");
384
385 const Date d2 = Date(28, February, 2015, 50, 165, 476, 1234, 253);
386 BOOST_CHECK_MESSAGE(d2.year() == 2015, "failed to reproduce year");
387 BOOST_CHECK_MESSAGE(d2.month() == March, "failed to reproduce month");
388 BOOST_CHECK_MESSAGE(d2.dayOfMonth() == 2, "failed to reproduce day");
389 BOOST_CHECK_MESSAGE(d2.hours() == 4, "failed to reproduce hour of day");
390 BOOST_CHECK_MESSAGE(d2.minutes() == 52,
391 "failed to reproduce minute of hour");
392 BOOST_CHECK_MESSAGE(d2.seconds() == 57,
393 "failed to reproduce second of minute");
394
395 if (Date::ticksPerSecond() >= 1000)
396 BOOST_CHECK_MESSAGE(d2.milliseconds() == 234,
397 "failed to reproduce number of milliseconds");
398 if (Date::ticksPerSecond() >= 1000000)
399 BOOST_CHECK_MESSAGE(d2.microseconds() == 253,
400 "failed to reproduce number of microseconds");
401
402 std::ostringstream s;
403 s << io::iso_datetime(Date(7, February, 2015, 1, 4, 2, 3, 4));
404
405 BOOST_CHECK_MESSAGE(s.str() == std::string("2015-02-07T01:04:02,003004"),
406 "datetime to string failed to reproduce expected result");
407
408 const Date d3 = Date(10, April, 2023, 11, 43, 13, 234, 253);
409
410 BOOST_CHECK_MESSAGE(d3 + Period(23, Hours) ==
411 Date(11, April, 2023, 10, 43, 13, 234, 253), "failed to add hours");
412
413 BOOST_CHECK_MESSAGE(d3 + Period(2, Minutes) ==
414 Date(10, April, 2023, 11, 45, 13, 234, 253), "failed to add minutes");
415
416 BOOST_CHECK_MESSAGE(d3 + Period(-2, Seconds) ==
417 Date(10, April, 2023, 11, 43, 11, 234, 253), "failed to add seconds");
418
419 BOOST_CHECK_MESSAGE(d3 + Period(-20, Milliseconds) ==
420 Date(10, April, 2023, 11, 43, 13, 214, 253), "failed to add milliseconds");
421
422 BOOST_CHECK_MESSAGE(d3 + Period(20, Microseconds) ==
423 Date(10, April, 2023, 11, 43, 13, 234, 273), "failed to add microseconds");
424
425#endif
426}
427
428void DateTest::canHash() {
429 BOOST_TEST_MESSAGE("Testing hashing of dates...");
430
431 Date start_date = Date(1, Jan, 2020);
432 int nb_tests = 500;
433
434 std::hash<Date> hasher;
435
436 // Check hash values
437 for (int i = 0; i < nb_tests; ++i) {
438 for (int j = 0; j < nb_tests; ++j) {
439 Date lhs = start_date + i;
440 Date rhs = start_date + j;
441
442 if (lhs == rhs && hasher(lhs) != hasher(rhs)) {
443 BOOST_FAIL("Equal dates are expected to have same hash value\n"
444 << "rhs = " << lhs << '\n'
445 << "lhs = " << rhs << '\n'
446 << "hash(lhs) = " << hasher(lhs) << '\n'
447 << "hash(rhs) = " << hasher(rhs) << '\n');
448 }
449
450 if (lhs != rhs && hasher(lhs) == hasher(rhs)) {
451 BOOST_FAIL("Different dates are expected to have different hash value\n"
452 << "rhs = " << lhs << '\n'
453 << "lhs = " << rhs << '\n'
454 << "hash(lhs) = " << hasher(lhs) << '\n'
455 << "hash(rhs) = " << hasher(rhs) << '\n');
456 }
457 }
458 }
459
460 // Check if Date can be used as unordered_set key
461 std::unordered_set<Date> set;
462 set.insert(x: start_date);
463
464 if (set.count(x: start_date) == 0) {
465 BOOST_FAIL("Expected to find date " << start_date << " in unordered_set\n");
466 }
467}
468
469
470test_suite* DateTest::suite(SpeedLevel speed) {
471 auto* suite = BOOST_TEST_SUITE("Date tests");
472
473 suite->add(QUANTLIB_TEST_CASE(&DateTest::testConsistency));
474 suite->add(QUANTLIB_TEST_CASE(&DateTest::ecbDates));
475 suite->add(QUANTLIB_TEST_CASE(&DateTest::immDates));
476 suite->add(QUANTLIB_TEST_CASE(&DateTest::asxDates));
477 suite->add(QUANTLIB_TEST_CASE(&DateTest::isoDates));
478 #ifndef QL_PATCH_SOLARIS
479 suite->add(QUANTLIB_TEST_CASE(&DateTest::parseDates));
480 #endif
481 #ifdef QL_HIGH_RESOLUTION_DATE
482 suite->add(QUANTLIB_TEST_CASE(&DateTest::intraday));
483 #endif
484 suite->add(QUANTLIB_TEST_CASE(&DateTest::canHash));
485
486 return suite;
487}
488

source code of quantlib/test-suite/dates.cpp