diff --git a/ql/time/ecb.cpp b/ql/time/ecb.cpp index f37d38945d6..fba486d6142 100644 --- a/ql/time/ecb.cpp +++ b/ql/time/ecb.cpp @@ -21,74 +21,160 @@ #include #include #include -#include +#include +#include +#include #include #include +#include +#include -using boost::algorithm::to_upper_copy; using std::string; namespace QuantLib { - namespace detail { + namespace { + // case-insensitive comparison. answers: lhs < rhs. + struct is_iless { + bool operator()(const boost::string_view lhs, + const boost::string_view rhs) const { + char lhsUpper[3]; + char rhsUpper[3]; + for (int i = 0; i < 3; ++i) { + lhsUpper[i] = std::toupper(lhs[i]); + rhsUpper[i] = std::toupper(rhs[i]); + } + return boost::string_view(lhsUpper, 3) < + boost::string_view(rhsUpper, 3); + } + }; + + using MonthBimap_t = boost::bimaps::bimap< + boost::bimaps::set_of, Month>; + + // bimap: generalization of map. can be queried by string_view or Month. + const MonthBimap_t MONTHS = []() { + MonthBimap_t months; + months.insert({"JAN", January}); + months.insert({"FEB", February}); + months.insert({"MAR", March}); + months.insert({"APR", April}); + months.insert({"MAY", May}); + months.insert({"JUN", June}); + months.insert({"JUL", July}); + months.insert({"AUG", August}); + months.insert({"SEP", September}); + months.insert({"OCT", October}); + months.insert({"NOV", November}); + months.insert({"DEC", December}); + return months; + }(); + + //clang-format off + // Start of maintenance period + // source: https://web.archive.org/web/20230610050642/https://www.ecb.europa.eu/press/calendars/reserve/html/index.en.html static std::set ecbKnownDateSet = { + // 2005 Date(38371), Date(38391), Date(38420), Date(38455), Date(38483), Date(38511), - Date(38546), Date(38574), Date(38602), Date(38637), Date(38665), - Date(38692), // 2005 + Date(38546), Date(38574), Date(38602), Date(38637), Date(38665), Date(38692), + + // 2006 Date(38735), Date(38756), Date(38784), Date(38819), Date(38847), Date(38883), - Date(38910), Date(38938), Date(38966), Date(39001), Date(39029), - Date(39064), // 2006 + Date(38910), Date(38938), Date(38966), Date(39001), Date(39029), Date(39064), + + // 2007 Date(39099), Date(39127), Date(39155), Date(39190), Date(39217), Date(39246), - Date(39274), Date(39302), Date(39337), Date(39365), Date(39400), - Date(39428), // 2007 + Date(39274), Date(39302), Date(39337), Date(39365), Date(39400), Date(39428), + + // 2008 Date(39463), Date(39491), Date(39519), Date(39554), Date(39582), Date(39610), - Date(39638), Date(39673), Date(39701), Date(39729), Date(39764), - Date(39792), // 2008 + Date(39638), Date(39673), Date(39701), Date(39729), Date(39764), Date(39792), + + // 2009 Date(39834), Date(39855), Date(39883), Date(39911), Date(39946), Date(39974), - Date(40002), Date(40037), Date(40065), Date(40100), Date(40128), - Date(40155), // 2009 + Date(40002), Date(40037), Date(40065), Date(40100), Date(40128), Date(40155), + + // 2010 Date(40198), Date(40219), Date(40247), Date(40282), Date(40310), Date(40345), - Date(40373), Date(40401), Date(40429), Date(40464), Date(40492), - Date(40520), // 2010 + Date(40373), Date(40401), Date(40429), Date(40464), Date(40492), Date(40520), + + // 2011 Date(40562), Date(40583), Date(40611), Date(40646), Date(40674), Date(40709), - Date(40737), Date(40765), Date(40800), Date(40828), Date(40856), - Date(40891), // 2011 - // http://www.ecb.europa.eu/press/pr/date/2011/html/pr110520.en.html + Date(40737), Date(40765), Date(40800), Date(40828), Date(40856), Date(40891), + + // 2012 Date(40926), Date(40954), Date(40982), Date(41010), Date(41038), Date(41073), - Date(41101), Date(41129), Date(41164), Date(41192), Date(41227), - Date(41255), // 2012 + Date(41101), Date(41129), Date(41164), Date(41192), Date(41227), Date(41255), + + // 2013 Date(41290), Date(41318), Date(41346), Date(41374), Date(41402), Date(41437), - Date(41465), Date(41493), Date(41528), Date(41556), Date(41591), - Date(41619), // 2013 - // http://www.ecb.europa.eu/press/pr/date/2013/html/pr130610.en.html + Date(41465), Date(41493), Date(41528), Date(41556), Date(41591), Date(41619), + + // 2014 Date(41654), Date(41682), Date(41710), Date(41738), Date(41773), Date(41801), - Date(41829), Date(41864), Date(41892), Date(41920), Date(41955), - Date(41983), // 2014 - // http://www.ecb.europa.eu/press/pr/date/2014/html/pr140717_1.en.html + Date(41829), Date(41864), Date(41892), Date(41920), Date(41955), Date(41983), + + // 2015 Date(42032), Date(42074), Date(42116), Date(42165), Date(42207), Date(42256), - Date(42305), - Date(42347), // 2015 - // https://www.ecb.europa.eu/press/pr/date/2015/html/pr150622.en.html + Date(42305), Date(42347), + + // 2016 Date(42396), Date(42445), Date(42487), Date(42529), Date(42578), Date(42627), - Date(42669), - Date(42718), // 2016 - // https://www.ecb.europa.eu/press/calendars/reserve/html/index.en.html + Date(42669), Date(42718), + + // 2017 Date(42760), Date(42809), Date(42858), Date(42900), Date(42942), Date(42991), - Date(43040), - Date(43089) // 2017 + Date(43040), Date(43089), + + // 2018 + Date(43131), Date(43167), Date(43216), Date(43265), Date(43307), Date(43356), + Date(43398), Date(43447), + + // 2019 + Date(43495), Date(43537), Date(43572), Date(43628), Date(43677), Date(43726), + Date(43768), Date(43817), + + // 2020 + Date(43859), Date(43908), Date(43957), Date(43992), Date(44034), Date(44090), + Date(44139), Date(44181), + + // 2021 + Date(44223), Date(44272), Date(44314), Date(44363), Date(44405), Date(44454), + Date(44503), Date(44552), + + // 2022 + Date(44601), Date(44636), Date(44671), Date(44727), Date(44769), Date(44818), + Date(44867), Date(44916), + + // 2023 + Date(44965), Date(45007), Date(45056), Date(45098), Date(45140), Date(45189), + Date(45231), Date(45280), + + // 2024 + Date(45322), Date(45364), Date(45399), Date(45455), Date(45497), Date(45553), + Date(45588), Date(45644) }; + //clang-format on } const std::set& ECB::knownDates() { - return detail::ecbKnownDateSet; + return ecbKnownDateSet; } void ECB::addDate(const Date& d) { - detail::ecbKnownDateSet.insert(d); + ecbKnownDateSet.insert(d); } void ECB::removeDate(const Date& d) { - detail::ecbKnownDateSet.erase(d); + ecbKnownDateSet.erase(d); + } + + namespace { + int ToInteger(const char c) { + const int i = static_cast(c) - static_cast('0'); + QL_ASSERT((i >= 0) && (i <= 9), "Character does not represent a digit. char: " << c); + return i; + } } Date ECB::date(const string& ecbCode, @@ -97,24 +183,12 @@ namespace QuantLib { QL_REQUIRE(isECBcode(ecbCode), ecbCode << " is not a valid ECB code"); - string code = to_upper_copy(ecbCode); - string monthString = code.substr(0, 3); - Month m; - if (monthString=="JAN") m = January; - else if (monthString=="FEB") m = February; - else if (monthString=="MAR") m = March; - else if (monthString=="APR") m = April; - else if (monthString=="MAY") m = May; - else if (monthString=="JUN") m = June; - else if (monthString=="JUL") m = July; - else if (monthString=="AUG") m = August; - else if (monthString=="SEP") m = September; - else if (monthString=="OCT") m = October; - else if (monthString=="NOV") m = November; - else if (monthString=="DEC") m = December; - else QL_FAIL("not an ECB month (and it should have been)"); - - Year y = std::stoi(code.substr(3, 2)); + // convert first 3 characters to `Month m` + const boost::string_view monthCode(ecbCode.data(), 3); + const Month m = MONTHS.left.at(monthCode); + + // convert 4th, 5th characters to `Year y` + Year y = ToInteger(ecbCode[3])*10 + ToInteger(ecbCode[4]); Date referenceDate = (refDate != Date() ? refDate : Date(Settings::instance().evaluationDate())); @@ -131,62 +205,24 @@ namespace QuantLib { QL_REQUIRE(isECBdate(ecbDate), ecbDate << " is not a valid ECB date"); - std::ostringstream ECBcode; - unsigned int y = ecbDate.year() % 100; - string padding; - if (y < 10) - padding = "0"; - switch(ecbDate.month()) { - case January: - ECBcode << "JAN" << padding << y; - break; - case February: - ECBcode << "FEB" << padding << y; - break; - case March: - ECBcode << "MAR" << padding << y; - break; - case April: - ECBcode << "APR" << padding << y; - break; - case May: - ECBcode << "MAY" << padding << y; - break; - case June: - ECBcode << "JUN" << padding << y; - break; - case July: - ECBcode << "JUL" << padding << y; - break; - case August: - ECBcode << "AUG" << padding << y; - break; - case September: - ECBcode << "SEP" << padding << y; - break; - case October: - ECBcode << "OCT" << padding << y; - break; - case November: - ECBcode << "NOV" << padding << y; - break; - case December: - ECBcode << "DEC" << padding << y; - break; - default: - QL_FAIL("not an ECB month (and it should have been)"); - } + // 3 characters for the month + const boost::string_view month = MONTHS.right.at(ecbDate.month()); + + // last two digits of the year + const unsigned int y = ecbDate.year() % 100; + + // c-style string. length: 6 == (3 for month + 2 for year + 1 for terminating null) + char ECBcode[6]; + std::snprintf(ECBcode, 6, "%3s%02u", month.data(), y); #if defined(QL_EXTRA_SAFETY_CHECKS) - QL_ENSURE(isECBcode(ECBcode.str()), - "the result " << ECBcode.str() << + QL_ENSURE(isECBcode(ECBcode), + "the result " << ECBcode << " is an invalid ECB code"); #endif - return ECBcode.str(); + return ECBcode; } - - Date ECB::nextDate(const Date& date) { Date d = (date == Date() ? Settings::instance().evaluationDate() : @@ -217,67 +253,64 @@ namespace QuantLib { if (ecbCode.length() != 5) return false; - string code = to_upper_copy(ecbCode); - - string str1("0123456789"); - string::size_type loc = str1.find(code.substr(3, 1), 0); - if (loc == string::npos) - return false; - loc = str1.find(code.substr(4, 1), 0); - if (loc == string::npos) - return false; + // first 3 characters need to represent month, case insensitive + { + const boost::string_view month(ecbCode.data(), 3); + if (MONTHS.left.find(month) == MONTHS.left.end()) + return false; + } - string monthString = code.substr(0, 3); - if (monthString=="JAN") return true; - else if (monthString=="FEB") return true; - else if (monthString=="MAR") return true; - else if (monthString=="APR") return true; - else if (monthString=="MAY") return true; - else if (monthString=="JUN") return true; - else if (monthString=="JUL") return true; - else if (monthString=="AUG") return true; - else if (monthString=="SEP") return true; - else if (monthString=="OCT") return true; - else if (monthString=="NOV") return true; - else if (monthString=="DEC") return true; - else return false; + // 4th, 5th characters need to be digit + return std::isdigit(static_cast(ecbCode[3])) + && std::isdigit(static_cast(ecbCode[4])); } string ECB::nextCode(const std::string& ecbCode) { QL_REQUIRE(isECBcode(ecbCode), ecbCode << " is not a valid ECB code"); - string code = to_upper_copy(ecbCode); - std::ostringstream result; - - string monthString = code.substr(0, 3); - if (monthString=="JAN") result << "FEB" << code.substr(3, 2); - else if (monthString=="FEB") result << "MAR" << code.substr(3, 2); - else if (monthString=="MAR") result << "APR" << code.substr(3, 2); - else if (monthString=="APR") result << "MAY" << code.substr(3, 2); - else if (monthString=="MAY") result << "JUN" << code.substr(3, 2); - else if (monthString=="JUN") result << "JUL" << code.substr(3, 2); - else if (monthString=="JUL") result << "AUG" << code.substr(3, 2); - else if (monthString=="AUG") result << "SEP" << code.substr(3, 2); - else if (monthString=="SEP") result << "OCT" << code.substr(3, 2); - else if (monthString=="OCT") result << "NOV" << code.substr(3, 2); - else if (monthString=="NOV") result << "DEC" << code.substr(3, 2); - else if (monthString=="DEC") { - unsigned int y = (std::stoi(code.substr(3, 2)) + 1) % 100; - string padding; - if (y < 10) - padding = "0"; - - result << "JAN" << padding << y; - } else QL_FAIL("not an ECB month (and it should have been)"); - + const boost::string_view month(ecbCode.data(), 3); + const Month monthEnum = MONTHS.left.at(month); + + string nextCodeStr; + nextCodeStr.reserve(5); + if (monthEnum != December) { + // use next month + const Month nextMonthEnum = static_cast(monthEnum + 1); + const boost::string_view nextMonth = MONTHS.right.at(nextMonthEnum); + nextCodeStr.append(nextMonth.data(), 3); + + // copy year + nextCodeStr += {ecbCode[3], ecbCode[4]}; + } else { + // previous month was DEC + nextCodeStr.append("JAN"); + + // init with previous year + nextCodeStr += { ecbCode[3], ecbCode[4] }; + + // increment year's last digit (e.g. '22' -> '23'). + // if overflow (e.g. '29' -> '20'), then also increment 2nd digit (e.g. '20' -> '30'). + const auto incrementAndCheckForOverlow = [](char& dig) -> bool { + if (dig == '9') { + dig = '0'; + return true; + } else { + ++dig; + return false; + } + }; + if (incrementAndCheckForOverlow(nextCodeStr[4])) + incrementAndCheckForOverlow(nextCodeStr[3]); + } + return nextCodeStr; #if defined(QL_EXTRA_SAFETY_CHECKS) - QL_ENSURE(isECBcode(result.str()), - "the result " << result.str() << + QL_ENSURE(isECBcode(nextCodeStr), + "the result " << nextCodeStr << " is an invalid ECB code"); #endif - return result.str(); + return nextCodeStr; } } diff --git a/test-suite/dates.cpp b/test-suite/dates.cpp index 2f0eea0b043..801fb63bd69 100644 --- a/test-suite/dates.cpp +++ b/test-suite/dates.cpp @@ -42,49 +42,96 @@ BOOST_FIXTURE_TEST_SUITE(QuantLibTest, TopLevelFixture) BOOST_AUTO_TEST_SUITE(DateTest) +BOOST_AUTO_TEST_CASE(ecbIsECBcode) { + BOOST_TEST_MESSAGE("Testing ECB codes for validity..."); + + BOOST_TEST(ECB::isECBcode("JAN00")); + BOOST_TEST(ECB::isECBcode("FEB78")); + BOOST_TEST(ECB::isECBcode("mar58")); + BOOST_TEST(ECB::isECBcode("aPr99")); + + BOOST_TEST(!ECB::isECBcode("")); + BOOST_TEST(!ECB::isECBcode("JUNE99")); + BOOST_TEST(!ECB::isECBcode("JUN1999")); + BOOST_TEST(!ECB::isECBcode("JUNE")); + BOOST_TEST(!ECB::isECBcode("JUNE1999")); + BOOST_TEST(!ECB::isECBcode("1999")); +} + BOOST_AUTO_TEST_CASE(ecbDates) { BOOST_TEST_MESSAGE("Testing ECB dates..."); - std::set knownDates = ECB::knownDates(); - if (knownDates.empty()) - BOOST_FAIL("empty EBC date vector"); + const std::set knownDates = ECB::knownDates(); + BOOST_TEST(!knownDates.empty(), + "empty ECB date vector"); - Size n = ECB::nextDates(Date::minDate()).size(); - if (n != knownDates.size()) - BOOST_FAIL("nextDates(minDate) returns " << n << + const Size n = ECB::nextDates(Date::minDate()).size(); + BOOST_TEST(n == knownDates.size(), + "nextDates(minDate) returns " << n << " instead of " << knownDates.size() << " dates"); - std::set::const_iterator i; - Date previousEcbDate = Date::minDate(), - currentEcbDate, ecbDateMinusOne; - for (i=knownDates.begin(); i!=knownDates.end(); ++i) { - - currentEcbDate = *i; + Date previousEcbDate = Date::minDate(); + for (const Date& currentEcbDate : knownDates) { if (!ECB::isECBdate(currentEcbDate)) BOOST_FAIL(currentEcbDate << " fails isECBdate check"); - ecbDateMinusOne = currentEcbDate-1; + const Date ecbDateMinusOne = currentEcbDate-1; if (ECB::isECBdate(ecbDateMinusOne)) BOOST_FAIL(ecbDateMinusOne << " fails isECBdate check"); - if (ECB::nextDate(ecbDateMinusOne)!=currentEcbDate) - BOOST_FAIL("next EBC date following " << ecbDateMinusOne << + if (ECB::nextDate(ecbDateMinusOne) != currentEcbDate) + BOOST_FAIL("next ECB date following " << ecbDateMinusOne << " must be " << currentEcbDate); - if (ECB::nextDate(previousEcbDate)!=currentEcbDate) - BOOST_FAIL("next EBC date following " << previousEcbDate << + if (ECB::nextDate(previousEcbDate) != currentEcbDate) + BOOST_FAIL("next ECB date following " << previousEcbDate << " must be " << currentEcbDate); previousEcbDate = currentEcbDate; } - Date knownDate = *knownDates.begin(); + const Date knownDate = *knownDates.begin(); ECB::removeDate(knownDate); - if (ECB::isECBdate(knownDate)) - BOOST_FAIL("unable to remove an EBC date"); + BOOST_TEST(!ECB::isECBdate(knownDate), + "unable to remove an ECB date"); ECB::addDate(knownDate); - if (!ECB::isECBdate(knownDate)) - BOOST_FAIL("unable to add an EBC date"); + BOOST_TEST(ECB::isECBdate(knownDate), + "unable to add an ECB date"); +} + +BOOST_AUTO_TEST_CASE(ecbGetDateFromCode) { + BOOST_TEST_MESSAGE("Testing conversion of ECB codes to dates..."); + + const Date ref2000((Day)1, January, (Year)2000); + BOOST_TEST(ECB::date("JAN05", ref2000) == Date((Day)19, January, (Year)2005)); + BOOST_TEST(ECB::date("FEB06", ref2000) == Date((Day) 8, February, (Year)2006)); + BOOST_TEST(ECB::date("MAR07", ref2000) == Date((Day)14, March, (Year)2007)); + BOOST_TEST(ECB::date("APR08", ref2000) == Date((Day)16, April, (Year)2008)); + BOOST_TEST(ECB::date("JUN09", ref2000) == Date((Day)10, June, (Year)2009)); + BOOST_TEST(ECB::date("JUL10") == Date((Day)14, July, (Year)2010)); + BOOST_TEST(ECB::date("AUG11") == Date((Day)10, August, (Year)2011)); + BOOST_TEST(ECB::date("SEP12") == Date((Day)12, September, (Year)2012)); + BOOST_TEST(ECB::date("OCT13") == Date((Day) 9, October, (Year)2013)); + BOOST_TEST(ECB::date("NOV14") == Date((Day)12, November, (Year)2014)); + BOOST_TEST(ECB::date("DEC15") == Date((Day) 9, December, (Year)2015)); +} + +BOOST_AUTO_TEST_CASE(ecbGetCodeFromDate) { + BOOST_TEST_MESSAGE("Testing creation of ECB code from a given date..."); + + BOOST_TEST("JAN06" == ECB::code(Date((Day)18, January, (Year)2006))); + BOOST_TEST("MAR10" == ECB::code(Date((Day)10, March, (Year)2010))); + BOOST_TEST("NOV17" == ECB::code(Date((Day) 1, November, (Year)2017))); +} + +BOOST_AUTO_TEST_CASE(ecbNextCode) { + BOOST_TEST_MESSAGE("Testing getting the next code from a given code..."); + + BOOST_TEST("FEB06" == ECB::nextCode("JAN06")); + BOOST_TEST("MAR10" == ECB::nextCode("FeB10")); + BOOST_TEST("NOV17" == ECB::nextCode("OCT17")); + BOOST_TEST("JAN18" == ECB::nextCode("dEC17")); + BOOST_TEST("JAN00" == ECB::nextCode("dec99")); } BOOST_AUTO_TEST_CASE(immDates) { @@ -474,4 +521,4 @@ BOOST_AUTO_TEST_CASE(canHash) { BOOST_AUTO_TEST_SUITE_END() -BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file +BOOST_AUTO_TEST_SUITE_END()