Section12.12Case Study: Building a Multi-File Program
Now that we know how to separate code into multiple files, we are ready to look at implementing the designs we came up with in SectionΒ 12.7. Since we have already done the design for our functions, we will skip directly to writing code for the βTop Downβ design.
We will be building a single project (set of files) with two programs. One program will be the program we want to write - one that reads in two dates and calculates the difference between them. The other program will be a test program that verifies functions work correctly.
Depending on the development environment you are using outside of this book, the practicalities of setting up two programs that build from overlapping files may vary. You may need to set up two separate βprojectsβ in your development environment or you may be able to set up one βprojectβ with multiple build targets.
The code will be split into multiple files, with the following structure:
DateFunctions.cxx: will contain the implementations of the date-related functions. We will be implementing it as a module called DateFunctions. We could instead build it as a .h/.cpp file pair - that implementation is available in the appendix to this chapter.
We will use incremental development to build our program in small steps, testing each part as we go. We will also use test driven developmentβfor each function that we implement, we will start by writing a test for it first and then using that test to develop the function. Only after the function is working, will we try to integrate it into the βrealβ program.
We need to pick somewhere to start. It makes sense to pick a getMonth function first, as it is a simple function that does not depend on anything else.
We will start by writing tests for the getMonth function. This will help us define the expected behavior of the function before we implement it. The two important valid cases are if the month has 1 or 2 digits. We should have tests for both of those.
We should also consider that happens if there is an invalid value. Either a number like 13 or a non-numeric string. We will throw an exception in those cases as it is not clear from the context of that low level function what the right way to handle the issue is. (Recall that the core idea of exceptions is to allow us to detect an issue in low-level code and propagate it to higher levels that might better know how to handle it.)
module;
#include <string>
#include <stdexcept>
export module DateFunctions;
using namespace std;
export int getMonth(const string& date) {
size_t slash = date.find('/');
if (slash == string::npos) {
throw logic_error("Date must contain '/'");
}
string monthPart = date.substr(0, slash);
// stoi will throw exceptions for invalid input
int monthNum = stoi(monthPart);
if (monthNum < 1 || monthNum > 12) {
throw logic_error("Month out of range");
}
// If we reach here, the month is valid
return monthNum;
}
Now we can go back to compile the tests with our module. The following activecode is set up to compile both the test tile (shown), as well as our module file, using a recipe like:
Correct. It turns out that stoi will turn 1a5 into 1, ignore the a5, and not throw an exception. To see what the function actually returns, we can add some print statements or use a debugger.
Before going on to other functions, we should fix getMonth. It appears we will need to scan the monthNum string to make sure that each character is a digit.
for(char c: monthPart) {
if(!isdigit(c)) {
throw logic_error("Month must be a number");
}
}
Activity12.12.1.
Try adding the loop shown above (or similar code that checks if all the characters are digits) to this copy of DateFunctions.cxx. (Right after the monthPart variable is assigned is a logical place). The activecode is set to compile, but not link. It will tell you if you have a compile error, but not if your implementation is correct. (We will check that below.)
Subsection12.12.4Using getMonth in the real program
Now that we have implemented and tested getMonth, we could use it in our main program. According to our design (ExampleΒ 12.7.1), we wonβt actually call getMonth from main. It will be used by dateToDays, which is called by daysBetween, which is called from main. So the only reason to work on main at this point would be if we wanted to do some manual testing using real user input.