Laboratory Exercises For Computer Science 151

Formatted Output

Formatted Output

Goals: This laboratory exercise reviews several standard procedures for conversion of data from one type (e.g., string, number, symbol) to another. The lab then uses these and other procedures for formatting output nicely. The resulting procedures allow the computation and printing of neatly arranged tables of information.

Data Conversion: The Strings Lab mentioned Scheme procedures for converting between strings and lists and between strings and symbols. The following table extends the collection of data conversion procedures mentioned there.

Procedure Sample Call Result of Example Comment
string->list(string->list "example") (#\e #\x #\a #\m #\p #\l #\e) makes a list of the characters in a string
list->string (list->string '(#\e #\x #\a #\m #\p #\l #\e)) "example" makes a string of the characters in a list
symbol->string (symbol->string 'example) "example" change a given symbol to a string
string->symbol (string->symbol "example") example convert a given string to a symbol
number->string (number->string 3.14159) "3.14159" convert the number to an string [in base 10]
string->number (string->number "3.3") 3.3 convert the [base 10] number to a string
char->integer (char->integer #\F) 70 convert a character to its corresponding integer code
integer->char (integer->char 70) #\F return the character with the given integer code
For the most part, these conversion procedures work as expected. If we start with data of one type, the procedure returns the corresponding information represented in another type.

  1. Try the examples in the table to check that the expected results are obtained.

  2. Check the following examples of converting numbers to strings:
    
    (number->string 2/3)
    (number->string -3/6)
    (number->string 1234567890)
    
    Be sure to check if all the results are as you expected.
In some cases, however, subtleties arise. For example, Scheme does not distinguish between upper and lower case letters when considering symbols. Thus, (eq? 'example 'Example) returns true #t, as these two symbols are considered by Scheme to be identical. This raises the question of how such symbols will be converted to a string.

In Scheme, this problem is resolved by allowing the implementation to specify a standard case. For Chez Scheme, symbols normally are considered to be in lower case.

  1. Try the following examples:
    
    (symbol->string 'Example)
    (symbol->string 'example)
    (symbol->string 'ExAmPlE)
    
    In each case, what is printed?

  2. What is the result of the following:

    (string->symbol (symbol->string 'ExAmPlE))

Since Chez Scheme normally uses lower case letters for a symbol, it uses a special notation for symbols which are forced to contain upper case letters.
  1. Try the following:
    
    (string->symbol "variable")
    (string->symbol "VARIABLE")
    (string->symbol "VaRiAbLe")
    (symbol->string (string->symbol "variable"))
    (symbol->string (string->symbol "VARIABLE"))
    (symbol->string (string->symbol "VaRiAbLe"))
    
    In each case, describe what is printed?

  2. To clarify this further, consider the Scheme procedure eval which evaluates a given Scheme expression. Now, try the following sequence:
    
    (define first 10)
    (define Second 20)
    (define ThIrD 30)
    first
    second
    third
    (eval first)
    (eval Second)
    (eval ThIrD)
    (eval (string->symbol "first"))
    (eval (string->symbol "Second"))
    (eval (string->symbol "second"))
    (eval (string->symbol "ThIrD"))
    (eval (string->symbol "third"))
    
    In each case, explain the result.
Another subtlety arises when converting real numbers to strings.
  1. Try each of the following expressions
    
    (/ 1.0 8.0)
    (/ 1.0 7.0)
    (/ 1.0 3.0)
    (number->string (/ 1.0 8.0))
    (number->string (/ 1.0 7.0))
    (number->string (/ 1.0 3.0))
    
    In each case, check how many digits are included in the string.
This example illustrates that Scheme prints differing numbers of significant digits for different real numbers, and the number->string procedure follows this same pattern.

Note: Actually, within the machine, Scheme stores numbers in a binary (base 2) representation, and real numbers are stored to a limited number of significant digits. The number of digits printed involves the precision needed to distinguish one real number from another in base 2 -- for the specified number of significant digits. Thus, while the number of digits printed or included in a string may be helpful from the standpoint of the storage of a real number, the format of the real number in string form may vary from number to number.

Formatted Output: We now turn to the question of how to format output nicely, so we can place numbers in appropriate columns when we print tables. For example, suppose we want to print a table of equivalent values of quarts and liters:


   Quarts    Liters
      1       0.94        
      2       1.89
      3       2.84
      4       3.78
      5       4.73
      6       5.68
      7       6.62
      8       7.57
      9       8.52
     10       9.46
     11      10.41
     12      11.36
Here, we want to print integer quarts values and real numbers to 2 decimal place accuracy for the liter equivalents.

To generate this table, our general approach can follow the development we described earlier in the Input and Output Lab. While the formatting of integers contains relatively few surprise, some comments may be helpful. The formatting of reals turns out to be rather tricky.

Formatting Integers: To print integers in a consistent format, we normally need to decide how many characters to allow for a number field, and we want the integer right justified within that space. In the sample table above, for example, we allocated 6 characters for the quarts integer. Thus, 5 spaces are inserted before the integers 1, 2, 3, ..., 9, and 4 spaces are inserted before 10, 11, and 12.

This suggests we want a procedure formatted-integer which takes both an integer and a number of characters (a field width) as parameters. Procedure formatted-integer should return a string of the specified length, with the integer at the end. If the integer requires more characters than the width specified, we will allow formatted-integer to return a longer string.

The development of formatted-integer is reasonably straightforward, if we start with an integer and if we use the number->string procedure. After obtaining a string, we just add the correct number of spaces at the start. Such a procedure definition is given below:


(define formatted-integer
   (lambda (int-value width)
      (if (not (integer? int-value))
          (error 'formatted-integer "cannot format non-integer as integer"))
      (let* ((int-string (number->string int-value))
             (int-length (string-length int-string))
             (number-init-blanks (max (- width int-length) 0))
             (blank-string (make-string number-init-blanks #\space)))
         (string-append blank-string int-string)
      )
   )
)
In this procedure, make-string constructs a string of the given length (number-init-blanks), where each character in the string is #\space.

  1. Run this procedure in the following cases. In each case, be sure the procedure returns what you expect.

    
    (formatted-integer 41264 5)
    (formatted-integer 41264 10)
    (formatted-integer 41264 20)
    (formatted-integer -41264 20)
    (formatted-integer 41264 2)
    (formatted-integer 412.64 10)
    
  2. Why does this procedure use let* rather than let?
    (What happens if you replace let* by let? Why?)

  3. What is the purpose of the max procedure in formatted-integer?

  4. Modify formatted-integer so that it generates an error if width is a not a non-negative integer.
Formatting Real Numbers: In printing real numbers, we must consider both how many characters to allocate for the entire number and how many digits we want printed after the decimal point. For example, in the Quarts/Liters table earlier in this lab, we allocated 11 characters altogether to each real liter value (8 to the left of the decimal point, one for the decimal point itself, and 2 to the right). Thus, we will want to write a procedure write-real with parameters real-value, width, and fractional-length.

In considering how to write write-real, it turns out that the details are rather complicated, because many cases arise. For example, it might seem natural to divide a real number into its whole number (integer) part and its fractional part (to the right of the decimal point). In such an approach, the real number 123.456 would be treated as the integer 123 followed by the fractional part 456 . Some examples follow:

Real Number Integer Part Fractional Part
123.456 123 456
-23.456 -23 456
0.456 0 456
-0.456 0 456
123.006 123 006
-23.056 -23 056
At first glace, each of these parts could be translated to an integer, and conversion to a string might use formatted-integer. Unfortunately, troubles arise with each of these parts.

For example, the table shows that the integer part of 0.456 and -0.456 are the same. If one just takes the integer part of a real number between -1 and 1, then one may not be able to distinguish between positives and negatives. To resolve this problem it is necessary to handle the sign explicitly. If a number is negative, we must put in the minus sign directly -- we cannot rely upon formatted-integer to do the job.

The table also reminds us that a fractional part may contain initial zeros. Thus, printing 006 as an integer would give only the value 6, and we cannot rely upon formatted-integer or integer->string to print the factional part either.

A further complication arises in that real numbers are not stored exactly within the computer; rather only a specified number of digits are kept. Further, the number is stored using a base 2 (or binary) representation, rather than the familiar decimal. While the details of conversion from a binary form to decimal are beyond what we can consider in this course, one direct consequence is that specific care must be taken not to introduce printing errors when converting from an internal, binary format to a decimal string format.

One complete version of write-real may be found in file /home/stone/courses/scheme/spring-1997/examples/write-real.ss. In addition to handling the various subtle cases mentioned above, this write-real procedure allows printing to any output port, and write-real performs considerable error checking of parameters.

While the resulting procedure is rather long and complex, we can use the procedure easily. First, we include the file containing this procedure into our Scheme environment with the statement:


(load "/home/stone/courses/scheme/spring-1997/examples/write-real.ss")
Then we can use write-real without further trouble, just as when we have defined any procedures ourselves.

  1. Run this procedure with a several real numbers, widths, and fractional-lengths to check that it runs correctly. For example, verify that the procedure works correctly on the examples given in the above table.

  2. What happens if write-real is given inappropriate data? For example, how does it respond if it is given a string, rather than an integer? If the field width or the fractional-length are not integers? If it is given the value 1.0/0.0 to print?

  3. Now that we have defined formatted-integer and write-real, write a procedure that generates the table of quarts and liters values shown above.


This document is available on the World Wide Web as

http://www.math.grin.edu/~walker/courses/151.fa00/lab-formatted-output.html

created April 3, 1997
last revised Novewmber 8, 2000