As an exercise, I've made a text processing utility that wraps arbitrarily long lines of text, but only using fixed-size arrays and other basic C features.
I have mostly programmed in Python, C++ and Java so far, and I recently picked up K&R (second edition) to learn some C. I have only gone through the first chapter so far, which is "A Tutorial Introduction". At the end of this chapter, the last paragraph says:
At this point we have covered what might be called the conventional core of C. With this handful of building blocks, it's possible to write useful programs of considerable size, and it would probably be a good idea if you paused long enough to do so. These exercises suggest programs of somewhat greater complexity than the ones earlier in the chapter.
Since I have quite a bit of time on my hands, I took this "challenge" quite literally: I set out to solve the exercises strictly using only the language features shown in that chapter.
One of these exercises (exercise 1-22) reads:
Write a program to "fold" long input lines into two or more shorter lines after the last non-blank character that occurs before the \$n\$-th column of input. Make sure your program does something intelligent with very long lines, and if there are no blanks or tabs before the specified column.
Probably the best way to deal with arbitrarily long input lines would be to use dynamically allocated character arrays, but the above restriction implies I can only use fixed-size arrays.
This took quite a bit more time than I was expecting, and the end result works, but the algorithm is a bit of a mess; this is why I'm asking for a code review. My main concern is the correctness of the algorithm and whether it can be made simpler, but anything is welcome. Next up would be possible refactorings, then readability, and then code style, probably.
Specification
Here's my specific interpretation of this exercise:
Make a program that, for each line of input:
- if the line spans a number of columns less than or equal to the maximum line length, print it unaltered;
- alternatively, if it exceeds the limit but it doesn't contain any blank separators, also print it unaltered;
- otherwise, split the line immediately before the last blank separator that occurs before the length limit, or, in its absence, the first blank separator; then:
- print the first part
- treat the remainder as if it were a new input line (i.e. apply this algorithm recursively)
Definitions
For the purposes of this question, define:
- blank character: either a space
\t
character - blank separator: any sequence of blank characters that comes immediately after a non-blank character (note that this excludes leading space)
- tab size: number of columns between tab stops; fixed at 4
- maximum line length: an arbitrary positive integer that determines the desired maximum number of columns in an output line
Examples
These examples are with the maximum line length (i.e. max columns) set to 10; I've added a vertical bar |
at the 11th column just to make it easier to visualize, but this character isn't actually there in the input or output shown below. Also note that , for the last two lines of input, some of the blank characters are tabs.
Input:
aaaaaaaa |
bbbbbbbbbb|
cccccccccc|ccc
dd dd dd d|d dd dd
eeeeeeeeee|eee eeee
ffffffffff| fffffffff
gggggggggg| ggggggggg gg ggg
hhh hhhhhh|hhhhhhhhhh
iiiiii| iiii iii
jjjjjj jj|jjjjjjjjjjjjjjjjjj
Output:
aaaaaaaa |
bbbbbbbbbb|
cccccccccc|ccc
dd dd dd |
dd dd dd |
eeeeeeeeee|eee
eeee |
ffffffffff|
fffffffff|
gggggggggg|
ggggggggg|
gg ggg |
hhh |
hhhhhhhhh|hhhhhhh
iiiiii|
iiii |
iii |
jjjjjj |
jjjjjj|jjjjjjjjjjjjjj
Implementation restrictions
Due to what I've said about only using features seen in the first chapter, I had to reinvent a few wheels. Here is a concrete list of restrictions I followed (which, if you propose any code, you should follow too).
I can only use the following:
- variables, numeric literals, arithmetic expressions, equality operators, boolean operators
while
loop andfor
loop- symbolic constants (i.e.
#define
macros with a constant value) getchar
andputchar
printf
- string and character constants
if
statement- fixed-size arrays
- functions
- character arrays
- external variables (i.e. global variables)
Now, I allowed myself some exceptions that don't significantly alter the challenge, just to make life slightly easier and avoid reinventing too many wheels:
- I can use
do
-while
loops - I can use
strcpy
andstrlen
- I can use
puts
- I can use
<stdbool.h>
- I used VLAs to be able to pass the maximum line size as an argument, defining the maximum line length as a global constant (instead of a symbolic constant), which are not technically "fixed-size arrays" (and weren't even a thing when the book was published), but the runtime size of the arrays I use is always the same throughout the lifetime of the program, so I considered this as valid.
My algorithm
This is the algorithm to deal with a single line of input which I ended up implementing. It takes into account that I can only use arrays of a fixed size.
The main idea is to only read at most \$M\$ columns of input at a time, where \$M\$ is the maximum line length, deal with that segment individually, and move on to the next one.
Code
One final disclaimer before you go on to read the code. I'm mainly using camelCase
as a naming convention. I know this is not conventional in C, but this is only an exercise for myself, so I just picked the naming convention I find the nicest.
Function that implements the algorithm
The function that implements the algorithm above is int readLineAndPrintFolded(const int maxLength, const int tabSize)
in the code below, at the very bottom, before main
. I recommend you start reading from there—the rest are helper functions and their name pretty much describes what they do.
Complete, runnable code
You can copy and paste this in a text file, compile it under the C99 standard, and run it.
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <ctype.h>
const int TAB_SIZE = 4;
const int MAX_LINE_LENGTH = 10;
/**
* Compute the number of spaces needed to substitute a tab character.
* @param column – zero-indexed index of the column in which the tab character starts
* @param tabSize – number of spaces per tab
* @return the number of spaces that, if inserted in place of a tab character, would
* reach the same column as the tab.
*/
int spacesToNextTabStop(int column, int tabSize) {
return tabSize - (column % tabSize);
}
/**
* Compute the number of columns spanned by a string.
* @param string – string of which to compute the column length
* @param tabSize – number of spaces per tab stop
* @return the number of columns spanned by a string if printed at the start of a line
*/
int columnLength(const char string[], const int tabSize) {
int col = 0;
for (int i = 0; string[i] != '\0'; ++i)
col += (string[i] == '\t') ? spacesToNextTabStop(col, tabSize) : 1;
return col;
}
/**
* Split a string into two segments, discarding the middle
* If cutFrom or cutTo ar invalid indices, or cutFrom > cutTo, no split is performed.
*
* @param string – string to split
* @param len – length of the string
* @param cutFrom – start of the middle section to split on
* @param cutTo – end of the middle section to split on
* @param left – destination for left segment
* @param right – destination for right segment
* @return whether a split was performed
*/
bool
split(const char string[], int len, int cutFrom, int cutTo, char left[], char right[]) {
if (!(0 <= cutFrom && cutFrom < len && 0 <= cutTo && cutTo < len
&& cutFrom <= cutTo))
return false;
// copy each part into the corresponding destination
for (int i = 0; i < cutFrom; ++i)
left[i] = string[i];
left[cutFrom] = '\0';
for (int i = cutTo; i < len; ++i)
right[i - cutTo] = string[i];
right[(len - cutTo)] = '\0';
return true;
}
/**
* Partition a string in two, splitting immediately before the last blank sequence
* that appears after a non-blank character.
*
* If no such blank is found, does nothing.
*
* @param string – original string
* @param left – destination for the first part of the string
* @param right – destination for the second part of the string
* @return whether a split was performed
*/
bool splitBeforeLastBlankSeparator(const char *string, char *left, char *right) {
size_t len = strlen(string);
// compute the split boundary
int boundary = -1;
int i = 0;
while (i < len) {
if (isblank(string[i])) {
boundary = i;
while (isblank(string[i])) ++i;
}
else ++i;
}
boundary = (boundary == 0) ? -1 : boundary; // don't split on leading space
// perform the split
return split(string, len, boundary, boundary, left, right);
}
/**
* Partition a string in two, splitting immediately before the first blank sequence.
*
* If no blank is found, does nothing.
*
* @param string – original string
* @param left – destination for the first part of the string
* @param right – destination for the second part of the string
* @return whether a split was performed
*/
int splitBeforeFirstBlank(const char *string, char *left, char *right) {
size_t len = strlen(string);
// compute the split boundary
int boundary = -1;
for (int i = 0; i < len; ++i) {
if (isblank(string[i])) {
boundary = i;
break;
}
}
// perform the split
return split(string, len, boundary, boundary, left, right);
}
/**
* Get a line of input, up to (but not including) the next newline character or EOF.
*
* Reads at most `lim` columns (excluding the newline, if present)
*
* @param destination Buffer in which to store the input characters;
* must have space for `lim + 1` characters
* @param lim Maximum numbers of columns to read
* @param tabSize number of spaces per tab
* @return Zero if a full line was read before reaching lim, 1 if it was reached,
* EOF if there is no input to be read
*/
int getLineContents(char *destination, int lim, int tabSize) {
int col = 0, i = 0;
int c;
while (col < lim && (c = getchar()) != EOF && c != '\n') {
destination[i++] = (char) c;
col += (c == '\t') ? spacesToNextTabStop(col, tabSize) : 1;
}
destination[i] = '\0';
return (i == 0 && c == EOF) ? EOF : col == lim;
}
/**
* Read a line from input and output it folded format
* Each line in the output will be at most maxLength characters long, where
* there is the possibility to split (i.e. blank).
*
* @param maxLength maximum length of lines (in columns)
* @return the number of output lines
*/
int readLineAndPrintFolded(const int maxLength, const int tabSize) {
const int charArraySize = maxLength + 1;
int lines = 0;
// get first segment
char segment[charArraySize];
int hasMore = getLineContents(segment, maxLength, tabSize);
if (hasMore == EOF) return lines; // nothing to be read
// while line exceeds limit, print current output line and start another
char beforeCut[charArraySize], afterCut[charArraySize];
while (hasMore == 1) {
// find blank to split on
if (!splitBeforeLastBlankSeparator(segment, beforeCut, afterCut)) {
// while there is no blank to split on,
// append segment (without ending the line) & reread
do {
if (hasMore == 1) printf("%s", segment);
else {
// line is finished without having found any blank
puts(segment);
return ++lines;
}
hasMore = getLineContents(segment, maxLength, tabSize);
} while (!splitBeforeFirstBlank(segment, beforeCut, afterCut));
}
// print the fragment before the blank and start continuation line
puts(beforeCut);
++lines;
// deal with the fragment after the cut:
int remainderCols = columnLength(afterCut, tabSize);
// copy the fragment after the cut into the beginning of the new segment
strcpy(segment, afterCut);
// read into the rest of segment until full
if (hasMore == 1)
hasMore = getLineContents(&segment[strlen(afterCut)],
maxLength - remainderCols, tabSize);
}
// print last sub-line
puts(segment);
return ++lines;
}
int main() {
while (readLineAndPrintFolded(MAX_LINE_LENGTH, TAB_SIZE) > 0);
}
Please help me, my task-completion-obsessed brain won't let me go on to the next chapter until I finish all exercises!