Login
7 branches 0 tags
Ben (X13/Arch) Simplified things a little 0643405 9 days ago 1260 Commits
nujel / stdlib / string / fmt.nuj
;;; Nujel - Copyright (C) 2020-2021 - Benjamin Vincent Schulenburg
;;; This project uses the MIT license, a copy should be included under /LICENSE
;;;
;;; Subroutines and macros for formatted text output

(def fmt (let*
           (def fmt/format-arg/default {:align :right
                               :debug #f
                               :base #f
                               :width #nil
                               :padding-char " "})

           (defn fmt/find-non-digit-from-right (s i)
                 (if (< i 0)
                     -1
                     (do (def char (ref s i))
                         (if (and (>= char #\0) (<= char #\9))
                             (fmt/find-non-digit-from-right s (- i 1))
                             i))))

           (defn fmt/parse-spec (opts spec)
                 (if (zero? (:length spec))
                     opts
                     (case (ref spec (- (:length spec) 1))
                           ((#\0 #\1 #\2 #\3 #\4 #\5 #\6 #\7 #\8 #\9)
                            (def next-non-digit (fmt/find-non-digit-from-right spec (- (:length spec) 1)))
                            (def number (:cut spec (+ 1 next-non-digit) (:length spec)))
                            (set! opts :width (read/single number))
                            (when (= #\0 (ref number 0))
                              (set! opts :padding-char "0"))
                            (fmt/parse-spec opts (:cut spec 0 (+ 1 next-non-digit))))
                           (#\? (fmt/parse-spec (set! opts :debug #t)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\X (fmt/parse-spec (set! opts :base :HEXADECIMAL)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\x (fmt/parse-spec (set! opts :base :hexadecimal)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\d (fmt/parse-spec (set! opts :base :decimal)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\o (fmt/parse-spec (set! opts :base :octal)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\b (fmt/parse-spec (set! opts :base :binary)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\< (fmt/parse-spec (set! opts :align :left)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\^ (fmt/parse-spec (set! opts :align :center)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\> (fmt/parse-spec (set! opts :align :right)
                                                (:cut spec 0 (- (:length spec) 1))))
                           (#\. (fmt/parse-spec (set! opts :precision (ref opts :width))
                                                (:cut spec 0 (- (:length spec) 1))))
                           (otherwise (exception :format-error "Unknown form-spec option" spec)))))

           (defn fmt/debug (opts)
                 (if-not (ref opts :debug)
                         opts
                         (set! opts :argument (list string/write (ref opts :argument)))))

           (defn fmt/number-format (opts)
                 (case (ref opts :base)
                       (:binary      (set! opts :argument (list int->string/binary  (ref opts :argument))))
                       (:octal       (set! opts :argument (list int->string/octal   (ref opts :argument))))
                       (:decimal     (set! opts :argument (list int->string/decimal (ref opts :argument))))
                       (:hexadecimal (set! opts :argument (list int->string/hex     (ref opts :argument))))
                       (:HEXADECIMAL (set! opts :argument (list int->string/HEX     (ref opts :argument))))
                       (otherwise opts)))

           (def fmt/number-format-prefixex {:binary "#b"
                                   :octal "#o"
                                   :decimal "#d"
                                   :hexadecimal "#x"
                                   :HEXADECIMAL "#x"})

           (defn fmt/number-format-prefix (opts)
                 (if (or (not (ref opts :debug)) (not (ref opts :base))) opts
                     (-> (if (ref fmt/number-format-prefixex (ref opts :base))
                             (set! opts :argument (list cat
                                                             (ref fmt/number-format-prefixex (ref opts :base))
                                                             (ref opts :argument)))
                             opts)
                         (set! :debug #f))))

           (defn fmt/add-padding (opts)
                 (if-not (ref opts :width)
                         opts
                         (set! opts :argument (list (case (ref opts :align)
                                                               (:right   pad-start)
                                                               (:center pad-middle)
                                                               (:left  pad-end))
                                                         (ref opts :argument)
                                                         (if (and (ref opts :debug) (ref opts :base))
                                                             (- (ref opts :width) 2)
                                                             (ref opts :width))
                                                         (ref opts :padding-char)))))

           (defn fmt/precision (opts)
                 (if-not (ref opts :precision)
                         opts
                         (set! opts :argument (list string/round
                                                         (ref opts :argument)
                                                         (ref opts :precision)))))

           (defn fmt/truncate (opts)
                 (if-not (ref opts :width)
                         opts
                         (set! opts :argument (list :cut
                                                         (ref opts :argument)
                                                         0
                                                         (+ 1 (ref opts :width))))))

           (defn fmt/output (opts)
                 (ref opts :argument))

           (defn fmt/format-arg (spec argument)
                 (-> (set! (fmt/parse-spec (:clone fmt/format-arg/default) spec) :argument argument)
                     fmt/number-format
                     fmt/precision
                     fmt/add-padding
                     fmt/truncate
                     fmt/number-format-prefix
                     fmt/debug
                     fmt/output))

           (defn fmt/valid-argument? (argument)
                 (or (int? argument) (symbol? argument)))

           (defn fmt/arg-sym (v)
                 (case (:type-name v)
                       (:int (fmt/arg-sym (cat "fmt-arg-" (:string v))))
                       (:symbol v)
                       (:string (:symbol v))
                       (otherwise (exception :type-error "Invalid fmt argument name" v))))

           (defn fmt/expr (expr arguments-used opts)
                 (when-not (string? expr)
                           (exception :format-error "fmt needs a string literal as a first argument, since it is implemented as a macro" expr))
                 (def split-expr  (split expr ":"))
                 (def argument    (car split-expr))
                 (def format-spec (or (cadr split-expr) ""))

                 (if (= "" argument)
                     (do (tree/-- opts :expr-count)
                         (set! arguments-used (ref opts :expr-count) #t)
                       (fmt/format-arg format-spec (fmt/arg-sym (ref opts :expr-count))))
                     (let ((read-vals (read argument)))
                          (when (cdr read-vals)
                            (exception :format-error "Format argument specifier contains more than a single atom" argument))
                          (when-not (fmt/valid-argument? (car read-vals))
                                    (exception :format-error "Format argument specifier should be either an integer or a symbol" argument))

                          (when (int? (car read-vals))
                            (when (or (< (car read-vals) 0) (>= (car read-vals) (:length arguments-used)))
                              (exception :format-error "fmt numbered argument is out of bounds" argument))
                            (set! arguments-used (car read-vals) #t))
                          (fmt/format-arg format-spec (fmt/arg-sym (car read-vals))))))

           (defmacro fmt (format-string . args)
                     "Return a formatted string"
                     (when-not (string? format-string)
                               (exception :type-error "fmt needs a string literal as a first argument, since it is implemented as a macro" format-string))
                     (def cuts #nil)
                     (dotimes (i (:length format-string))
                       (case (ref format-string i)
                             (#\{ (do (when (int? (car cuts)) (exception :format-error "fmt placeholders can't be nested" format-string))
                                      (set! cuts (cons i cuts))))
                             (#\} (do (when-not (int? (car cuts)) (exception :format-error "fmt expects all brackets to be closed" format-string))
                                      (set! cuts (cons (cons (car cuts) i) (cdr cuts)))))))
                     (when (int? (car cuts)) (exception :format-error "fmt placeholders can't be nested" format-string))
                     (def expr-list #nil)
                     (def last-pos (:length format-string))
                     (def arguments-used (-> (:alloc Array (:length args)) (array/fill! #f)))

                     (def opts {:expr-count (:length arguments-used)})
                     (doseq (c cuts)
                            (def lit (:cut format-string (+ (cdr c) 1) last-pos))
                            (when-not (= "" lit) (set! expr-list (cons lit expr-list)))
                            (def expr (fmt/expr (:cut format-string (+ 1 (car c)) (cdr c)) arguments-used opts))
                            (set! expr-list (cons expr expr-list))
                            (set! last-pos (car c)))
                     (when (> last-pos 0)
                       (def lit (:cut format-string 0 last-pos))
                       (set! expr-list (cons lit expr-list)))
                     (dotimes (i (:length arguments-used))
                       (when-not (ref arguments-used i)
                                 (exception :format-error "fmt expects all arguments to be used" (list format-string (ref args i)))))
                     (def expr (if (cdr expr-list)
                                   (cons 'cat expr-list)
                                   (if (string? (car expr-list))
                                       (car expr-list)
                                       (cons 'string/display expr-list))))
                     (def fmt/args/map-fun/count 0)
                     (defn fmt/args/map-fun (arg)
                           (def s (:symbol (cat "fmt-arg-" (:string fmt/args/map-fun/count))))
                           (inc! fmt/args/map-fun/count)
                           (list 'def s arg))
                     (if args
                         `(let* ~@(map args fmt/args/map-fun)
                                ~expr)
                         expr))))

(defmacro pfmt (format-string . args)
          "Print a formatted string"
          `(print (fmt ~format-string ~@args)))

(defmacro efmt (format-string . args)
          "Print a formatted string"
          `(error (fmt ~format-string ~@args)))

(defmacro pfmtln (format-string . args)
          "Print a formatted string"
          `(println (fmt ~format-string ~@args)))

(defmacro efmtln (format-string . args)
          "Print a formatted string"
          `(errorln (fmt ~format-string ~@args)))