460 lines
17 KiB
Racket
460 lines
17 KiB
Racket
#lang racket/base
|
||
(require racket/contract/base)
|
||
|
||
;; cat = "consider as text" :)
|
||
|
||
;; TO DO:
|
||
;; - avoid unnecessary intermediate strings
|
||
;; - see "Printing Floating-Point Numbers Quickly and Accurately"
|
||
;; by Berger & Dybvig, PLDI 1996 for ideas
|
||
|
||
;; MAYBE TO DO:
|
||
;; - rename 'cat' -> 'catd' ("cat like display") and make 'cat'
|
||
;; recur into lists, vectors, etc?
|
||
;; - decimal separators (see "man 7 locale", lconv numeric fields)
|
||
;; - 'catmon' : like 'catn' but for monetary amounts (see strfmon)?
|
||
;; (perhaps as separate library)
|
||
;; - prop:cat, separate from prop:custom-write?
|
||
|
||
(define (non-empty-string? x)
|
||
(and (string? x) (positive? (string-length x))))
|
||
|
||
(define align-mode/c
|
||
(or/c 'left 'right 'center))
|
||
(define padding/c non-empty-string?)
|
||
|
||
(define sign-mode/c
|
||
(or/c #f '+ '++ 'parens
|
||
(let ([ind/c (or/c string? (list/c string? string?))])
|
||
(list/c ind/c ind/c ind/c))))
|
||
|
||
(define base/c
|
||
(or/c (integer-in 2 36)
|
||
(list/c 'up (integer-in 2 36))))
|
||
|
||
;; Precision is one of
|
||
;; - Nat, for "up to N"
|
||
;; - '(= N), for "exactly N"
|
||
(define precision/c
|
||
(or/c exact-nonnegative-integer?
|
||
(list/c '= exact-nonnegative-integer?)))
|
||
|
||
(define cat-n-c
|
||
(->* ()
|
||
(#:width (or/c exact-nonnegative-integer? #f)
|
||
#:limit (or/c exact-nonnegative-integer? +inf.0)
|
||
#:limit-marker string?
|
||
#:pad-to exact-nonnegative-integer?
|
||
#:align align-mode/c
|
||
#:padding padding/c
|
||
#:left-padding padding/c
|
||
#:right-padding padding/c)
|
||
#:rest list?
|
||
string?))
|
||
|
||
(define cat-1-c
|
||
(->* (any/c)
|
||
(#:width (or/c exact-nonnegative-integer? #f)
|
||
#:limit (or/c exact-nonnegative-integer? +inf.0)
|
||
#:limit-marker string?
|
||
#:pad-to exact-nonnegative-integer?
|
||
#:align align-mode/c
|
||
#:padding padding/c
|
||
#:left-padding padding/c
|
||
#:right-padding padding/c)
|
||
string?))
|
||
|
||
(provide/contract
|
||
[cat cat-n-c]
|
||
[catw cat-1-c]
|
||
[catp cat-1-c]
|
||
[catn
|
||
(->* (rational?)
|
||
(#:sign sign-mode/c
|
||
#:base base/c
|
||
#:precision precision/c
|
||
#:pos/exp-range (list/c (or/c exact-integer? +inf.0)
|
||
(or/c exact-integer? -inf.0))
|
||
#:exp-precision precision/c
|
||
#:exp-format-exponent (or/c #f string? (-> exact-integer? string?))
|
||
#:pad-digits-to exact-positive-integer?
|
||
#:digits-padding padding/c)
|
||
string?)]
|
||
[catnp
|
||
(->* (rational?)
|
||
(#:sign sign-mode/c
|
||
#:base base/c
|
||
#:precision precision/c
|
||
#:pad-digits-to exact-positive-integer?
|
||
#:digits-padding padding/c)
|
||
string?)]
|
||
[catne
|
||
(->* (rational?)
|
||
(#:sign sign-mode/c
|
||
#:base base/c
|
||
#:precision precision/c
|
||
#:format-exponent (or/c #f string? (-> exact-integer? string?))
|
||
#:pad-digits-to exact-positive-integer?
|
||
#:digits-padding padding/c)
|
||
string?)])
|
||
|
||
;; ----------------------------------------
|
||
|
||
(define (%limit #:limit limit
|
||
#:limit-marker limit-marker
|
||
s)
|
||
(cond [(> (string-length s) limit)
|
||
(string-append (substring s 0 (- limit (string-length limit-marker)))
|
||
limit-marker)]
|
||
[else s]))
|
||
|
||
(define (%pad #:pad-to pad-to
|
||
#:align align-mode
|
||
#:left-padding left-padding
|
||
#:right-padding right-padding
|
||
s)
|
||
(cond [(< (string-length s) pad-to)
|
||
(let* ([s-length (string-length s)]
|
||
[to-pad-length (max 0 (- pad-to s-length))])
|
||
(let-values ([(left-pad-length right-pad-length)
|
||
(case align-mode
|
||
((left) (values 0 to-pad-length))
|
||
((right) (values to-pad-length 0))
|
||
((center)
|
||
(values (floor (/ to-pad-length 2))
|
||
(ceiling (/ to-pad-length 2)))))])
|
||
(string-append
|
||
(build-padding 'left left-padding left-pad-length)
|
||
s
|
||
(build-padding 'right right-padding right-pad-length))))]
|
||
[else s]))
|
||
|
||
(define (build-padding side padding pad-length)
|
||
(cond [(zero? pad-length) ""]
|
||
[(char? padding)
|
||
(make-string pad-length padding)]
|
||
[(and (string? padding) (= (string-length padding) 1))
|
||
(make-string pad-length (string-ref padding 0))]
|
||
[(string? padding)
|
||
(let* ([pattern padding]
|
||
[pattern-length (string-length pattern)]
|
||
[whole-copies (quotient pad-length pattern-length)]
|
||
[part-length (remainder pad-length pattern-length)]
|
||
[pattern-copies (for/list ([i (in-range whole-copies)]) pattern)])
|
||
(apply string-append
|
||
;; For left, start at start of string
|
||
;; For right, end at end of string.
|
||
(case side
|
||
((left)
|
||
(append pattern-copies
|
||
(list (substring pattern 0 part-length))))
|
||
((right)
|
||
(cons (substring pattern (- pattern-length part-length) pattern-length)
|
||
pattern-copies)))))]))
|
||
|
||
(define (do-checks who limit limit-marker width)
|
||
(when (> width limit)
|
||
(error who "pad-to length greater than limit (~s): ~s" limit width))
|
||
(when (> (string-length limit-marker) limit)
|
||
(error who "limit-marker string longer than limit (~s): ~e"
|
||
limit limit-marker)))
|
||
|
||
;; ----------------------------------------
|
||
|
||
(define (%cat s
|
||
#:who who
|
||
#:limit limit
|
||
#:limit-marker limit-marker
|
||
#:pad-to pad-to
|
||
#:align align
|
||
#:right-padding right-padding
|
||
#:left-padding left-padding)
|
||
(do-checks who limit limit-marker pad-to)
|
||
(%pad (%limit (if (list? s) (apply string-append s) s)
|
||
#:limit limit
|
||
#:limit-marker limit-marker)
|
||
#:pad-to pad-to
|
||
#:align align
|
||
#:left-padding left-padding
|
||
#:right-padding right-padding))
|
||
|
||
(define (cat #:width [width #f]
|
||
;; I was greatly tempted to name this keyword option #:nip instead
|
||
;; (or maybe #:nip-to)
|
||
#:limit [limit (or width +inf.0)]
|
||
#:limit-marker [limit-marker "..."]
|
||
#:pad-to [pad-to (or width 0)]
|
||
#:align [align 'left]
|
||
#:padding [padding " "]
|
||
#:right-padding [right-padding padding]
|
||
#:left-padding [left-padding padding]
|
||
. vs)
|
||
(%cat (map (lambda (v) (if (string? v) v (format "~a" v))) vs)
|
||
#:who 'cat
|
||
#:limit limit
|
||
#:limit-marker limit-marker
|
||
#:pad-to pad-to
|
||
#:align align
|
||
#:right-padding right-padding
|
||
#:left-padding left-padding))
|
||
|
||
(define (catw #:width [width #f]
|
||
#:limit [limit (or width +inf.0)]
|
||
#:limit-marker [limit-marker "..."]
|
||
#:pad-to [pad-to (or width 0)]
|
||
#:align [align 'left]
|
||
#:padding [padding " "]
|
||
#:right-padding [right-padding padding]
|
||
#:left-padding [left-padding padding]
|
||
v)
|
||
(%cat (format "~s" v)
|
||
#:who 'catw
|
||
#:limit limit
|
||
#:limit-marker limit-marker
|
||
#:pad-to pad-to
|
||
#:align align
|
||
#:right-padding right-padding
|
||
#:left-padding left-padding))
|
||
|
||
(define (catp #:width [width #f]
|
||
#:limit [limit (or width +inf.0)]
|
||
#:limit-marker [limit-marker "..."]
|
||
#:pad-to [pad-to (or width 0)]
|
||
#:align [align 'left]
|
||
#:padding [padding " "]
|
||
#:right-padding [right-padding padding]
|
||
#:left-padding [left-padding padding]
|
||
v)
|
||
(%cat (format "~v" v)
|
||
#:who 'cat
|
||
#:limit limit
|
||
#:limit-marker limit-marker
|
||
#:pad-to pad-to
|
||
#:align align
|
||
#:right-padding right-padding
|
||
#:left-padding left-padding))
|
||
|
||
;; ----
|
||
|
||
(define (catn N
|
||
#:sign [sign-mode #f]
|
||
#:base [base 10]
|
||
#:precision [precision 3]
|
||
#:pos/exp-range [pos/exp-range #f]
|
||
#:exp-precision [exp-precision 5]
|
||
#:exp-format-exponent [exp-format-exponent #f]
|
||
#:pad-digits-to [pad-digits-to 1]
|
||
#:digits-padding [digits-padding " "])
|
||
(let* ([N-abs (abs N)]
|
||
[positional?
|
||
(or (zero? N-abs)
|
||
(not pos/exp-range)
|
||
(let ([max-neg-exp (car pos/exp-range)]
|
||
[min-pos-exp (cadr pos/exp-range)])
|
||
(< (expt base max-neg-exp) N-abs (expt base min-pos-exp))))])
|
||
(if positional?
|
||
(catnp N
|
||
#:who 'catn
|
||
#:sign sign-mode
|
||
#:base base
|
||
#:precision precision
|
||
#:pad-digits-to pad-digits-to
|
||
#:digits-padding digits-padding)
|
||
(catne N
|
||
#:who 'catn
|
||
#:sign sign-mode
|
||
#:base base
|
||
#:precision exp-precision
|
||
#:format-exponent exp-format-exponent
|
||
#:pad-digits-to pad-digits-to
|
||
#:digits-padding digits-padding))))
|
||
|
||
(define (catnp N
|
||
#:who [who 'catnp]
|
||
#:sign [sign-mode #f]
|
||
#:base [base 10]
|
||
#:precision [precision 3]
|
||
#:pad-digits-to [pad-digits-to 1]
|
||
#:digits-padding [digits-padding " "])
|
||
;; precision: up to (or exactly) this many digits after decimal point
|
||
;; precision = 0 means no decimal point
|
||
;; precision = '(= 0) means keep decimal point
|
||
;; pad-digits-to: includes decimal point, doesn't include sign
|
||
(let*-values ([(upper? base) (normalize-base base)]
|
||
[(exactly? precision) (normalize-precision precision)])
|
||
(let* ([N-abs (abs N)]
|
||
[digits-part (%positional N-abs base upper? precision exactly?)]
|
||
[padded-digits-part
|
||
(%pad digits-part
|
||
#:pad-to pad-digits-to
|
||
#:align 'right
|
||
#:left-padding digits-padding
|
||
#:right-padding #f)])
|
||
(let-values ([(pre-sign-part post-sign-part) (get-sign-parts N sign-mode)])
|
||
(string-append pre-sign-part padded-digits-part post-sign-part)))))
|
||
|
||
(define (catne N
|
||
#:who [who 'catne]
|
||
#:sign [sign-mode #f]
|
||
#:base [base 10]
|
||
#:precision [precision 5]
|
||
#:format-exponent [format-exponent #f]
|
||
#:pad-digits-to [pad-digits-to 1]
|
||
#:digits-padding [digits-padding " "])
|
||
(let*-values ([(upper? base) (normalize-base base)]
|
||
[(exactly? precision) (normalize-precision precision)])
|
||
(let* ([N-abs (abs N)]
|
||
[digits-part
|
||
(%exponential N-abs base format-exponent precision exactly?)]
|
||
[padded-digits-part
|
||
(%pad digits-part
|
||
#:pad-to pad-digits-to
|
||
#:align 'right
|
||
#:left-padding digits-padding
|
||
#:right-padding #f)])
|
||
(let-values ([(pre-sign-part post-sign-part) (get-sign-parts N sign-mode)])
|
||
(string-append pre-sign-part padded-digits-part post-sign-part)))))
|
||
|
||
(define (normalize-base base)
|
||
(if (pair? base)
|
||
(values (eq? (car base) 'up) (cadr base))
|
||
(values #f base)))
|
||
|
||
(define (normalize-precision precision)
|
||
(if (pair? precision)
|
||
(values #t (cadr precision))
|
||
(values #f precision)))
|
||
|
||
(define (%positional N-abs base upper? precision exactly?)
|
||
(let* ([Nw (inexact->exact (floor N-abs))]
|
||
[Nf (- N-abs Nw)]
|
||
[whole-part (number->string* Nw base upper?)]
|
||
[frac-part
|
||
(let* ([Nf* (inexact->exact (round (* Nf (expt base precision))))])
|
||
(cond [(and exactly? (= precision 0)) ""]
|
||
[exactly? (number->fraction-string Nf* base upper? precision)]
|
||
[(= Nf* 0) #f]
|
||
[else
|
||
(let-values ([(needed-precision Nf**)
|
||
(let loop ([np precision] [Nf* Nf*])
|
||
(let-values ([(q r) (quotient/remainder Nf* base)])
|
||
(cond [(zero? r) (loop (sub1 np) q)]
|
||
[else (values np Nf*)])))])
|
||
(number->fraction-string Nf** base upper? needed-precision))]))]
|
||
[digits-part
|
||
(cond [frac-part (string-append whole-part "." frac-part)]
|
||
[else whole-part])])
|
||
digits-part))
|
||
|
||
(define (%exponential N-abs base format-exponent significand-precision exactly?)
|
||
(define-values (N* e-adjust actual-precision)
|
||
(scale N-abs base significand-precision exactly?))
|
||
;; hack: from 1234 want "1.234"; convert to "1234", mutate to ".234" after saving "1"
|
||
(let* ([digits (number->string* N* base #f)]
|
||
[leading-digit (string (string-ref digits 0))]
|
||
[exponent (- significand-precision e-adjust)])
|
||
(string-set! digits 0 #\.)
|
||
(string-append leading-digit
|
||
(if (or exactly? (positive? actual-precision)) digits "")
|
||
(cond [(procedure? format-exponent)
|
||
(format-exponent exponent)]
|
||
[else
|
||
(string-append
|
||
(cond [(string? format-exponent) format-exponent]
|
||
[(= base 10) "e"]
|
||
[else (format "×~s^" base)])
|
||
(if (negative? exponent) "-" "+")
|
||
(%pad (number->string (abs exponent))
|
||
#:pad-to 2
|
||
#:align 'right
|
||
#:left-padding "0"
|
||
#:right-padding #f))]))))
|
||
|
||
(define (scale N-abs base significand-precision exactly?)
|
||
(if (zero? N-abs)
|
||
(values 0 0 (if exactly? significand-precision 0))
|
||
(scale/nz N-abs base significand-precision exactly?)))
|
||
|
||
(define (scale/nz N-abs base significand-precision exactly?)
|
||
(let* ([N (inexact->exact N-abs)]
|
||
[normalized-min (expt base significand-precision)]
|
||
[normalized-max (* base normalized-min)])
|
||
(let*-values ([(N*0 e-adjust0)
|
||
(let ([e-est (- significand-precision
|
||
(inexact->exact (floor (/ (log N-abs) (log base)))))])
|
||
(values (* N (expt base e-est)) e-est))]
|
||
[(N* e-adjust)
|
||
(let loop ([N N*0] [e e-adjust0] [r #f])
|
||
;; if r != #f, then N is integer
|
||
(cond [(< N normalized-min)
|
||
(loop (* N base) (add1 e) #f)]
|
||
[(>= N normalized-max)
|
||
(let-values ([(q r) (quotient/remainder (floor N) base)])
|
||
(loop q (sub1 e) r))]
|
||
[else
|
||
(let ([N* (if r
|
||
(if (>= (* 2 r) base) (add1 N) N)
|
||
(round* N))])
|
||
(cond [(< N* normalized-max)
|
||
(values N* e)]
|
||
[else (loop N* e #f)]))]))]
|
||
[(N* actual-precision)
|
||
(if exactly?
|
||
(values N* significand-precision)
|
||
(let loop ([N N*] [p significand-precision])
|
||
(let-values ([(q r) (quotient/remainder N base)])
|
||
(cond [(zero? r) (loop q (sub1 p))]
|
||
[else (values N p)]))))])
|
||
(values N* e-adjust actual-precision))))
|
||
|
||
;; ----
|
||
|
||
(define (get-sign-parts N sign-mode)
|
||
(define (get indicator)
|
||
(if (string? indicator)
|
||
(values indicator "")
|
||
(values (car indicator) (cadr indicator))))
|
||
(let ([indicator-table
|
||
(case sign-mode
|
||
((#f) '("" "" "-"))
|
||
((+) '("+" "" "-"))
|
||
((++) '("+" "+" "-"))
|
||
((parens) '("" "" ("(" ")")))
|
||
(else sign-mode))])
|
||
(cond [(or (negative? N) (eqv? -0.0 N))
|
||
(get (caddr indicator-table))]
|
||
[(zero? N)
|
||
(get (cadr indicator-table))]
|
||
[else ;; positive
|
||
(get (car indicator-table))])))
|
||
|
||
(define (number->string* N base upper?)
|
||
(cond [(memv base '(2 8 10 16))
|
||
(let ([s (number->string N base)])
|
||
(if (and (= base 16) upper?)
|
||
(string-upcase s)
|
||
s))]
|
||
[(zero? N)
|
||
(string #\0)]
|
||
[else
|
||
(apply string
|
||
(let loop ([N N] [digits null])
|
||
(cond [(zero? N) (reverse digits)]
|
||
[else (let-values ([(q r) (quotient/remainder N base)])
|
||
(loop q (cons (get-digit r upper?) digits)))])))]))
|
||
|
||
(define (number->fraction-string N base upper? precision)
|
||
(let ([s (number->string* N base upper?)])
|
||
(string-append (make-string (- precision (string-length s)) #\0) s)))
|
||
|
||
;; Allow base up to 36!
|
||
(define (get-digit d upper?)
|
||
(cond [(< d 10) (integer->char (+ d (char->integer #\0)))]
|
||
[else (integer->char (+ (- d 10) (char->integer (if upper? #\A #\a))))]))
|
||
|
||
(define (round* x) ;; round is round-to-even :(
|
||
(if (integer? x)
|
||
x
|
||
(+ (truncate x)
|
||
(if (even? (truncate (+ x x))) 0 1))))
|