Skip to content

Commit

Permalink
ASX: simplified/improved implementation; completed test coverage (#1854)
Browse files Browse the repository at this point in the history
  • Loading branch information
lballabio committed Dec 28, 2023
2 parents 9f29f97 + cf68b47 commit 7dfe04c
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 92 deletions.
122 changes: 38 additions & 84 deletions ql/time/asx.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@
#include <ql/settings.hpp>
#include <ql/utilities/dataparsers.hpp>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/utility/string_view.hpp>
#include <string>

using boost::algorithm::to_upper_copy;
using std::string;
#include <cctype>

namespace QuantLib {

namespace {
const boost::string_view All_MONTH_CODES = "FGHJKMNQUVXZ";
}

bool ASX::isASXdate(const Date& date, bool mainCycle) {
if (date.weekday()!=Friday)
return false;
Expand All @@ -57,109 +60,60 @@ namespace QuantLib {
if (in.length() != 2)
return false;

string str1("0123456789");
string::size_type loc = str1.find(in.substr(1,1), 0);
if (loc == string::npos)
// 2nd character of code needs to be digit
if (!std::isdigit(static_cast<unsigned char>(in[1])))
return false;

if (mainCycle) str1 = "hmzuHMZU";
else str1 = "fghjkmnquvxzFGHJKMNQUVXZ";
loc = str1.find(in.substr(0,1), 0);
return loc != string::npos;
// 1st character needs to represent the correct month
const boost::string_view validMonthCodes = mainCycle ? "HMUZ" : All_MONTH_CODES;
return validMonthCodes.find(std::toupper(in[0])) != boost::string_view::npos;
}

std::string ASX::code(const Date& date) {
QL_REQUIRE(isASXdate(date, false),
date << " is not an ASX date");

std::ostringstream ASXcode;
unsigned int y = date.year() % 10;
switch(date.month()) {
case January:
ASXcode << 'F' << y;
break;
case February:
ASXcode << 'G' << y;
break;
case March:
ASXcode << 'H' << y;
break;
case April:
ASXcode << 'J' << y;
break;
case May:
ASXcode << 'K' << y;
break;
case June:
ASXcode << 'M' << y;
break;
case July:
ASXcode << 'N' << y;
break;
case August:
ASXcode << 'Q' << y;
break;
case September:
ASXcode << 'U' << y;
break;
case October:
ASXcode << 'V' << y;
break;
case November:
ASXcode << 'X' << y;
break;
case December:
ASXcode << 'Z' << y;
break;
default:
QL_FAIL("not an ASX month (and it should have been)");
}
// month() is 1-based!
const char monthCode = All_MONTH_CODES[date.month()-1];
const char yearDigit = static_cast<char>(static_cast<int>('0') + (date.year() % 10));
std::string code{monthCode, yearDigit};

#if defined(QL_EXTRA_SAFETY_CHECKS)
QL_ENSURE(isASXcode(ASXcode.str(), false),
"the result " << ASXcode.str() <<
" is an invalid ASX code");
#ifdef QL_EXTRA_SAFETY_CHECKS
QL_ENSURE(isASXcode(code, false),
"the result " << code <<
" is an invalid ASX code");
#endif
return ASXcode.str();

return code;
}

Date ASX::date(const std::string& asxCode,
const Date& refDate) {
QL_REQUIRE(isASXcode(asxCode, false),
asxCode << " is not a valid ASX code");

Date referenceDate = (refDate != Date() ?
refDate :
Date(Settings::instance().evaluationDate()));

std::string code = to_upper_copy(asxCode);
std::string ms = code.substr(0,1);
QuantLib::Month m;
if (ms=="F") m = January;
else if (ms=="G") m = February;
else if (ms=="H") m = March;
else if (ms=="J") m = April;
else if (ms=="K") m = May;
else if (ms=="M") m = June;
else if (ms=="N") m = July;
else if (ms=="Q") m = August;
else if (ms=="U") m = September;
else if (ms=="V") m = October;
else if (ms=="X") m = November;
else if (ms=="Z") m = December;
else QL_FAIL("invalid ASX month letter");

Year y = std::stoi(code.substr(1,1));
const Date referenceDate = (refDate != Date() ?
refDate :
Date(Settings::instance().evaluationDate()));

const char ms = std::toupper(asxCode.front());
const std::size_t idxZeroBased = All_MONTH_CODES.find(ms);
QL_ASSERT(idxZeroBased != All_MONTH_CODES.npos, "invalid ASX month letter. code: " + asxCode);

// QuantLib::Month is 1-based!
const QuantLib::Month m = static_cast<QuantLib::Month>(idxZeroBased + 1);

// convert 2nd char to year digit
Year y = static_cast<int>(asxCode[1]) - static_cast<int>('0');
QL_ASSERT((y>=0) && (y <= 9), "invalid ASX year digit. code: " + asxCode);

/* year<1900 are not valid QuantLib years: to avoid a run-time
exception few lines below we need to add 10 years right away */
if (y==0 && referenceDate.year()<=1909) y+=10;
Year referenceYear = (referenceDate.year() % 10);
const Year referenceYear = (referenceDate.year() % 10);
y += referenceDate.year() - referenceYear;
Date result = ASX::nextDate(Date(1, m, y), false);
if (result<referenceDate)
return ASX::nextDate(Date(1, m, y+10), false);

return result;
return (result >= referenceDate) ? result : ASX::nextDate(Date(1, m, y+10), false);
}

Date ASX::nextDate(const Date& date, bool mainCycle) {
Expand Down
41 changes: 33 additions & 8 deletions test-suite/dates.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,12 +209,8 @@ BOOST_AUTO_TEST_CASE(asxDates) {
"F9", "G9", "H9", "J9", "K9", "M9", "N9", "Q9", "U9", "V9", "X9", "Z9"
};

Date counter = { 1, January, 2000 };
Date last = { 1, January, 2040 };
Date asx;

while (counter <= last) {
asx = ASX::nextDate(counter, false);
for (Date counter(1, January, 2000); counter <= Date(1,January, 2040); ++counter) {
const Date asx = ASX::nextDate(counter, false);

// check that asx is greater than counter
if (asx <= counter)
Expand All @@ -229,7 +225,7 @@ BOOST_AUTO_TEST_CASE(asxDates) {
<< counter.weekday() << " " << counter << ")");

// check that asx is <= to the next ASX date in the main cycle
if (asx>ASX::nextDate(counter, true))
if (asx > ASX::nextDate(counter, true))
BOOST_FAIL(asx.weekday() << " " << asx
<< " is not less than or equal to the next future in the main cycle "
<< ASX::nextDate(counter, true));
Expand All @@ -246,9 +242,38 @@ BOOST_AUTO_TEST_CASE(asxDates) {
BOOST_FAIL(ASX::date(ASXcode, counter) << " is wrong for " << ASXcode
<< " at reference date " << counter);
}
}
}

counter = counter + 1;

BOOST_AUTO_TEST_CASE(asxDatesSpecific) {
BOOST_TEST_MESSAGE("Testing ASX functionlity with specific dates...");

// isASXdate
{
// date is ASX date depending on mainCycle
const Date date((Day)12, January, (Year)2024);
BOOST_ASSERT(date.weekday() == Friday);

// check mainCycle
BOOST_TEST(ASX::isASXdate(date, /*mainCycle*/false));
BOOST_TEST(!ASX::isASXdate(date, /*mainCycle*/true));
}

// nextDate from code + ref date
BOOST_TEST(Date((Day)8, February, (Year)2002)
== ASX::nextDate("F2", /*mainCycle*/false, Date((Day)1, January, (Year)2000)));

BOOST_TEST(Date((Day)9, June, (Year)2023)
== ASX::nextDate("K3", /*mainCycle*/true, Date((Day)1, January, (Year)2014)));

// nextCode
BOOST_TEST("F4" == ASX::nextCode(Date((Day)1, January, (Year)2024), /*mainCycle*/false));
BOOST_TEST("G4" == ASX::nextCode(Date((Day)15, January, (Year)2024), /*mainCycle*/false));
BOOST_TEST("H4" == ASX::nextCode(Date((Day)15, January, (Year)2024), /*mainCycle*/true));

BOOST_TEST("G4" == ASX::nextCode("F4", /*mainCycle*/false, Date((Day)1, January, (Year)2020)));
BOOST_TEST("H5" == ASX::nextCode("Z4", /*mainCycle*/true, Date((Day)1, January, (Year)2020)));
}

BOOST_AUTO_TEST_CASE(testConsistency) {
Expand Down

0 comments on commit 7dfe04c

Please sign in to comment.