A (sort of) live sample
Sample Problem Statement
Your boss says, "I need a program next week, um, that will read lines from the keyboard and, ah, reverse each end-to-end then print both lines. Oh, unless the line starts with #, then just print it, eh?"
Questions for the boss:
Q: how many lines?
A: As many as there are.
Q: how long are the lines?
A: Oh, whatever. Probably 80 characters or so.
Q: what language?
A: it doesn't matter.
Q: Just #?
A: I guess so, for now anyway.
Q: only from the keyboard?
A: Yeah, or a short file I suppose.
Q: anything else?
A: Not that I can think of just now.
You should be getting a little worried about now, since the boss is both unsure of what he wants and unclear about what little he's sure of. If you don't want to brush up your résumé just yet, you should be planning how to meet … somewhat fluid requirements.
Bad approach
Just write the code -- how hard can it be?
#include <stdio.h>
int main(int argc, char *argv[])
{
char buf[80];
char reverse[80];
int i, j;
/* add filename stuff later */
gets(buf);
if (buf[0] != '#')
{
puts(buf);
j = 80;
for (i = 0; i < 80; i++)
reverse[j--] = buf[i];
puts(reverse);
}
return 0;
}
What's wrong with this code? After all, it compiles OK.
Where to begin? First, it won't work. Second, it doesn't meet the requirements. Third, it assumes no errors. Fourth, it's incomplete. Fifth, you didn't learn from your Q&A session. Sixth, a clean compile means little if the program logic is flawed. And seventh, it ... it's been done backwards and upside down. And so on.
Better approach
Do some planning and checking.
Requirement statement
The C program 'rv' will read lines up to 80 characters long from stdin or from a filename on the command line on a system in the Linux Lab, T127. It will print out each line, and if the line does not begin with '#' it will print the line again reversed end-to-end until end-of-file is reached. It will be available next Wednesday by 4:00.
Test Plan:
|
Test |
Test input |
Expected output |
Why test this? |
|
1 |
abcd |
abcd |
short string |
|
2 |
#ghi |
#ghi |
# start |
|
3 |
mnopqrstuvw |
mnopqrstuvw |
longer string |
|
4 |
#rstuvwxyz |
#rstuvwxyz |
# but longer |
Check requirements with boss
"Yeah, that looks good. But I need it Tuesday at noon, and I'd like some blank lines in there to separate each input line. Oh, and don't reverse lines that start with '//' either, I guess. Any not just the Lab, any Linux system."
Oh, my! Aren't you glad you asked? I wonder what else …
Revised requirement statement
The C program 'rv' will read lines up to 80 characters long from stdin or from a filename on the command line on a Linux system. It will print out each line, and if the line does not begin with '#' or '//' it will print the line again reversed end-to-end, and follow it by a blank line, until end-of-file is reached. It will be available next Tuesday at noon.
Test Plan:
|
Test |
Test input |
Expected output |
Why test this? |
|
1 |
abcd |
abcd |
short string |
|
2 |
#ghi |
#ghi |
# start |
|
3 |
mnopqrstuvw |
mnopqrstuvw |
longer string |
|
4 |
//rstuvwxyz |
#rstuvwxyz |
// start |
Check revision with boss
"Yeah, that looks OK for now."
That's good, then. We'll just go ahead then … but with some care, OK?
External design
Input files
· stdin or a named file
Input fields
· an 80 character buffer, plus space for
newline and the
end-of-string marker
Output files
· stdout
Output fields
· input buffer unless the input is from stdin
· input buffer, reversed end-to-end
· blank line between input lines
Error files
· stderr
Error messages
· "too many arguments"
· "file filename open error"
· "line too long, ignored"
User document
The 'rv' program will read lines up to 80 characters long from the keyboard or from a named file (that is, 'rv filename') on any Linux system. It will print out each line if the input is not from the keyboard, and if the line does not begin with '#' or '//' it will print the line again reversed end-to-end until end-of-file (Control-D from the keyboard) is reached. Each line's output will be separated from the next by a blank line.
Install document
Have the root user copy the executable program 'rv' into /usr/local/bin with permissions 0755 (rwxr-xr-x). Make sure your $PATH includes this directory, or ask the sysadmin to alter your ~/.bash-profile as necessary.
Test Plan
I keyboard
1. input: nothing (^D with no lines)
output: none, just exit
why: test end-of-file handling
2. input: empty line (return only)
output: 3 blank lines (input, reverse, blank)
why: test that a null input line is handled
3. input: line abcd
output: line dcba plus blank line
why: test simple reversal
4. input: line #abcd
output: blank line
why: test no output after # line (no reversal)
5. input: line //abcd
output: blank line
why: test no output after // line (no reversal)
6. input: lines x#abcd, x//defg
output: lines dcba#x,blank line, gfed//x,blank line
why: normal reversals when # and // not first characters
7. input: lines 012…8, #12…8, //2…8 (all lines 79 char)
output: lines 8…210,blank, #12…8,blank //2…8, blank
why: just under the 80 character limit
8. input: lines 012…9, #12…9, //2…9 (all lines 80 char)
output: lines 9…210,blank, #12…9,blank, //2…9,blank,
why: just at the 80 character limit
9. input: lines 012…0, #12…0, //2…0 (all lines 81 char)
output: "line too long, ignored" and blank line 3
times;
short line ("0\n") ignored
why: just over the 80 character limit
II file input
10. input: rv missing-file
output: "file missing-file open error" and exit
why: test open error handling
11. input: two filenames
output: "too many arguments" and exit
why: test too many arguments
12. input: empty file
output: none and exit
why: immediate end-of-file
13. input: file of 6 lines, copies of tests 2 to 6 above
output: input and exactly the same as from the keyboard
why: file and keyboard behaviour must be identical
14. input: file of 9 lines, copies of tests 7 to 9 above
output: input and exactly the same as from the keyboard
why: file and keyboard behaviour must be identical
Review with boss
"Yeah, that's OK I guess, but are you sure you need that much testing for such a simple little program?"
There's a good reason Dilbert refers to his boss as a PHB!
Internal design
Data dictionary
· Buffer large enough to read a line of 80 characters plus newline plus end-of-string
#define BUF (80 + 2)
char buf[BUF];
· NULL-terminated (i.e., expandable) list of exception strings for lines that are not to be reversed
char *except[] = { "#", "//", NULL };
Functions
#define TRUE 1
#define FALSE 0
int get_line(char *buf, FILE *file);
Read an input line, skipping those longer than BUF - 1. That is, 80 characters of text plus newline. Return FALSE for end-of-file, otherwise TRUE (i.e., a valid line exists in buf).
int if_rev(char *buf, char *except[]);
Check the start of the line in buf[] for each of the strings in except[] until either a match or the NULL entry is reached in except[]. Return TRUE if no except[] entry is found (i.e., do_rev() is to be done), else FALSE.
void do_rev(char *buf);
Reverses the buffer in place but leaves the newline and end-of-string alone. No return code, no error messages. Argument is buffer to reverse on entry, returns reversed buffer as output.
Algorithm is to swap the first and last characters, moving toward centre of the string until all are done (except the central character if the string less newline is an odd length).
Build process
Use a Makefile or equivalent:
rv: rv-main.o get_line.o if_rev.o do_rev.o
è gcc -o rv rv-main.o get_line.o
if_rev.o
do_rev.o
rv-main.o: rv-main.c rv.h
è gcc -c rv-main.c
get_line.o: get_line.c rv.h
è gcc -c get_line.c
if_rev.o: if_rev.c rv.h
è gcc -c if_rev.c
do_rev.o: do_rev.c rv.h
è gcc -c do_rev.c
clean:
è rm *.o rv
backup:
è cp *.c *.h Makefile bak
Note: è indicates a required TAB character
Code management
· directory ~/work/rv for .c and .h files, and the Makefile.
· directory ~/work/rv/bak for regular backups
Test plan update
Now that a reversal method has been chosen, make sure it works for both odd- and even-length lines with additional tests:
15. input: lines 1234, 12345,from keyboard
output: 4321, blank line, 54321, blank line
why: test both even- and odd-length reversals
16. input: repeat test 15 from filefrom file
output: input and exactly the same as from the keyboard
why: file and keyboard behaviour must be identical
PDL
· rv-main.c
IF more than 1 argument
PRINT error to STDERR
EXIT with error
ELSE if exactly 1 argument
OPEN file
IF error on open
PRINT error to STDERR
EXIT with error
ENDIF
ELSE
SET STDIN as file to use
ENDIF
WHILE not end-of-file from file get next line
IF file is not STDIN
PRINT input line
ENDIF
IF line is to be reversed
PRINT reverse of line
PRINT blank separator line
ENDIF
END WHILE
IF file not STDIN
CLOSE file
ENDIF
EXIT
· get_line.c
READ a line
IF end-of-file
RETURN FALSE
WHILE line does not end with newline
PRINT error to STDERR
DO remove trailing parts
READ rest of input
IF end-of-file
RETURN FALSE
WHILE line does not end with newline
READ next line
IF end-of-file
RETURN FALSE
END WHILE
RETURN TRUE
· if_rev.c
FOR each item in exception list
IF this entry matches start of line
RETURN FALSE
END FOR
RETURN TRUE
· do_rev.c
WHILE not at the middle of the line
EXCHANGE 2 characters from either end
MOVE pointers towards the centre
END WHILE
RETURN
CODE
Except for rv.h, file headers are omitted from all .c files for the sake of clarity, and to fit the code onto overhead pages more easily. They may not be omitted in actual programs.
/**********************************************/
/* File: rv.h Version 1.1 */
/* Author: R. Allison 040-xxx-xxx */
/* Course: CST8229 Section 091 */
/* Assignment 0, Demo Program "reverso" */
/* Due: 01 Jan 2001 Submitted 01 Jan 2001 */
/* Professor: Robert Allison */
/* Purpose: include file for all .c files */
/**********************************************/
/* includes */
#include <stdio.h>
#include <string.h>
/* define constant values */
#define BUF (80
+ 2) /* 80 characters
+
newline + end-of-string */
#define TRUE 1
#define FALSE 0
/* define function prototypes */
int get_line(char*, FILE*);
int if_rev(char*, char**);
void do_rev(char*);
/**********************************************/
/* Function: main */
/* Purpose: reverso main function */
/* Inputs: optional command line filename */
/* Outputs: return 1 for OK, 0 for error */
/* Version: 1.1, use fputs, not puts */
/* Author: R. Allison 040-xxx-xxx */
/**********************************************/
#include "rv.h"
int main(int argc, char *argv[])
{
/* Buffer to read a line */
char buf[BUF];
/* List of exceptions strings */
char *except[] = { "#", "//", NULL };
/* input file, default stdin */
FILE *file = stdin;
/* IF more than 1 argument */
if (argc > 2)
{
/* PRINT error to STDERR */
fprintf(stderr, "too many arguments\n");
/* EXIT with error */
return 0;
}
/* ELSE if exactly 1 argument */
if (argc == 2)
{
/* OPEN file */
/* IF error on open */
if ((file = fopen(argv[1], "r")) == NULL)
{
/* PRINT error to STDERR */
fprintf(stderr,
"file %s open error\n",
argv[1]);
/* EXIT with error */
return 0;
}
}
/* WHILE not
end-of-file from stdin get
next
line */
while (get_line(buf, file))
{
/* IF file is not STDIN */
if (file != stdin)
/* PRINT input line */
fputs(buf, stdout);
/* IF line is to be reversed */
if (if_rev(buf, except))
{
/* PRINT reverse of line */
do_rev(buf);
fputs(buf, stdout);
}
/* PRINT blank separator line */
fputc('\n', stdout);
}
/* IF file not STDIN */
if (file != stdin)
/* CLOSE file */
close(file);
/* EXIT */
return 1;
}
Function stubs for testing main() function
· get_line.c stub
#include "rv.h"
int get_line(char *buf, FILE *input)
{
return(fgets(buf, BUF, input) != NULL);
}
· if_rev.c stub
#include "rv.h"
int if_rev(char *string, char **list)
{
return TRUE;
}
· do_rev.c stub
#include "rv.h"
void do_rev(char *string)
{
return;
}
Real functions once main() is known to be OK
/**********************************************/
/* Function: get_line */
/* Purpose: read input lines from file */
/* Inputs: buffer space, file pointer */
/* Outputs: valid line in buffer space, */
/* returns TRUE, unless EOF then FALSE */
/* Version: 1.0 */
/* Author: R. Allison 040-xxx-xxx */
/**********************************************/
#include "rv.h"
int get_line(char *buf, FILE *input)
{
int rv = TRUE; /* default return value */
/* READ a line */
/* IF end-of-file */
if (fgets(buf, BUF, input) == NULL)
/* RETURN FALSE */
rv = FALSE;
/* WHILE line does not end with newline */
else while (buf[strlen(buf) - 1] != '\n')
{
/* PRINT error to STDERR */
fprintf(stderr,
"line too long,
ignored\n");
/* DO remove trailing parts */
do
{
/* READ rest of input */
/* IF end-of-file */
if (fgets(buf, BUF, input) == NULL)
{
/* RETURN FALSE */
rv = FALSE;
break;
}
/* WHILE line does not end with newline */
} while (buf[strlen(buf) - 1] != '\n');
if (rv == FALSE)
break;
/* READ next line */
/* IF end-of-file */
if (fgets(buf, BUF, input) == NULL)
{
/* RETURN FALSE */
rv = FALSE;
break;
}
}
/* RETURN TRUE */
return rv;
}
/**********************************************/
/* Function: if_rev */
/* Purpose: see if line is to be reversed */
/* Inputs: line to check, list of prefixes */
/* Outputs: returns TRUE to reverse or FALSE */
/* Version: 1.1, use strncmp, not strcmp */
/* Author: R. Allison 040-xxx-xxx */
/**********************************************/
#include "rv.h"
int if_rev(char *string, char **list)
{
int ret_val = TRUE;/* default return value */
int index = 0; /* index into list */
/* FOR each item in exception list */
while (list[index] != NULL)
{
/* IF this entry matches start of line */
if
(strncmp(string, list[index],
strlen(list[index])) == 0)
{
/* RETURN FALSE */
ret_val = FALSE;
break;
}
index++;
}
return ret_val;
}
/**********************************************/
/* Function: do_rev */
/* Purpose: reverse string in-place */
/* Inputs: string to reverse */
/* Outputs: reversed string */
/* Version: 1.0 */
/* Author: R. Allison 040-xxx-xxx */
/**********************************************/
#include "rv.h"
void do_rev(char *string)
{
/* ends to swap */
int begin = 0, end = strlen(string) - 2;
char temp; /* place for swapping */
/* WHILE not at the middle of the line */
while (begin < end)
{
/* EXCHANGE 2 characters from either end */
temp = string[end];
string[end] = string[begin];
string[begin] = temp;
/* MOVE pointers towards the centre */
begin++;
end--;
}
return;
}
Final test output
I keyboard
1. input: nothing (^D with no lines)
output: none, just exit
why: test end-of-file handling
[allisor rv]$ rv
^D[allisor rv]$
result: as expected
2. input: empty line (return only)
output: 3 blank lines (input, reverse, blank)
why: test that a null input line is handled
[allisor rv]$ rv
result: as expected
3. input: line abcd
output: line dcba plus blank line
why: test simple reversal
abcd
dcba
result: as expected
4. input: line #abcd
output: blank line
why: test no output after # line (no reversal)
#abcd
result: as expected
5. input: line //abcd
output: blank line
why: test no output after // line (no reversal)
//abcd
result: as expected
6. input: lines x#abcd, x//defg
output: lines dcba#x,blank line, gfed//x,blank line
why: normal reversals when # and // not first characters
x#abcd
dcba#x
x//defg
gfed//x
result: as expected
7. input: lines 012…8, #12…8, //2…8 (all lines 79 char)
output: lines 8…210,blank, #12…8,blank //2…8, blank
why: just under the 80 character limit
012…678
876…210
#12…678
//2…678
result: as expected
8. input: lines 012…9, #12…9, //2…9 (all lines 80 char)
output: lines 9…210,blank, #12…9,blank, //2…9,blank,
why: just at the 80 character limit
012…6789
9876…210
#12…6789
//2…6789
result: as expected
9. input: lines 012…0, #12…0, //2…0 (all lines 81 char)
output: "line too long, ignored"
3
times; short line ("0\n") ignored
why: just over the 80 character limit
012…6789
0
line too long, ignored
#12…6789
0
line too long, ignored
//2…6789
0
line too long, ignored
result: as expected
II file input
10. input: rv missing-file
output: "file missing-file open error" and exit
why: test open error handling
[allisor rv]$ rv missing-file
file missing-file open error
[allisor rv]$
result: as expected
11. input: two filenames
output: "too many arguments" and exit
why: test too many arguments
[allisor rv]$ rv file1 file2
too many arguments
[allisor rv]$
result: as expected
12. input: empty file
output: none and exit
why: immediate end-of-file
[allisor rv]$ echo "" > test12
[allisor rv]$ rv test12
[allisor rv]$
result: as expected
13. input: file of 6 lines, copies of tests 2 to 6 above
output: input and exactly the same as from the keyboard
why: file and keyboard behaviour must be identical
[allisor rv]$ cat test13
abcd
#abcd
//abcd
x#abcd
x//defg
[allisor rv]$ rv test13
abcd
dcba
#abcd
//abcd
x#abcd
dcba#x
x//defg
gfed//x
[allisor rv]$
result: as expected
14. input: file of 9 lines, copies of tests 7 to 9 above
output: input and exactly the same as from the keyboard
why: file and keyboard behaviour must be identical
[allisor rv]$ cat test14
3 groups of 3 lines, separated by blank lines
[allisor rv]$ rv test14
012…678
876…210
#12…678
//2…678
012…6789
9876…210
#12…6789
//2…6789
line too long, ignored
line too long, ignored
line too long, ignored
[allisor rv]$
result: as expected
15. input: lines 1234, 12345,from keyboard
output: 4321, blank line, 54321, blank line
why: test both even- and odd-length reversals
[allisor rv]$ rv
1234
4321
12345
54321
[allisor rv]$
result: as expected
16. input: repeat test 15 from filefrom file
output: input and exactly the same as from the keyboard
why: file and keyboard behaviour must be identical
[allisor rv]$ cat test15
1234
12345
[allisor rv]$ rv test15
1234
4321
12345
54321
[allisor rv]$
result: as expected
Delivery and conclusion
Of course, when you're all finished and hand it over to your boss, that's when he says, "No, no. This is all wrong! You're missing '#' and '//' at the start of lines when there are spaces in front of them!
"Look at this:
[boss]$ rv
#asdf
fdsa#
//qwerty
ytrewq//
[boss]$
"Now do it over. And fast! I need it immediately."
You should have asked about leading spaces back in the requirements.
You can't win.
But the good news is that a simple modification to if_rev() (and your documentation, of course) will fix it ‑ scan off the spaces (and tabs, did I mention tabs?) before comparing. Then re-run all your tests plus some extra tests for leading white space (of course) and you're done.
If your boss now asks for no reverses if the line (after white space, or a string of '*', or something) starts with, say, '%' or '>' or '@!/', your design will already accommodate that. Any other request (perhaps a prompt when the input is from stdin) can easily be added as well.
Lucky thing, you say? No, planning and forethought.