6
\$\begingroup\$

My attempt at Exercise 1-21 in the The C Programming Language book written by Brian Kernighan and Dennis Ritchie.

Exercise Prompt

Write a program entab that replaces strings of blanks by the minimum number of tabs and blanks to achieve the same spacing.

sample.txt

    apple   banana                  pear

Expected Output with Test Text

\tapple\tbanana\t\t\tpear

Code

//Exercise 1-21. Write a program entab that replaces strings of blanks by the minimum
//number of tabs and blanks to achieve the same spacing. Use the same tab stops as for detab.
//When either a tab or a single blank would suffice to reach a tab stop, which should be given
//preference?

#include <stdio.h>

#define CHARMAX 1000    // max chars per line
#define LINEMAX 1000    // max total number of lines
#define TABSTOP 8


int main() {
    int ch = 0;
    size_t charCount = 0;   // running total of characters
    size_t lineCount = 1;   // running total of lines
    size_t space_count = 0; // running count of spaces

    while ((ch = getchar()) != EOF) {
        charCount++;
        if (ch == '\n') {
            lineCount++;
            charCount = 0;
        }
        if (lineCount > LINEMAX) {
            printf("\n!PROGRAM HALTED -MAX LINES REACHED- MAXIMUM NUMBER OF LINE ALLOWED IS %d!\n", LINEMAX);
            break;
        }
        if (ch == ' ') {
            while ((ch = getchar()) == ' ') {
                space_count++;
                if (space_count % TABSTOP == 0) {
                    putchar('\t');
                    space_count = 0;
                }
            }
            if (space_count != 0) {
                putchar('\t');
            }
        }
        if (charCount < CHARMAX) {
            putchar(ch);
        } else {
            while ((ch = getchar()) != '\n') {
                ;
            }
            putchar(ch);
            lineCount++;
            charCount = 0;
        }
    }
}
```
\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

Thank you for putting your work out there, it's brave of you! That is how we improve.


The motivation for CHARMAX / LINEMAX is unclear, given that they are not part of the problem specification. Gold star for giving each magic number a manifest constant name.

The usual motivation for CHARMAX (or MAX_LINE_LENGTH, to distinguish from total document length) would be to allow a get_line() function to get away with static allocation instead of having to malloc().

The LINEMAX just kind of mystifies me. I mean, what if $ yes | entab processes an unbounded number of lines? Is it somehow "correct" to bail after a while? Or should the pipeline have looked like $ yes | head -${LINEMAX} | entab if we wanted finite output?


    size_t charCount = 0;   // running total of characters
    size_t lineCount = 1;   // running total of lines
    size_t space_count = 0; // running count of spaces

Pick a coding standard. There's lots to choose from, maybe GNU or Google. And then stick with it. In particular, avoid arbitrarily mixing camelCase with snake_case.

Kudos, the identifiers are wonderfully descriptive.

Consider deleting the // comments, as they don't really tell us anything we didn't already know.


        } else {
            while ((ch = getchar()) != '\n') {
                ;
            }

tiny style nit: The ; semicolon on a line by itself is great, it calls attention to "this is a no-op!". But it's maybe a bit weird within { } braces. Use one or the other. This comes back to: pick a style guide, write down which one, and adhere to it. Which is a lot easier if you routinely use a linter or a code reformatter which implements that guide.

Bigger item: We're discarding characters till end-of-line? I don't find that in the original spec. Ok, fine, maybe, write down the revised spec that you are implementing. One can only report a code defect ("bug") with respect to a spec. Missing specs cause endless misery.

Again, the usual motivation for such line truncation would revolve around static allocation for a line buffer. But your code makes no such allocation, so it just kind of seems like gratuitous data deletion.


Here's a bigger critique. Your loop features four if clauses, some with dependent clauses. And there's quite a lot of mutation going on, the most important being consuming input characters.

I don't find it especially easy to reason about invariants in this code.

Consider introducing a helper function that is responsible for processing exactly one line of input text. (For one thing, that makes writing unit tests way easier.) Let's define a term.

def: We are at start-of-line when at start of input file, and also right after we've seen a \n newline input character.

The outer loop invariant would be "we are at start-of-line each time the while loop begins again", and helper would have its own inner loop which is responsible for making the invariant true. That is, it's responsible for consuming a full line and returning control to the outer loop.

Here's an example document for a unit test.

sp16 = " " * 16
doc = f"{sp16}A{sp16}B"

expected_output = "\t\tA\t\t B"

I don't find it especially easy to reason about space_count OBOB issues given the current structure.


Maybe the input is specified as having no TAB characters?

If so, we should signal an error upon encountering one.

If not, maybe reset space_count to zero?


// When either a tab or a single blank would suffice to reach a tab stop, which should be given preference?

It seems the problem author was inviting an explicit response to this, perhaps in the form of a comment.


Overall?

You seem to have adopted some novel requirements that don't appear in the problem statement.

Running with those new requirements, the code appears to implement them correctly.

There is an opportunity to structure the code into smaller chunks, each having greater clarity for future maintainers who will attend to bug fixes and feature enhancements.

\$\endgroup\$
3
\$\begingroup\$

Infinite loop

Should a long and last line of input lack a final '\n', the below is an infinite loop.

        while ((ch = getchar()) != '\n') {
            ;
        }

Better to quit the loop if a '\n' or EOF detected.

Unnecessary CHARMAX, LINEMAX

Neither LINEMAX nor CHARMAX are needed to fulfill coding goal.

Scant Tests

Code deserves test cases where the space first occurs on line offsets%8 of 0, 1, 7 with lengths spaces runs lengths of 1 to 9.

Also test cases of lines starting with spaces of various lengths.

Consider also last lines that do not end with a '\n'.

Research freopen() for a way to automate testing of stdin via a file.

For DEBUG mode, consider any space that is ever printed is replaced with a '.' to ease visual testing.

I suspect a bug.

\$\endgroup\$
3
  • \$\begingroup\$ I noticed that Vim added \n at the end of every line after writing the buffer. I think I will edit my text files within my IDE to avoid this. \$\endgroup\$
    – Eric
    Commented Jan 24, 2023 at 11:25
  • \$\begingroup\$ Happen to have any articles to share re: Research freopen() for a way to automate testing of stdin via a file.? Having a bit of trouble finding quality info on this. \$\endgroup\$
    – Eric
    Commented Jan 25, 2023 at 11:27
  • \$\begingroup\$ @iamericfletcher simple example. Else you need a more specific request. \$\endgroup\$ Commented Jan 25, 2023 at 14:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.