Formatted Numeric Output
May 18, 2012
We provide functions similar to C: (number->decimal num wid align)
outputs integers and (number->float num wid prec align)
outputs floats, where the output action is to return the formatted string, which can passed on to display
or concatenated to other strings with string-append
or processed in other ways as needed. Num is the number to output. Wid is the number of print positions to output. Prec is the number of digits after the decimal point. Align is 'left
if the output is to be left-aligned in the output string, 'right
if the output is to be right-aligned in the output string, and 'center
if the output is to be centered in the output string; the align parameter is optional, and defaults to 'right
if not specified. If the number is too big to fit in the specified wid, the output string consists of wid hash characters “#
“. We won’t use Scheme’s number->string
function, which would be cheating.
We begin with the code to align the output. Align
takes a string, a width, and an alignment specification and returns a string with the input properly aligned; it also handles the case that the output is too big:
(define (align str wid aline)
(let ((len (string-length str)))
(cond ((< wid len) (rept wid #\#))
((eq? aline'left) (string-append str (rept (- wid len))))
((eq? aline 'center)
(let* ((left (quotient (- wid len) 2)) (right (- wid len left)))
(string-append (rept left) str (rept right))))
((eq? aline 'right) (string-append (rept (- wid len)) str))
(else (error 'align "invalid alignment specifier")))))
To format integers, we split the sign from the number and handle each separately. Note the special treatment for an input of 0, which causes the digits
function to return an empty list:
(define (number->decimal num wid . aline)
(if (not (integer? num))
(error 'number->decimal "invalid input")
(let ((aline (if (pair? aline) (car aline) 'right))
(sign (if (negative? num) "-" (if (zero? num) "0" "")))
(num (list->string (map digit->char (digits (abs num))))))
(align (string-append sign num) wid aline))))
Formatting decimal numbers is harder. Here, we split the number into three parts — sign, integer part, fractional part — and carefully deal with 0:
(define (number->float num wid prec . aline)
(if (not (number? num))
(error 'number->float "invalid input")
(let* ((aline (if (pair? aline) (car aline) 'right))
(sign (if (negative? num) "-" (if (zero? num) "0" "")))
(num (abs num))
(left (inexact->exact (truncate num)))
(right (inexact->exact (round (* (- num left) (expt 10 prec)))))
(left (list->string (map digit->char (digits left))))
(right (if (zero? right) (rept prec #\0)
(list->string (map digit->char (digits right))))))
(align (string-append sign left "." right) wid aline))))
Here are some examples:
> (number->decimal 1234 12 'left)
"1234 "
> (number->decimal -1234 12 'right)
" -1234"
> (number->decimal 0 12 'center)
" 0 "
> (number->float 1234.56 12 4 'left)
"1234.5600 "
> (number->float -1234.5678 12 2 'right)
" -1234.57"
> (number->float 1234 12 2 'center)
" 1234.00 "
> (number->float 1234.56 12 4)
" 1234.5600"
> (number->float 0.1234 12 4)
" .1234"
> (number->float 0 12 4)
" 0.0000"
We used digits
from the Standard Prelude. You can run the program at http://programmingpraxis.codepad.org/yZiaS4Jl, where you can also see the digit->char
and rept
functions.
This may not conform exactly with the proposed problem, but an interesting read regarding implementing printf for C++ programmers:
http://insanecoding.blogspot.com/2010/03/c-201x-variadic-templates.html
I thought it would be fun to implement the Dartmouth BASIC printing format. This was back in the days where formatting didn’t seem all the necessary as the computer printed how users intuitively expected things to go. In FORTH since that language (a) has a standard word to pick apart a floating point number and (b) default printing is rather unfriendly in that language.
Some tests: