; SRFI 48 ; Zhu Chongkai mrmathematica@yahoo.com ; 28-May-2005 (module format mzscheme (require mzlib/pretty) (provide s:format) (define (s:format . args) (cond ((null? args) (raise (make-exn:fail:contract:arity "format: expects at least 1 argument, given 0" (current-continuation-marks)))) ((string? (car args)) (apply s:format #f args)) ((< (length args) 2) (raise (make-exn:fail:contract:arity "format: expects at least 1 string arguments, given 0" (current-continuation-marks)))) (else (let ((output-port (car args)) (format-string (cadr args)) (args (cddr args))) (let ((port (cond ((output-port? output-port) output-port) ((eq? output-port #t) (current-output-port)) ((eq? output-port #f) (open-output-string)) (else (raise-type-error 'format "output-port/boolean" 0 args))))) (define (point-five? n) (let ((absn (abs n))) (= 0.5 (- absn (truncate absn))))) (define (round* n scale) ;; assert scale < 0 ;; Note: Scheme's "round to even" rule for 0.5* (let ((one (expt 10 (- scale)))) (/ (round (* n one)) one))) (define (string-index str c) (let ((len (string-length str))) (let loop ((i 0)) (cond ((= i len) #f) ((eqv? c (string-ref str i)) i) (else (loop (+ i 1))))))) (define (string-grow str len char) (let ((off (- len (string-length str)))) (if (positive? off) (string-append (make-string off char) str) str))) (define (string-pad-right str len char) (let ((slen (string-length str))) (cond ((< slen len) (string-append str (make-string (- len slen) char))) ((> slen len) (substring (number->string (round* (string->number str) len)) 0 len)) (else str)))) (define (format-fixed number-or-string width digits) (cond ((string? number-or-string) (string-grow number-or-string width #\space)) ((number? number-or-string) (let ((real (real-part number-or-string)) (imag (imag-part number-or-string))) (cond ((not (zero? imag)) (string-grow (string-append (format-fixed real 0 digits) (if (negative? imag) "" "+") (format-fixed imag 0 digits) "i") width #\space)) (digits (let* ((rounded-number (exact->inexact (round* real (- digits)))) (rounded-string (number->string rounded-number)) (dot-index (string-index rounded-string #\.)) (exp-index (string-index rounded-string #\e)) (length (string-length rounded-string)) (pre-string (cond (exp-index (if dot-index (substring rounded-string 0 (+ dot-index 1)) (substring rounded-string 0 (+ exp-index 1)))) (dot-index (substring rounded-string 0 (+ dot-index 1))) (else rounded-string))) (exp-string (if exp-index (substring rounded-string exp-index length) "")) (frac-string (if exp-index (substring rounded-string (+ dot-index 1) exp-index) (substring rounded-string (+ dot-index 1) length)))) (string-grow (string-append pre-string (if dot-index "" ".") (string-pad-right frac-string digits #\0) exp-string) width #\space))) (else ;; no digits (string-grow (number->string real) width #\space))))) (else (raise-type-error 'format "number/string" number-or-string)))) (define documentation-string "(format [] [...]) -- is #t, #f or an output-port OPTION [MNEMONIC] DESCRIPTION -- Implementation Assumes ASCII Text Encoding ~H [Help] output this text ~A [Any] (display arg) for humans ~S [Slashified] (write arg) for parsers ~W [WriteCircular] like ~s but outputs circular and recursive data structures ~~ [tilde] output a tilde ~T [Tab] output a tab character ~% [Newline] output a newline character ~& [Freshline] output a newline character if the previous output was not a newline ~D [Decimal] the arg is a number which is output in decimal radix ~X [heXadecimal] the arg is a number which is output in hexdecimal radix ~O [Octal] the arg is a number which is output in octal radix ~B [Binary] the arg is a number which is output in binary radix ~w,dF [Fixed] the arg is a string or number which has width w and d digits after the decimal ~C [Character] charater arg is output by write-char ~_ [Space] a single space character is output ~Y [Yuppify] the list arg is pretty-printed to the output ~? [Indirection] recursive format: next 2 args are format-string and list of arguments ~K [Indirection] same as ~? ") (define (require-an-arg args) (unless (pair? args) (raise-mismatch-error 'format "too few arguments: " args))) (define (format-help format-strg arglist) (letrec ((length-of-format-string (string-length format-strg)) (anychar-dispatch (lambda (pos arglist last-was-newline) (if (>= pos length-of-format-string) arglist ; return unused args (let ((char (string-ref format-strg pos))) (cond ((eqv? char #\~) (tilde-dispatch (+ pos 1) arglist last-was-newline)) (else (write-char char port) (anychar-dispatch (+ pos 1) arglist #f))))))) (has-newline? (lambda (whatever last-was-newline) (or (eqv? whatever #\newline) (and (string? whatever) (let ((len (string-length whatever))) (if (zero? len) last-was-newline (eqv? #\newline (string-ref whatever (- len 1))))))))) (tilde-dispatch (lambda (pos arglist last-was-newline) (cond ((>= pos length-of-format-string) (write-char #\~ port) ; tilde at end of string is just output arglist) (else (case (char-upcase (string-ref format-strg pos)) ((#\A) ; Any -- for humans (require-an-arg arglist) (let ((whatever (car arglist))) (display whatever port) (anychar-dispatch (+ pos 1) (cdr arglist) (has-newline? whatever last-was-newline)))) ((#\S) ; Slashified -- for parsers (require-an-arg arglist) (let ((whatever (car arglist))) (write whatever port) (anychar-dispatch (+ pos 1) (cdr arglist) (has-newline? whatever last-was-newline)))) ((#\W) (require-an-arg arglist) (let ((whatever (car arglist))) (write whatever port) (anychar-dispatch (+ pos 1) (cdr arglist) (has-newline? whatever last-was-newline)))) ((#\D) ; Decimal (require-an-arg arglist) (display (number->string (car arglist) 10) port) (anychar-dispatch (+ pos 1) (cdr arglist) #f)) ((#\X) ; HeXadecimal (require-an-arg arglist) (display (number->string (car arglist) 16) port) (anychar-dispatch (+ pos 1) (cdr arglist) #f)) ((#\O) ; Octal (require-an-arg arglist) (display (number->string (car arglist) 8) port) (anychar-dispatch (+ pos 1) (cdr arglist) #f)) ((#\B) ; Binary (require-an-arg arglist) (display (number->string (car arglist) 2) port) (anychar-dispatch (+ pos 1) (cdr arglist) #f)) ((#\C) ; Character (require-an-arg arglist) (write-char (car arglist) port) (anychar-dispatch (+ pos 1) (cdr arglist) (eqv? (car arglist) #\newline))) ((#\~) ; Tilde (write-char #\~ port) (anychar-dispatch (+ pos 1) arglist #f)) ((#\%) ; Newline (newline port) (anychar-dispatch (+ pos 1) arglist #t)) ((#\&) ; Freshline (unless last-was-newline (newline port)) (anychar-dispatch (+ pos 1) arglist #t)) ((#\_) ; Space (write-char #\space port) (anychar-dispatch (+ pos 1) arglist #f)) ((#\T) ; Tab (write-char #\tab port) (anychar-dispatch (+ pos 1) arglist #f)) ((#\Y) ; Pretty-print (pretty-print (car arglist) port) (anychar-dispatch (+ pos 1) (cdr arglist) #f)) ((#\F) (require-an-arg arglist) (display (format-fixed (car arglist) 0 #f) port) (anychar-dispatch (+ pos 1) (cdr arglist) #f)) ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9) ;; gather "~w[,d]F" w and d digits (let loop ((index (+ pos 1)) (w-digits (list (string-ref format-strg pos))) (d-digits '()) (in-width? #t)) (if (>= index length-of-format-string) (raise-mismatch-error 'format "improper numeric format directive in " format-strg) (let ((next-char (string-ref format-strg index))) (cond ((char-numeric? next-char) (if in-width? (loop (+ index 1) (cons next-char w-digits) d-digits in-width?) (loop (+ index 1) w-digits (cons next-char d-digits) in-width?))) ((char=? next-char #\F) (let ((width (string->number (list->string (reverse w-digits)))) (digits (if (zero? (length d-digits)) #f (string->number (list->string (reverse d-digits)))))) (display (format-fixed (car arglist) width digits) port) (anychar-dispatch (+ index 1) (cdr arglist) #f))) ((char=? next-char #\,) (if in-width? (loop (+ index 1) w-digits d-digits #f) (raise-mismatch-error 'format "too many commas in directive " format-strg))) (else (raise-mismatch-error 'format "~w.dF directive ill-formed in " format-strg))))))) ((#\? #\K) ; indirection -- take next arg as format string (cond ; and following arg as list of format args ((< (length arglist) 2) (raise-mismatch-error 'format "less arguments than specified for ~?: " arglist)) ((not (string? (car arglist))) (raise-mismatch-error 'format "~? requires a string: " (car arglist))) (else (format-help (car arglist) (cadr arglist)) (anychar-dispatch (+ pos 1) (cddr arglist) #f)))) ((#\H) ; Help (display documentation-string port) (anychar-dispatch (+ pos 1) arglist #t)) (else (raise-mismatch-error 'format "unknown tilde escape: " (string-ref format-strg pos))))))))) (anychar-dispatch 0 arglist #f))) (let ((unused-args (format-help format-string args))) (if (not (null? unused-args)) (raise-mismatch-error 'format "unused arguments " unused-args)) (if (eq? output-port #f) ;; if format into a string (get-output-string port)) ;; then return the string )))))))