Formatted Output

April 8, 2014

The only output functions provided by standard Scheme are display, for plain output, and write, for system-formatted output. That’s rather limiting. At the other extreme, Lisp provides the format function, which has more options than you can dream about (before I discarded it in favor of the HyperSpec, the spine of my printed copy of CLtL2 was broken in two places, format and loop). Many languages provide some kind of formatted output — does anyone remember PIC in COBOL? In today’s exercise we will implement the printf function popularized by C and used in several languages.

There are three functions in the printf family: (sprintf fmt expr) returns a string formatted according to the specification given by format and containing the values expr …; (printf fmt expr) displays a string formatted similarly to sprintf to the current output port, and (fprintf port fmt expr) displays a string formatted similarly to sprintf to the indicated port. The fmt and port arguments are always required.

The fmt argument is a string that contains literal text, escape sequences, and specifications of how the expressions should be formatted. A specification consists of a literal percent-sign %, zero or more modifiers, and a single-character specifier. The single-character specifiers that we will support are:

c ascii character
d decimal integer
f floating-point number
o octal integer
s string
x hexadecimal integer
% literal percent sign

There should be as many expressions as there are format specifiers in the fmt string, except that a % literal percent sign specifier does not consume an expression. As many as four modifiers may appear between the literal percent sign % that starts a specifier and the single-character specifier that ends it:

- left-justify the expression in its field; if not given, the expression is right-justified
0 left-pad with leading zeros instead of spaces
width pad field to this width using spaces (or zeros)
.prec digits to right of decimal point, or maximum string length

The modifiers must appear in the order shown above. The width and prec arguments are unsigned decimal integers.

Escape sequences are introduced by a backslash; the following escape sequences are supported:

\b backspace
\f formfeed
\n newline
\r carriage return
\t horizontal tab
\ddd character with octal value ddd, where ddd is 1 to 3 digits between 0 and 7 inclusive
\c any other character c literally, for instance \\ for backslash or \" for quotation mark

Depending on the environment, a newline may (or may not) imply a carriage return, or vice-versa. Several examples are given on the next page.

Your task is to write a function that provides formatted output for your language, using the definition of printf given above or some other specification that is suitable for your language and your aspirations. When you are finished, you are welcome to read or run a suggested solution, or to post your own solution or discuss the exercise in the comments below.


Pages: 1 2

3 Responses to “Formatted Output”

  1. Michael D said

    The problem seems to call for `,prec` (note the comma), but your code and examples on the next page appear to expect `.prec` (period-delimited), which is what I believe `sprintf` expects as well.

  2. programmingpraxis said

    Fixed. Thank you.

  3. Jussi Piitulainen said

    ;;; Argument specs in a format string are {dec}, {bin} for decimal and
    ;;; binary formatting of an exact integer, {new} for a newline, and {}
    ;;; for just display. They can start with a position: {0|dec} refers
    ;;; to the same argument as the first {bin} and does not advance the
    ;;; argument counter. -- Just a skeleton.

    (define (format spec args out)
      (define n (string-length spec))
      (let outer ((k 0) (b 0) (e 0))
         ((= e n)
          (display (substring spec b e) out))
         ((char=? (string-ref spec e) #\{)
          (display (substring spec b e) out)
          (let inner ((b (+ e 1)) (e (+ e 1)))
             ((char=? (string-ref spec e) #\})
              (outer (format-case (substring spec b e) args k out) (+ e 1) (+ e 1)))
             (else (inner b (+ e 1))))))
         (else (outer k b (+ e 1))))))

    (define (format-case spec args k out)
      (define n (string-length spec))
          (lambda ()
            (let scan ((k 0))
               ((= k n) (values #f spec))
               ((char=? (string-ref spec k) #\|)
                (values (substring spec 0 k) (substring spec (+ k 1) n)))
               (else (scan (+ k 1))))))
        (lambda (pos spec)
          (define (arg) (list-ref args (if pos (string->number pos) k)))
          (define (next) (if pos k (+ k 1)))
           ((string=? spec "dec")
            (display (number->string (arg) 10) out) (next))
           ((string=? spec "bin")
            (display (number->string (arg) 2) out) (next))
           ((string=? spec "new") (newline out) k)
           ((string=? spec "") (display (arg) out) (next))
           (else (error "Unknown format spec" spec))))))

    ;;; > (format "Binary {bin}{new}= Decimal {0|dec}{new}< Decimal {}{new}"
    ;;; '(2 3) (current-output-port))
    ;;; Binary 10
    ;;; = Decimal 2
    ;;; < Decimal 3

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: