#lang racket/base

;; Functions to format numbers, and data structures containing numbers.

(require racket/string racket/list racket/pretty racket/contract racket/match
         "math.rkt"
         "contract.rkt" "contract-doc.rkt")

(provide digits-for-range real->plot-label ->plot-label real->string/trunc)

(define (remove-trailing-zeros str)
  (let loop ([i  (string-length str)])
    (cond [(zero? i)  "0"]
          [(char=? #\0 (string-ref str (sub1 i)))  (loop (sub1 i))]
          [(char=? #\. (string-ref str (sub1 i)))  (substring str 0 (sub1 i))]
          [else  (substring str 0 i)])))

;; Returns the number of fractional digits needed to distinguish numbers [x-min..x-max]
(defproc (digits-for-range [x-min real?] [x-max real?]
                           [extra-digits exact-integer? 3]) exact-integer?
  (define range (abs (- x-max x-min)))
  (+ extra-digits (if (zero? range) 0 (- (floor-log10 range)))))

(define (int-str->e-str str)
  (define n (string-length str))
  (cond [(or (= 0 n) (string=? str "0"))  "0"]
        [else
         (define fst (substring str 0 1))
         (define rst (substring str 1 n))
         (format "~ae~a" (remove-trailing-zeros (format "~a.~a" fst rst)) (sub1 n))]))

(begin
  (require rackunit)
  (check-equal? (int-str->e-str "") "0")
  (check-equal? (int-str->e-str "0") "0")
  (check-equal? (int-str->e-str "10") "1e1"))

(define (frac-str->e-str str)
  (define n (string-length str))
  (let loop ([i 0])
    (cond [(= i n)  "0"]
          [(char=? #\0 (string-ref str i))  (loop (add1 i))]
          [else
           (define fst (substring str i (add1 i)))
           (define rst (substring str (add1 i) n))
           (cond [(= 0 (string-length rst))  (format "~ae~a" fst (- (add1 i)))]
                 [else  (format "~a.~ae~a" fst rst (- (add1 i)))])])))

(begin
  (require rackunit)
  (check-equal? (frac-str->e-str "") "0")
  (check-equal? (frac-str->e-str "0") "0")
  (check-equal? (frac-str->e-str "00") "0")
  (check-equal? (frac-str->e-str "1") "1e-1")
  (check-equal? (frac-str->e-str "01") "1e-2"))

(define (zero-string n)
  (list->string (build-list n (λ _ #\0))))

(defproc (real->plot-label [x real?] [digits exact-integer?]) any
  (cond
    [(zero? x)  "0"]
    [else
     (define front-sign (if (x . < . 0) "-" ""))
     (define mid-sign (if (x . < . 0) "-" "+"))
     (let* ([x  (abs (inexact->exact x))])
       ;; Round away any extra digits
       (define round-fac (expt 10 digits))
       (define y (/ (round (* x round-fac)) round-fac))
       ;; Parse the output of real->decimal-string
       (define-values (int-str frac-str)
         (match-let ([(list _ int-str frac-str)
                      (regexp-match #rx"(.*)\\.(.*)" (real->decimal-string y (max 0 digits)))])
           (values int-str (remove-trailing-zeros frac-str))))
       ;; Get scientific notation for the integer and fractional parts
       (define int-e-str (int-str->e-str int-str))
       (define frac-e-str (frac-str->e-str frac-str))
       ;(printf "int-str = ~v, frac-str = ~v~n" int-str frac-str)
       ;(printf "int-e-str = ~v, frac-e-str = ~v~n" int-e-str frac-e-str)
       (define int-zero? (string=? int-str "0"))
       (define frac-zero? (string=? frac-str "0"))
       (define int-e-zero? (string=? int-e-str "0"))
       (define frac-e-zero? (string=? frac-e-str "0"))
       ;; Build a list of possible output strings
       (define strs
         (list (cond [(and int-zero? frac-zero?)  "0"]
                     [int-zero?   (format "~a.~a" front-sign frac-str)]
                     [frac-zero?  (format "~a~a" front-sign int-str)]
                     [else        (format "~a~a.~a" front-sign int-str frac-str)])
               (cond [(and int-e-zero? frac-zero?)  "0"]
                     [int-e-zero?  (format "~a.~a" front-sign frac-str)]
                     [frac-zero?   (format "~a~a" front-sign int-e-str)]
                     [else         (format "~a(~a)~a.~a" front-sign int-e-str mid-sign frac-str)])
               (cond [(and int-zero? frac-e-zero?)  "0"]
                     [int-zero?     (format "~a~a" front-sign frac-e-str)]
                     [frac-e-zero?  (format "~a~a" front-sign int-str)]
                     [else          (format "~a~a~a(~a)" front-sign int-str mid-sign frac-e-str)])
               (cond [(and int-e-zero? frac-e-zero?)  "0"]
                     [int-e-zero?   (format "~a~a" front-sign frac-e-str)]
                     [frac-e-zero?  (format "~a~a" front-sign int-e-str)]
                     [else
                      (format "~a(~a)~a(~a)" front-sign int-e-str mid-sign frac-e-str)])))
       ;; Return the shortest possible output string
       (argmin string-length strs))]))

(defproc (->plot-label [a any/c] [digits exact-integer? 7]) string?
  (let loop ([a a])
    (cond [(string? a)   a]
          [(symbol? a)   (symbol->string a)]
          [(real? a)     (real->plot-label a digits)]
          [(list? a)     (string-append "(" (string-join (map loop a) " ") ")")]
          [(cons? a)     (string-append "(" (loop (car a)) " . " (loop (cdr a)) ")")]
          [(boolean? a)  (if a "true" "false")]
          [(char? a)     (list->string (list a))]
          [else  (pretty-format a)])))

;; Like real->decimal-string, but removes trailing zeros
(defproc (real->string/trunc [x real?] [e exact-integer?]) string?
  (remove-trailing-zeros (real->decimal-string x (max 0 e))))
