;; write-real.ss: procedures for writing formatted real numbers in Scheme ;; John David Stone ;; Department of Mathematics and Computer Science ;; Grinnell College ;; April 10-11, 1997 ;; My objective is to write out any given real number, to a specified ;; output port (assumed to be open), right-justified in a field of ;; specified width (as measured in columns), rounded to a specified number ;; of places after the decimal point. For instance, the call ;; ;; (write-real 3.14159 12 3 target) ;; ;; causes the twelve-character string " 3.142" to be written to the ;; output port that is the current value of TARGET. ;; It is an error if the first argument is not a real number, or the second ;; argument is not an integer, or the third argument is not a natural ;; number, or the fourth argument is not an output port. ;; Chez Scheme's REAL? predicate returns #T for some values that are not, ;; in fact, real numbers -- IEEE floating-point positive and negative ;; infinites and NaN (``not a number'') values. It is an error for the ;; first argument to be one of these. ;; If the real number is negative, a minus sign is written just before the ;; first digit of the number. It is possible for the minus sign to precede ;; a numeral containing no digits other than zero; for instance, the call ;; ;; (write-real -0.0001 6 2 target) ;; ;; writes " -0.00" to TARGET. A digit is printed to the left of the ;; decimal point even if the real number is between -1 and +1. ;; If the number of characters in the written representation of the number, ;; rounded to the specified number of decimal places and prefixed if ;; necessary by the minus sign, is greater than the specified field width, ;; the full representation is nevertheless written to the specified output ;; port, with no leading spaces. This condition is always satisfied if ;; the field-width is non-positive, so the programmer can supply a ;; field-width of, say, 0 to ensure that the numeral occupies the least ;; possible number of columns. ;; If the third argument (the ``fraction length'') is zero, the real number ;; is rounded to the nearest integer and the decimal point is suppressed. ;; It is possible for the written representation to be "-0" in this case, ;; too; this occurs whenever the value to be printed is less than 0 and ;; greater than -0.5. ;; The fourth argument may be omitted, in which case the formatted value ;; is written to the current output port. (define write-real (lambda (num field-width fraction-length . optional-target) ;; Recover the optional argument, if it is present. (let ((target (cond ((null? optional-target) (current-output-port)) ((null? (cdr optional-target)) (car optional-target)) (else (error 'write-real "No more than four arguments are permitted"))))) ;; Make sure the preconditions are satisfied. (if (not (strictly-real? num)) (error 'write-real "The first argument must be a real number")) (if (not (integer? field-width)) (error 'write-real "The field width must be an integer")) (if (or (not (integer? fraction-length)) (negative? fraction-length)) (error 'write-real "The fraction length must be a natural number")) (if (not (output-port? target)) (error 'write-real "The fourth argument must be an output port")) ;; The plan is to build the numeral as a string, compute the number ;; of spaces needed to pad it to the correct field width, write out ;; exactly that many spaces, and then write out the numeral. (let* ((numeral (number->numeral num fraction-length)) (len (string-length numeral))) (if (< len field-width) (display (make-string (- field-width len) #\space) target)) (display numeral target))))) ;; The STRICTLY-REAL? procedure determines whether its argument is a real ;; number in the strict sense -- an IEEE floating-point value other than an ;; infinity or a not-a-number. (define strictly-real? (lambda (obj) (and (real? obj) (or (<= obj 0) (<= 0 obj)) ; A not-a-number value fails this test. (or (zero? obj) (not (= obj (/ obj 2))))))) ; An infinity fails this test. ;; The hard work of actually building the numeral is deferred to the ;; NUMBER->NUMERAL procedure, which takes a real number and the number of ;; decimal places that are supposed to be visible and returns the string ;; denoting the appropriately rounded value. (define number->numeral (lambda (num fraction-length) ;; Test the preconditions. (if (not (strictly-real? num)) (error 'number->numeral "The first argument must be a real number")) (if (or (not (integer? fraction-length)) (negative? fraction-length)) (error 'number->numeral "The second argument must be a natural number")) ;; Determine first whether a minus sign is needed, and subsequently ;; consider only the absolute value of the number until the very last ;; step. (let ((sign (if (negative? num) "-" "")) (absnum (abs num))) ;; Separate the number into its whole and fractional parts. (let* ((whole (trunc absnum)) ;; WHOLE is an exact integer. (fraction (- absnum whole))) ;; As an intermediate stage, ``explode'' WHOLE into a list of ;; single-digit integers. (let ((whole-digit-list (explode-into-digits whole))) ;; Recover the first FRACTION-LENGTH digits of FRACTION as ;; another list of single-digit integers; retain the left-over ;; remainder, appropriately scaled up. (let ((recovered (recover-digits-from-fraction fraction fraction-length))) ;; The reason for keeping the residue around is to be able to ;; round the last digit of the numeral correctly; it must be ;; rounded up if the residue is 0.5 or more. A difficulty ;; arises if the digit to be rounded up is a 9: The rounding ;; causes a carry, which must be propagated through ;; successively more significant digits as long as all of them ;; are 9s. The PROPAGATE-CARRY procedure manages the details ;; of this process and returns a pair in which the car is the ;; list of digits of the whole part and the cdr is the list of ;; digits of the fractional part, now correctly rounded. (let ((digit-lists (propagate-carry whole-digit-list (car recovered) (cdr recovered)))) ;; Now we have all the pieces. Recover the revised ;; digit-lists and convert each digit to the appropriate ;; character representation. String together the sign, the ;; digits making up the whole part, a decimal point if ;; necessary, and the digits making up the fractional part; ;; return the resulting string. (string-append sign (apply string (map digit->char (car digit-lists))) (if (zero? fraction-length) "" ".") (apply string (map digit->char (cdr digit-lists))))))))))) ;; The TRUNC procedure recovers the integer part of any non-negative ;; real, as an exact value. (define trunc (lambda (num) ;; Test the precondition. (if (or (not (strictly-real? num)) (negative? num)) (error 'trunc "The argument must be a non-negative real number")) ;; Apply TRUNCATE and force the result to be exact. (inexact->exact (truncate num)))) ;; The EXPLODE-INTO-DIGITS procedure takes any non-negative exact integer as ;; argument and returns the list of single-digit integers that are the ;; positional weights of the digits of the decimal numeral for the given ;; integer. For instance, the call ;; ;; (explode-into-digits 3875) ;; ;; yields the list (3 8 7 5). ;; The first element of the list returned by EXPLODE-INTO-DIGITS is never ;; 0, except that (explode-into-digits 0) returns (0). (define explode-into-digits (lambda (num) ;; Test the precondition. (if (or (not (integer? num)) (inexact? num) (negative? num)) (error 'explode-into-digits "The argument must be an exact non-negative integer")) ;; Start with the least significant digit of NUM; place it on the list ;; SO-FAR of digits so far recovered. Divide by 10. If the result is ;; zero, you're done; otherwise, prepend the least significant digit ;; of the remaining number to SO-FAR and repeat. (let loop ((rest (quotient num 10)) (so-far (list (remainder num 10)))) (if (zero? rest) so-far (loop (quotient rest 10) (cons (remainder rest 10) so-far)))))) ;; The RECOVER-DIGITS-FROM-FRACTION procedure takes two arguments, the ;; first of which is a non-negative real number less than 1 and the second ;; of which is a natural number, and recovers a number of digits specified ;; by the second argument from the decimal expansion of the first argument. ;; It returns a pair in which the car is a list of integers in the range ;; from 0 to 9, giving the successive digits of the fractional part (most ;; significant digit first) and the cdr is a ``residue'' -- the part of the ;; fractional part that is left over after these digits have been ;; generated, scaled up by a factor of 10^PLACES. ;; For example, the call ;; ;; (recover-digits-from-fraction 0.00390625 5) ;; ;; yields ((0 0 3 9 0) . 0.625) -- the car (0 0 3 9 0), the cdr 0.625. (define recover-digits-from-fraction (lambda (fraction places) ;; Test the preconditions. (if (or (not (real? fraction)) (negative? fraction) (<= 1 fraction)) (error 'recover-digits-from-fraction "The first argument must be a non-negative real less than 1")) (if (or (not (integer? places)) (negative? places)) (error 'recover-digits-from-fraction "The second argument must be a natural number")) ;; In the following loop, RESIDUE is again the part of FRACTION not ;; yet reduced to digits, PLACES-LEFT is the number of digits that ;; remain to be recovered from RESIDUE, and RESULT is the list of ;; the digits so far recovered. (let loop ((residue fraction) (places-left places) (result '())) (if (zero? places-left) ;; At the end of the process, simply pair off the list of digits, ;; reversed to bring the most significant digit to the front, and ;; the residue. (cons (reverse result) residue) ;; Otherwise, recover the next digit by multiplying the residue ;; by 10 and truncating. Subtract away the value of this digit ;; to obtain the next residue, reduce by 1 the number of digits ;; yet to be recovered, and attach the newly recovered digit to ;; the list. (let* ((scaled-residue (* residue 10)) (digit (trunc scaled-residue))) (loop (- scaled-residue digit) (- places-left 1) (cons digit result))))))) ;; The PROPAGATE-CARRY procedure takes three arguments, two lists of ;; integers in the range from 0 to 9, representing respectively some number ;; of digits preceding and following the decimal point in a numeral that is ;; ultimately to be printed, and a ``residue'' (which must be a ;; non-negative real number less than 1). If the residue is less than 1/2, ;; the procedure simply constructs and returns a pair from the two lists; but ;; if the residue is greater than or equal to 1/2, PROPAGATE-CARRY adds 1 ;; to the least significant digit in the list of digits that will follow ;; the decimal point (or, if that list is empty, to the least significant ;; digit of the other list of digits). This has the effect of rounding up ;; the least significant digit of the numeral to be printed. ;; It is, however, possible that the digit to which 1 is added is a 9, ;; in which case the sum is no longer a digit. In this case, the resulting ;; 10 is changed to a 0, and the increment is carried to the next digit and ;; processed similarly. If a carry is generated by the most significant ;; digit in the list of digits to follow the decimal point, it is ;; propagated to the least significant digit in the list of digits to ;; that will precede the decimal point; if the carry is propagated all the ;; way to the most significant digit on that list as well, an additional ;; digit 1 is prepended. PROPAGATE-CARRY then pairs the revised lists and ;; returns the pair. ;; For example, the call ;; ;; (propagate-carry '(1 4 9) '(9 9 9 9) 0.6) ;; ;; yields the result ((1 5 0) 0 0 0 0) -- the car is (1 5 0), the revised ;; version of (1 4 9), and the cdr is (0 0 0 0), the revised version of ;; (9 9 9 9). In this case the carry has been propagated through all the ;; digits that will wind up to the right of the decimal point and also ;; through the least significant digit to the left of the decimal point. (define propagate-carry (lambda (before-point after-point residue) ;; Test the preconditions. (if (or (not (list-of-digits? after-point)) (not (list-of-digits? before-point))) (error 'propagate-carry (string-append "The first and second arguments must be " "lists of single-digit integers"))) (if (or (not (real? residue)) (negative? residue) (<= 1 residue)) (error 'propagate-carry "The third argument must be a non-negative real less than 1")) ;; If the residue is less than 1/2, repackage the lists and return. (if (< residue 1/2) (cons before-point after-point) ;; Otherwise, try handling the carry inside AFTER-POINT list. (let ((first-try (propagate-through-list after-point))) ;; If that works, bundle BEFORE-POINT with the revised list. (cons (if (car first-try) before-point ;; Otherwise, try handling the carry inside ;; BEFORE-POINT. (let ((second-try (propagate-through-list before-point))) ;; If that works, bundle the revised lists. (if (car second-try) (cdr second-try) ;; Otherwise, attach a 1 and then bundle. (cons 1 (cdr second-try))))) (cdr first-try)))))) ;; The LIST-OF-DIGITS? procedure determines whether its argument is a list ;; of integers in the range from 0 to 9 inclusive. (define list-of-digits? (lambda (ls) (or (null? ls) (and (pair? ls) (integer? (car ls)) (<= 0 (car ls) 9) (list-of-digits? (cdr ls)))))) ;; The PROPAGATE-THROUGH-LIST procedure takes a list of digits, most ;; significant digit first, and returns a pair in which the car is a ;; Boolean indicating whether it found a digit other than 9 and the cdr ;; is a similar list, but with each 9 that is not followed by any other ;; digit replaced with 0 and the last digit other than 9 (if there is one) ;; incremented by 1. ;; For instance, the call ;; ;; (propagate-through-list '(7 1 2 9 9)) ;; ;; yields (#t 7 1 3 0 0) -- the car is #t, indicating that the carry was ;; handled inside the list, and the cdr is (7 1 3 0 0), the revised version ;; of (7 1 2 9 9). (define propagate-through-list (lambda (ls) ;; Test the precondition. (if (not (list-of-digits? ls)) (error 'propagate-through-list "The argument must be a list of digits")) ;; Reverse LS to bring the least significant digit to the front, then ;; traverse down the list until either the end of the list or a digit ;; other than 9 is encountered. (let loop ((rest (reverse ls)) (so-far '())) ;; If there are no more digits to test, the carry cannot be ;; accommodated, so return a pair with #F as its car and SO-FAR as ;; its cdr. (cond ((null? rest) (cons #f so-far)) ;; If the leading digit of REST is a 9, attach a 0 to SO-FAR ;; and recurse. ((= (car rest) 9) (loop (cdr rest) (cons 0 so-far))) ;; Otherwise, add 1 to the leading digit and return a pair ;; consisting of #T and the revised list. (else (cons #t (append (reverse (cdr rest)) (list (+ (car rest) 1)) so-far))))))) ;; The DIGIT->CHAR procedure finds and returns the character that ;; represents a given integer in the range from 0 to 9. It presupposes ;; that the characters #\0 through #\9 are in successive positions in the ;; character set, as they are in ASCII. (define digit->char (lambda (digit) ;; Test the precondition. (if (or (not (integer? digit)) (not (<= 0 digit 9))) (error 'digit->char "The argument must be a single-digit integer")) ;; Think of the digit as a ``distance beyond #\0'' in the character ;; set. Start at the code for #\0, add the distance, return the ;; character at the resulting position in the character set. (integer->char (+ (char->integer #\0) digit)))) ;; Given the utility procedures developed here, it's also easy to put ;; together a corresponding WRITE-INTEGER procedure that outputs an integer ;; value, right-justified in a field of specified (minimum) width: (define write-integer (lambda (num field-width . optional-target) ;; Recover the optional argument, if it is present. (let ((target (cond ((null? optional-target) (current-output-port)) ((null? (cdr optional-target)) (car optional-target)) (else (error 'write-integer "No more than three arguments are permitted"))))) ;; Make sure the preconditions are satisfied. (if (not (strictly-integer? num)) (error 'write-integer "The first argument must be an integer")) (if (not (integer? field-width)) (error 'write-integer "The field width must be an integer")) (if (not (output-port? target)) (error 'write-integer "The third argument must be an output port")) ;; The plan is to build the numeral as a string, compute the number ;; of spaces needed to pad it to the correct field width, write out ;; exactly that many spaces, and then write out the numeral. (let* ((numeral (integer->numeral num)) (len (string-length numeral))) (if (< len field-width) (display (make-string (- field-width len) #\space) target)) (display numeral target))))) ;; The CONJOIN procedure forms the conjunction of two predicates of arity ;; 1. (define conjoin (lambda (first? second?) (lambda (obj) (and (first? obj) (second? obj))))) ;; In Chez Scheme, the IEEE infinities satisfy INTEGER?, so we need a ;; STRICTLY-INTEGER? procedure that excludes them. (define strictly-integer? (conjoin strictly-real? integer?)) ;; The INTEGER->NUMERAL procedure takes any integer as argument and returns, ;; as a string, the decimal numeral for that integer. (define integer->numeral (lambda (num) ;; Test the precondition. (if (not (strictly-integer? num)) (error 'integer->numeral "The argument must be an integer")) (string-append (if (negative? num) "-" "") (apply string (map digit->char (explode-into-digits (abs (inexact->exact num)))))))) ;;;;; write-integer.ss: procedures for writing formatted real numbers in Scheme ;; Henry M. Walker ;; Department of Mathematics and Computer Science ;; Grinnell College ;; January 15, 1998 ;;;The following procedure, based on lab work from April 13, 1997, ;;;prints a formatted integer. (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) ) ) ) (define write-int (lambda (int-value width . optional-target) (let ((target (cond ((null? optional-target) (current-output-port)) ((null? (cdr optional-target)) (car optional-target)) (else (error 'write-real "No more than three arguments are permitted"))))) ;; Make sure the preconditions are satisfied. (if (not (integer? int-value)) (error 'write-int "The first argument must be an integer")) (if (not (integer? width)) (error 'write-int "The field width must be an integer")) (if (not (output-port? target)) (error 'write-int "The third argument must be an output port")) ;; Formatted-integer produces desired string (display (formatted-integer int-value width) target))))