Difference between revisions of "Component programming with ranges"
(→Generating Dates in a Year) |
m (code tags) |
||
Line 43: | Line 43: | ||
While intuitively straightforward, this task has many points of complexity: | While intuitively straightforward, this task has many points of complexity: | ||
− | * While generating all dates in a year is trivial, thanks to D's std.datetime module, the order in which they must be processed is far from obvious, due to the following complicating factors: | + | * While generating all dates in a year is trivial, thanks to D's <code>std.datetime</code> module, the order in which they must be processed is far from obvious, due to the following complicating factors: |
* Since we're writing to the console, we're limited to outputting one line at a time; we can't draw one cell of the grid and then go back up a few lines, move a few columns over, and draw the next cell in the grid. We have to somehow print the first lines of all cells in the top row, followed by the second lines, then the third lines, etc., and repeat this process for each row in the grid. | * Since we're writing to the console, we're limited to outputting one line at a time; we can't draw one cell of the grid and then go back up a few lines, move a few columns over, and draw the next cell in the grid. We have to somehow print the first lines of all cells in the top row, followed by the second lines, then the third lines, etc., and repeat this process for each row in the grid. | ||
* As a result, the order in which the days in a month are processed is not the natural, chronological order. We have to assemble the dates for the first weeks in each of the first 3 months, say, (if we are putting 3 months per row in the grid), print those out, then assemble the dates for the second weeks in each month, print those out, etc.. | * As a result, the order in which the days in a month are processed is not the natural, chronological order. We have to assemble the dates for the first weeks in each of the first 3 months, say, (if we are putting 3 months per row in the grid), print those out, then assemble the dates for the second weeks in each month, print those out, etc.. | ||
Line 81: | Line 81: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | Already, that loop body is looking pretty complicated. But it's still missing one key ingredient: state transitions. Once we finish reading the preamble, we need to transition to State.Body so that the next line can be processed correctly, and ditto for the transition to State.Epilogue. So our code becomes: | + | Already, that loop body is looking pretty complicated. But it's still missing one key ingredient: state transitions. Once we finish reading the preamble, we need to transition to <code>State.Body</code> so that the next line can be processed correctly, and ditto for the transition to <code>State.Epilogue</code>. So our code becomes: |
<syntaxhighlight lang=D> | <syntaxhighlight lang=D> | ||
Line 108: | Line 108: | ||
Note that almost all of this code is just scaffolding; we haven't even written the code that does the real processing of the input data! | Note that almost all of this code is just scaffolding; we haven't even written the code that does the real processing of the input data! | ||
− | Furthermore, suppose the way we process the body changes depending on something in the preamble. For example, the preamble may define an encoding, so then we have to save this encoding value somewhere outside the loop, so that when we transition to State.Body, we will know how to correctly decode the input line. | + | Furthermore, suppose the way we process the body changes depending on something in the preamble. For example, the preamble may define an encoding, so then we have to save this encoding value somewhere outside the loop, so that when we transition to <code>State.Body</code>, we will know how to correctly decode the input line. |
Such ad hoc structure resolutions are a breeding ground for bugs, because we're adding all sorts of state variables, flags, and other such things to the outer scope of the loop. Each piece of the code depends on some number of variables that are set by other code elsewhere, causing the code to be very fragile. This approach also often leads to complicated loop conditions, which invite even more bugs. | Such ad hoc structure resolutions are a breeding ground for bugs, because we're adding all sorts of state variables, flags, and other such things to the outer scope of the loop. Each piece of the code depends on some number of variables that are set by other code elsewhere, causing the code to be very fragile. This approach also often leads to complicated loop conditions, which invite even more bugs. |
Revision as of 18:47, 3 August 2013
Contents
Preface
This article was inspired by Walter's article on component programming in D and based on a related discussion thread in the D forum. In short, Walter's article addressed the question of why, despite years of carefully programming in paradigms that purportedly makes your code more reusable, so very little code from the past is actually reused. Component-style programming, in which code is assembled from reusable components may be data sources, data sinks, or algorithms (that transforms data sources and puts them into data sinks), is proposed as a possible solution to this problem.
In this article, we will consider how component-style programming using ranges can greatly untangle a complicated algorithm into manageable pieces that are straightforward to write, easy to debug, and highly reusable.
The Task
We shall consider the classic task of laying out a yearly calendar on the console, such that given a particular year, the program will print out a number of lines that displays the 12 months in a nice grid layout, with numbers indicating each day within the month. Something like this:
January February March 1 2 3 4 5 1 2 1 2 6 7 8 9 10 11 12 3 4 5 6 7 8 9 3 4 5 6 7 8 9 13 14 15 16 17 18 19 10 11 12 13 14 15 16 10 11 12 13 14 15 16 20 21 22 23 24 25 26 17 18 19 20 21 22 23 17 18 19 20 21 22 23 27 28 29 30 31 24 25 26 27 28 24 25 26 27 28 29 30 31 April May June 1 2 3 4 5 6 1 2 3 4 1 7 8 9 10 11 12 13 5 6 7 8 9 10 11 2 3 4 5 6 7 8 14 15 16 17 18 19 20 12 13 14 15 16 17 18 9 10 11 12 13 14 15 21 22 23 24 25 26 27 19 20 21 22 23 24 25 16 17 18 19 20 21 22 28 29 30 26 27 28 29 30 31 23 24 25 26 27 28 29 30 July August September 1 2 3 4 5 6 1 2 3 1 2 3 4 5 6 7 7 8 9 10 11 12 13 4 5 6 7 8 9 10 8 9 10 11 12 13 14 14 15 16 17 18 19 20 11 12 13 14 15 16 17 15 16 17 18 19 20 21 21 22 23 24 25 26 27 18 19 20 21 22 23 24 22 23 24 25 26 27 28 28 29 30 31 25 26 27 28 29 30 31 29 30 October November December 1 2 3 4 5 1 2 1 2 3 4 5 6 7 6 7 8 9 10 11 12 3 4 5 6 7 8 9 8 9 10 11 12 13 14 13 14 15 16 17 18 19 10 11 12 13 14 15 16 15 16 17 18 19 20 21 20 21 22 23 24 25 26 17 18 19 20 21 22 23 22 23 24 25 26 27 28 27 28 29 30 31 24 25 26 27 28 29 30 29 30 31
While intuitively straightforward, this task has many points of complexity:
- While generating all dates in a year is trivial, thanks to D's
std.datetime
module, the order in which they must be processed is far from obvious, due to the following complicating factors: - Since we're writing to the console, we're limited to outputting one line at a time; we can't draw one cell of the grid and then go back up a few lines, move a few columns over, and draw the next cell in the grid. We have to somehow print the first lines of all cells in the top row, followed by the second lines, then the third lines, etc., and repeat this process for each row in the grid.
- As a result, the order in which the days in a month are processed is not the natural, chronological order. We have to assemble the dates for the first weeks in each of the first 3 months, say, (if we are putting 3 months per row in the grid), print those out, then assemble the dates for the second weeks in each month, print those out, etc..
- Furthermore, within the rows representing each week, some days may be missing, depending on where the boundaries of adjacent months fall; these missing days must then be filled out in the following month's first week before the first full week in the month is printed. It is not that simple to figure out where a week starts and ends, and how many rows are needed per month.
- If some months have more full weeks than others, they may occupy less vertical space than other months on the same row in the grid; so we need to insert blank spaces into these shorter months in order for the grid cells to line up vertically in the output.
Considering all of the above points, it would appear at first glance that we are doomed to writing algorithms specific only to this task, because each piece of the puzzle depends on the others in complex ways. It would appear hopeless to actually get any reusable components out of our calendar program! And indeed, this would be the case if we approached the problem from the traditional imperative approach.
Furthermore, such a complex algorithm would be difficult to write, and would be more prone to bugs because of its complexity.
Sources of Complexity
One of the more influential courses I took in college was on Jackson Structured Programming. It identified two sources of programming complexity (i.e., where bugs are most likely to occur):
- Mismatches between the structure of the program and the structure of the data (e.g., you're reading an input file that has a preamble, body, and epilogue, but your code has a single loop over lines in the file), or between two or more data structures that you are processing at the same time (e.g., laying out a yearly calendar where the boundaries of weeks don't correspond with the boundaries of the months, and the grid structure doesn't correspond with the line-by-line sequence of the final output);
- Writing loop invariants (or equivalently, loop conditions).
Most non-trivial loops in imperative code have both, which makes them doubly prone to bugs. Take the example of reading a file with three sections in a single loop over the lines of the file. The mismatch between the code structure (a single loop) and the file structure (three sequential sections) often prompts people to add boolean flags, state variables, and the like, in order to resolve the conflict between the two structures. For example, to keep track of which section we're in when processing each line, we may use a state variable, like this:
auto file = File("inputfile");
enum State { Preamble, Body, Epilogue }
auto state = State.Preamble;
foreach (line; file.byLine()) {
final switch (state) {
case State.Preamble:
... // process preamble
break;
case State.Body:
... // process body
break;
case State.Epilogue:
... // process epilogue
break;
}
}
Already, that loop body is looking pretty complicated. But it's still missing one key ingredient: state transitions. Once we finish reading the preamble, we need to transition to State.Body
so that the next line can be processed correctly, and ditto for the transition to State.Epilogue
. So our code becomes:
auto file = File("inputfile");
enum State { Preamble, Body, Epilogue }
auto state = State.Preamble;
foreach (line; file.byLine()) {
final switch (state) {
case State.Preamble:
... // process preamble
if (endOfPreamble)
state = State.Body;
break;
case State.Body:
... // process body
if (endOfBody)
state = State.Epilogue;
break;
case State.Epilogue:
... // process epilogue
break;
}
}
Note that almost all of this code is just scaffolding; we haven't even written the code that does the real processing of the input data!
Furthermore, suppose the way we process the body changes depending on something in the preamble. For example, the preamble may define an encoding, so then we have to save this encoding value somewhere outside the loop, so that when we transition to State.Body
, we will know how to correctly decode the input line.
Such ad hoc structure resolutions are a breeding ground for bugs, because we're adding all sorts of state variables, flags, and other such things to the outer scope of the loop. Each piece of the code depends on some number of variables that are set by other code elsewhere, causing the code to be very fragile. This approach also often leads to complicated loop conditions, which invite even more bugs.
Untangling Complexity
In contrast, if you structure your code according to the structure of the input (i.e., one loop for processing the preamble, one loop for processing the body, one loop for processing the epilogue), it becomes considerably less complex, easier to read (and write!), and far less bug prone. Your loop conditions become simpler, and thus easier to reason about and leave less room for bugs to hide.
But to be able to process the input in this way requires that we encapsulate our input so that it can be processed by 3 different loops. Once we go down that road, we start to arrive at the concept of input ranges... then we abstract away the three loops into three components, and we arrive at Walter's component-style programming, where each component has a well-defined input and output, and the transformation process from input to output has a straightforward correspondence.
Structuring the Calendar Program
Consider our calendar program. Using the traditional approach, we may structure our code one of two ways:
- Have a single main loop that prints out each line of the calendar at a time. The problem is, the loop body will be extremely complex, because sometimes we need to output month names, sometimes weeks. Then within each week, we have to know what day to start the week, what day to end it, and we have to keep track of which days of the month we're currently on, in order to continue from the previously-output line correctly. On top of that, we're processing multiple months at a time, so we have to generate these dates out-of-order, yet in the end the dates generated for each month must add up to a chronological order.
- Create a grid buffer of characters, then loop over dates of the year and place them in the buffer in the right place. This also introduces a lot of complexity: where should we place the month names, and, given a particular month and date, how do we know where on the month's cell we should place the day? Since the starting point of each subsequent month depends on the ending point of the previous month, we will need all kinds of state variables and counters to keep track of everything. And in the end, we still have to write another loop over the buffer to print out each line in display order.
Neither approach is very appealing, because they are catering to one of the conflicting structures at the expense of the others, resulting in a level of complexity that's difficult to deal with. Such code will be very prone to bugs due to its sheer complexity. It will also have no reusable pieces.
Using ranges, however, our task becomes considerably more tractable. First, let's identify all of the different structures that we will need to deal with:
- Generating dates in a year
- Grouping dates by month
- Grouping dates by week
- Formatting the days in a week
- Grouping formatted weeks into months
- Laying out some number of months horizontally in a grid to form rows
- Outputting each line of each grid row
- Outputting all the rows
Each of the above tasks can be separated out into their own ranges: we can create an input range that generates all the dates in a year, then write an algorithm that, given a sequence of dates, breaks them up into chunks by month, then given a chunk of dates within a month, we can write an algorithm for grouping them by week, and so forth. This separation of tasks greatly simplifies the code within each component, and thus reduces the complexity of the code required and decreases the likelihood of bugs. For example, it's far easier to ensure you never put more than 7 days into a week if you have a single place where dates are grouped into weeks.
It is also far easier to write unittests for checking code correctness when the code is in small, manageable pieces: it's rather hard to write unittests for a giant outer loop that contains several levels of nested inner loops. We'd have no confidence that every possible code path was tested, because there are too many of them!
Then once we have all these components, we just need a little bit of glue code to piece them together to do what we want.
Let's walk through these components one by one, and show how we can write our calendar program in nice, reusable pieces.
Generating Dates in a Year
Our first task is to generate all the dates in a year. Thanks to D's std.datetime module, this is rather easy: create a Date object, then repeatedly add durations of 1 day to it. For our purposes, though, we can't just do this in a loop, because it has to interface with the other components, which do not have a matching structure to a loop over dates. So we capture this task of generating dates by encapsulating it within an input range. We could implement this range manually, but in the spirit of code reuse, we make use of D's Phobos standard library that provides convenient primitives for constructing ranges. Here is our implementation:
/**
* Returns: A range of dates in a given year.
*/
auto datesInYear(int year) {
return Date(year, 1, 1)
.recurrence!((a,n) => a[n-1] + dur!"days"(1))
.until!(a => a.year > year);
}
The recurrence() primitive lets us specify a range that's programmatically generated from an initial value. In this case, we start with the first day of the year, January 1st, represented as Date(year, 1, 1), then specify the relation that generates the next date in the sequence. This relation is specified as a lambda that takes a state vector a, representing the sequence generated so far, and an index n, and returns the date one day after the previous date, a[n-1]. (Note that in spite of the array indexing notation, recurrence is smart enough to only store as many previous dates as is necessary to generate the next one; in this case, only one previous date is stored. So there is no undue memory consumption caused by storing all dates in the year.) Thanks to D's std.datetime standard library module, the Date object automatically handles such details as the number of days in a month, leap years, etc., so this simple code is enough to generate all subsequent dates correctly.
The until() primitive lets us limit the range of generated dates to only dates within the specified year.
This is relatively straightforward; it is a typical example of a range implementation. For ease of usage, we have encapsulated it within a function that creates the range and returns it.
Grouping Dates by Month
Our next task on the list is to group dates by month. For maximum utility, we'd like to take a range of Dates, and break it up into subranges where all the Dates in a given subrange belong to the same month.
chunkBy
We could simply proceed and write this code directly; but in the spirit of code reuse, we note that this particular task is not really specific to dates. At its core, what we're doing is that given a range of items with properties x, y, z, we want to break the range up into subranges where all items in each subrange share the same value for a particular chosen property, say x. Or, to phrase it in terms of the Dates that we're dealing with, given a range of Dates, each of which consists of year, month, and day, we'd like to be able to select a particular property, such as month, and break the range up into subranges by that property (i.e., each subrange contains the dates for a single month). Since we don't know if, in the future, we might want to group Dates by year instead, the selection of which property to use should be parametrized.
But there is no reason why we should limit ourselves to using only properties of the item as the grouping criterion; that would not cover the case of, say, grouping Dates by week, since the Date object doesn't have a week field we may group by. So, boiled down to the essentials, what we're doing is to map each item to some value, be it selecting the month from a Date, or computing a function on a number, etc.. This then leads to the generic definition:
auto chunkBy(alias attrFun, Range)(Range r)
if (isInputRange!Range &&
is(typeof(
unaryFun!attrFun(ElementType!Range.init) ==
unaryFun!attrFun(ElementType!Range.init)
))
)
{
...
}
That is, given a range r and some function that maps items in the range to some value type that can be compared with the == operator, chunkBy() returns a range of subranges of the original range, such that all the items in each subrange is mapped by the function to the same value. This is expressed by the example usage below, which is a unittest in the actual code of the calendar program:
unittest {
auto range = [
[1, 1],
[1, 1],
[1, 2],
[2, 2],
[2, 3],
[2, 3],
[3, 3]
];
auto byX = chunkBy!"a[0]"(range);
auto expected1 = [
[[1, 1], [1, 1], [1, 2]],
[[2, 2], [2, 3], [2, 3]],
[[3, 3]]
];
foreach (e; byX) {
assert(!expected1.empty);
assert(e.equal(expected1.front));
expected1.popFront();
}
auto byY = chunkBy!"a[1]"(range);
auto expected2 = [
[[1, 1], [1, 1]],
[[1, 2], [2, 2]],
[[2, 3], [2, 3], [3, 3]]
];
foreach (e; byY) {
assert(!expected2.empty);
assert(e.equal(expected2.front));
expected2.popFront();
}
}
This unittest exemplifies the point that in component-style programming, we work with self-contained components with well-defined, straightforward interfaces, which makes them straightforward to implement, easy to test, and amenable to reuse.
Now we present the full definition of chunkBy():
auto chunkBy(alias attrFun, Range)(Range r)
if (isInputRange!Range &&
is(typeof(
unaryFun!attrFun(ElementType!Range.init) ==
unaryFun!attrFun(ElementType!Range.init)
))
)
{
alias attr = unaryFun!attrFun;
alias AttrType = typeof(attr(r.front));
static struct Chunk {
private Range r;
private AttrType curAttr;
@property bool empty() {
return r.empty || !(curAttr == attr(r.front));
}
@property ElementType!Range front() { return r.front; }
void popFront() {
assert(!r.empty);
r.popFront();
}
}
static struct ChunkBy {
private Range r;
private AttrType lastAttr;
this(Range _r) {
r = _r;
if (!empty)
lastAttr = attr(r.front);
}
@property bool empty() { return r.empty; }
@property auto front() {
assert(!r.empty);
return Chunk(r, lastAttr);
}
void popFront() {
assert(!r.empty);
while (!r.empty && attr(r.front) == lastAttr) {
r.popFront();
}
if (!r.empty)
lastAttr = attr(r.front);
}
static if (isForwardRange!Range) {
@property ChunkBy save() {
ChunkBy copy;
copy.r = r.save;
copy.lastAttr = lastAttr;
return copy;
}
}
}
return ChunkBy(r);
}
While this code is more complex than before, it is still relatively straightforward: the outer range iterates over the first elements of each subrange, and its .front method returns said subrange as a separate iterable object. Each subrange iterates over the source range until the criterion that defines that subrange is no longer satisfied, at which point the iteration ends. There are only a bare minimum of state variables (keeping track of the value of the function defining the current subrange), and no intricate interdependencies that are difficult to understand.
byMonth
Armed with chunkBy, grouping a range of Dates by month is now trivial to implement:
/**
* Chunks a given input range of dates by month.
* Returns: A range of ranges, each subrange of which contains dates for the
* same month.
*/
auto byMonth(InputRange)(InputRange dates)
if (isDateRange!InputRange)
{
return chunkBy!"a.month()"(dates);
}
And the accompanying unittest shows how this component can be put together with our first component, datesInYear(), to produce a range of dates in each month of a year:
unittest {
auto months = datesInYear(2013).byMonth();
int month = 1;
do {
assert(!months.empty);
assert(months.front.front == Date(2013, month, 1));
months.popFront();
} while (++month <= 12);
assert(months.empty);
}
We didn't verify that the dates within each month as yet -- we will get to that eventually -- but at this point it suffices to show how, by composing two components, we're now able to produce ranges of dates for each month of a year.
Grouping Dates by Week
Another piece of the puzzle of calendar layout is to be able to group a range of dates by week. This is also a relatively simple task, since for the purposes of our calendar we can regard each week as beginning on a Sunday and ending on a Saturday. Again, we write a function that takes a range of dates, and breaks it up into subranges by week:
/**
* Chunks a given input range of dates by week.
* Returns: A range of ranges, each subrange of which contains dates for the
* same week. Note that weeks begin on Sunday and end on Saturday.
*/
auto byWeek(InputRange)(InputRange dates)
if (isDateRange!InputRange)
{
static struct ByWeek {
InputRange r;
@property bool empty() { return r.empty; }
@property auto front() {
return until!((Date a) => a.dayOfWeek == DayOfWeek.sat)
(r, OpenRight.no);
}
void popFront() {
assert(!r.empty());
r.popFront();
while (!r.empty && r.front.dayOfWeek != DayOfWeek.sun)
r.popFront();
}
}
return ByWeek(dates);
}
This time, our code is simpler because weeks always end on a Saturday, which allows us to use the until() algorithm included in D's Phobos standard library.
This simplicity belies something very powerful that we have just achieved, though. If you notice, nowhere in the definition of byWeek() is there any reference to dates in a year, or dates in a month. It can be applied to both! If applied to the output of datesInYear(), for example, it would give us the dates of the year grouped by weeks in the year. If applied to one of the subranges returned by byMonth(), it would give us the dates of that month grouped by weeks, which is exactly what we need later on when we want to format the days in a month. So already, we can see that byWeek() is a reusable component that can be reused outside of the confines of our task at hand.
Another thing worth pointing out is that if a month starts in the middle of the week, byWeek() will return less than a full week in its first subrange, because it breaks the range up by Sundays, not necessarily by every 7 items in the range. The same thing goes for ranges that end in the middle of the week. So we have, indirectly, solved one of the key problems in calendar layout: the handling of partial weeks in a month. Yet, this solution does not involve tricky if-statements or other conditionals at all, just a straightforward iteration over a range of dates! We are now starting to see the power of component-style programming.
Formatting Days in a Week
Our next step is to take a range of dates in (possibly partial) weeks, and format them into string snippets suitable for assembling into our output lines later. In order to maximize the ways we can reuse this functionality, we return a range of string snippets that other code can then process in whatever way needed.
For the purposes of our example, we will forego runtime configurability in the output of our calendar program, and simply fix some constant parameters by which we will format the output:
/// The number of columns per day in the formatted output.
enum ColsPerDay = 3;
/// The number of columns per week in the formatted output.
enum ColsPerWeek = 7 * ColsPerDay;
These can be recast as runtime parameters, should we ever wish to increase the configurability of our calender program.
To help with code readability, we also write a little helper function that returns a string of n spaces, which we will use to add padding where it's needed:
/**
* Returns: A string containing exactly n spaces.
*/
string spaces(size_t n) {
return repeat(' ').take(n).array.to!string;
}
The actual code for formatting each week is relatively straightforward:
auto formatWeek(Range)(Range weeks)
if (isInputRange!Range && isInputRange!(ElementType!Range) &&
is(ElementType!(ElementType!Range) == Date))
{
struct WeekStrings {
Range r;
@property bool empty() { return r.empty; }
string front()
out(s) { assert(s.length == ColsPerWeek); }
body
{
auto buf = appender!string();
// Insert enough filler to align the first day with its respective
// day-of-week.
assert(!r.front.empty);
auto startDay = r.front.front.dayOfWeek;
buf.put(spaces(ColsPerDay * startDay));
// Format each day into its own cell and append to target string.
string[] days = map!((Date d) => " %2d".format(d.day))(r.front)
.array;
assert(days.length <= 7 - startDay);
days.copy(buf);
// Insert more filler at the end to fill up the remainder of the
// week, if it's a short week (e.g. at the end of the month).
if (days.length < 7 - startDay)
buf.put(spaces(ColsPerDay * (7 - startDay - days.length)));
return buf.data;
}
void popFront() {
r.popFront();
}
}
return WeekStrings(weeks);
}
Most of the work is done in the .front function, which handles partial weeks by computing how many missing days there are on each side, and inserting filler spaces in their place. The formatting of the days themselves is handled by using std.algorithm.map. The buffering of the output is handled by std.array.appender, which is a Phobos-provided component that gives an output range interface to a string buffer. All in all, this is pretty straightforward, and easy to test.
In the accompanying unittest, we couple formatWeeks with the other components we have built so far, to show off what we can achieve now:
unittest {
auto jan2013 = datesInYear(2013)
.byMonth().front // pick January 2013 for testing purposes
.byWeek()
.formatWeek()
.join("\n");
assert(jan2013 ==
" 1 2 3 4 5\n"~
" 6 7 8 9 10 11 12\n"~
" 13 14 15 16 17 18 19\n"~
" 20 21 22 23 24 25 26\n"~
" 27 28 29 30 31 "
);
}
As you can see, we can now format single months in a nice layout, simply by chaining together existing components. In the completed calendar program, there is currently no functionality for displaying only a single month, but as this unittest shows, it would be trivial to implement.
Another important thing to note about this unittest, is that formatWeek() returns a range of strings to be output; it is not at all tied down to printing the entire month in one shot. We will make use of this fact when we do our final calendar layout, by consuming the first lines of each month in a row of months first, then the second lines of each month in the row, etc., and thus achieve a line-by-line output to the terminal that doesn't require a grid buffer.
But let's not get ahead of ourselves. First, let's officially put the functionality already demonstrated by the above unittest into a form that can be reused.
Formatting Months
In addition to printing out the days in the month, we want to print the name of the month at the top, so that we know which month the block of formatted days belongs to. For cosmetic purposes, we center the name of the month horizontally over its days:
/**
* Formats the name of a month centered on ColsPerWeek.
*/
string monthTitle(Month month) {
static immutable string[] monthNames = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
];
static assert(monthNames.length == 12);
// Determine how many spaces before and after the month name we need to
// center it over the formatted weeks in the month
auto name = monthNames[month - 1];
assert(name.length < ColsPerWeek);
auto before = (ColsPerWeek - name.length) / 2;
auto after = ColsPerWeek - name.length - before;
return to!string(spaces(before) ~ name ~ spaces(after));
}
Next, we write the function that formats a month by returning a range containing the month name followed by the formatted weeks in the month:
/**
* Formats a month.
* Parameters:
* monthDays = A range of Dates representing consecutive days in a month.
* Returns: A range of strings representing each line of the formatted month.
*/
auto formatMonth(Range)(Range monthDays)
if (isInputRange!Range && is(ElementType!Range == Date))
{
assert(!monthDays.empty);
assert(monthDays.front.day == 1);
return chain(
[ monthTitle(monthDays.front.month) ],
monthDays.byWeek().formatWeek());
}
Thanks to the reusable components we have built, this is quite simple.
To make formatMonth easier to use in the final code, we wrap it in a function that applies it to each month in a range of months:
/**
* Formats a range of months.
* Parameters:
* months = A range of ranges, each inner range is a range of Dates in a
* month.
* Returns:
* A range of ranges of formatted lines for each month.
*/
auto formatMonths(Range)(Range months)
if (isInputRange!Range && is(ElementType!(ElementType!Range) == Date))
{
return months.map!((month) => month.formatMonth());
}
Again, we note that formatMonths does not assume anything about the number of months it is given to format; we may pass in all 12 months in the year, or any subset thereof. In particular, in the next step, we will be grouping months into grid rows, and passing each row to formatMonths to obtain the range of formatted lines of the months in the row. The way formatMonths is designed makes it reusable in a wide variety of situations.
Formatting a Row of Months
Now we come to the most crucial piece of our calendar program: the code that formats a row of months in the grid of the final calendar output. Here is where we will take the ranges of formatted lines for each month, and paste them together horizontally to form the output lines to the terminal. Will this require some tricky loops, or clever if-statements, to achieve?
Actually, with the components we have built so far, this part of the code is almost disappointingly straightforward. Again, we note that the task of pasting together rectangular blocks of string snippets is not a calendar-specific problem; it is a general problem that can be applied to any block of any string snippets. Hence, we cast our next component in generic terms:
/**
* Horizontally pastes a forward range of rectangular blocks of characters.
*
* Each rectangular block is represented by a range of fixed-width strings. If
* some blocks are longer than others, the shorter blocks are padded with
* spaces at the bottom.
*
* Parameters:
* ror = A range of of ranges of fixed-width strings.
* sepWidth = Number of spaces t 7 - startDay)
* Returns:
* A range of ranges of formatted lines for each month.
*/
auto pasteBlocks(Range)(Range ror, int sepWidth)
if (isForwardRange!Range && is(ElementType!(ElementType!Range) : string))
{
struct Lines {
Range ror;
string sep;
size_t[] colWidths;
bool _empty;
this(Range _ror, string _sep) {
ror = _ror;
sep = _sep;
_empty = ror.empty;
// Store the widths of each column so that we can insert fillers if
// one of the subranges run out of data prematurely.
foreach (r; ror.save) {
colWidths ~= r.empty ? 0 : r.front.length;
}
}
@property bool empty() { return _empty; }
@property auto front() {
return
// Iterate over ror and colWidths simultaneously
zip(ror.save, colWidths)
// Map each subrange to its front element, or empty fillers if
// it's already empty.
.map!((a) => a[0].empty ? spaces(a[1]) : a[0].front)
// Join them together to form a line
.join(sep);
}
/// Pops an element off each subrange.
void popFront() {
assert(!empty);
_empty = true; // assume no more data after popping (we're lazy)
foreach (ref r; ror) {
if (!r.empty) {
r.popFront();
if (!r.empty)
_empty = false; // well, there's still data after all
}
}
}
}
static assert(isInputRange!Lines);
string separator = spaces(sepWidth);
return Lines(ror, separator);
}
This is again an input range, with rather straightforward implementation. The .front method is where the pasting of each corresponding line of the input months into a single line is implemented.
There's really only one minor complication: how to deal with months that occupy a different amount of vertical space due to the dates falling in such a way that they cover a smaller number of (possibly partial) weeks. To handle this, we simply insert a row of blank spaces in place of a week, when a subrange runs out before other subranges. For maximum reusability, we make no assumptions about the width of the blocks; so we use an array to keep track of the widths of each column and use those widths for inserting the blank spaces where necessary.
Actually, the astute reader will have noticed that pasteBlocks doesn't even use any of our previously built components. It's a completely generic component that can be used in a wide variety of other programs! Only its accompanying unittest actually puts it together with our other calendar-specific components in a demonstration of our achievement so far:
unittest {
// Make a beautiful, beautiful row of months. How's that for a unittest? :)
auto row = datesInYear(2013).byMonth().take(3)
.formatMonths()
.array()
.pasteBlocks(1)
.join("\n");
assert(row ==
" January February March \n"~
" 1 2 3 4 5 1 2 1 2\n"~
" 6 7 8 9 10 11 12 3 4 5 6 7 8 9 3 4 5 6 7 8 9\n"~
" 13 14 15 16 17 18 19 10 11 12 13 14 15 16 10 11 12 13 14 15 16\n"~
" 20 21 22 23 24 25 26 17 18 19 20 21 22 23 17 18 19 20 21 22 23\n"~
" 27 28 29 30 31 24 25 26 27 28 24 25 26 27 28 29 30\n"~
" 31 "
);
}
Formatting a Year
At this point, we're pretty much done. All that's left is to put all the pieces together to format all the rows in the grid of months for a year:
/**
* Formats a year.
* Parameters:
* year = Year to display calendar for.
* monthsPerRow = How many months to fit into a row in the output.
* Returns: A range of strings representing the formatted year.
*/
auto formatYear(int year, int monthsPerRow)
{
enum colSpacing = 1;
return
// Start by generating all dates for the given year
datesInYear(year)
// Group them by month
.byMonth()
// Group the months into horizontal rows
.chunks(monthsPerRow)
// Format each row
.map!((r) =>
// By formatting each month
r.formatMonths()
// Storing each month's formatting in a row buffer
.array()
// Horizontally pasting each respective month's lines together
.pasteBlocks(colSpacing)
.join("\n"))
// Insert a blank line between each row
.join("\n\n");
}
It's worth pointing out that even though we store each month's formatting in an array, this is an array of ranges of each month's formatting. Each line of the formatting is lazily generated when .front is invoked; there is no memory overhead incurred by storing all formatted lines of the months in a row before we start producing output lines. If we were to trace through the execution of the program, we will find that the formatted lines of each month are generated on-the-fly as each output line is being assembled. And indeed, formatMonth() returns an input range, not a forward range or higher, and pasteBlocks does not cache any of the generated string snippets. This lazy evaluation is very similar to the way functional languages work, and is an example of D's support for functional-style code.
For maximum flexibility, we have written formatYear in such a way that it returns an input range of strings representing the lines of the final, formatted calendar. Thus, one could reuse it ways outside the scope of the present example; for example, using pasteBlocks to paste multiple calendars together horizontally.
To hook this up to the outside world, we write a simple main() function that lets the user specify which year the calendar should be formatted for:
int main(string[] args) {
// This is as simple as it gets: parse the year from the command-line:
if (args.length < 2) {
stderr.writeln("Please specify year");
return 1;
}
int year = to!int(args[1]);
// Print the calender
enum MonthsPerRow = 3;
writeln(formatYear(year, MonthsPerRow));
return 0;
}
And that's it. The output of the program is exactly the output displayed at the beginning of this article.
Conclusions
The calendar program that we wrote above proves that component-style programming using the range concept is a very powerful approach to programming:
- It allows us to untangle very complex code into straightforward, manageable pieces.
- It produces components that are highly-reusable outside of their original purpose: in fact, chunkBy is a candidate for inclusion in Phobos.
- It is easy to write, easy to read, and easy to test, and hence much less prone to bugs. The formatYear function is a prime example of how the code is easy to read: it starts with the dates in a year and successively transforms it until it arrives at the final output. There are no convoluted loops, hard-to-understand state variables and complicated if-statements. Yet it is able to express the level of complexity required to format a yearly calendar.
- New features can be easily added by putting the components together in a different way. For example, we can extend main() to allow printing just a single month instead of the whole year.
It is also the first calendar formatting program, to my knowledge, that doesn't look frighteningly similar to an entry to the IOCCC. :-)
Acknowledgements
Many thanks to Andrei Alexandrescu for his insightful article On Iteration where the concept of ranges was first developed, to Walter Bright for his excellent article on component programming in D and for creating the D programming language in the first place, to Timon Gehr for providing a more concise implementation of datesInYear(), and to all D forum members who gave helpful feedback and encouraged me to turn my original forum posting into this article.
Thanks also go to my former professor Gunnar Gotshalks who first introduced me to Jackson Structured Programming, which gave me a deep insight into how mismatches between program structure and data structure (or between two or more data structures) is the source of much of the complexity of code.
Appendix
The full source code for the calendar program developed in this article is available on github.
The exact version of the code used in this article is here.