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
dcba

short string

2

#ghi

#ghi

# start

3

mnopqrstuvw

mnopqrstuvw
wvutsrqponm

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
dcba

short string

2

#ghi

#ghi

# start

3

mnopqrstuvw

mnopqrstuvw
wvutsrqponm

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.