自己動手列印整數

Liutos發表於2022-05-02

在 Common Lisp 中,列印整數一般用函式format。例如,上面的程式碼會往標準輸出中列印出233這個數字:

(format t "~D" 233)

除此之外,format還可以控制列印內容的寬度、填充字元、是否列印正負號等方面。例如,要控制列印的內容至少佔據6列的話,可以用如下程式碼

(format t "~6D" 233)

如果不使用字串形式的 DSL,而是以關鍵字引數的方式來實現一個能夠達到同樣效果的函式format-decimal,程式碼可能如下:

(defun format-decimal (n
                       &key
                         mincol)
  "列印整數 N 到標準輸出。

MINCOL 如果不為 NIL,則表示所列印的內容至少要佔據的列數。"
  ;; 通過取餘的方式得到 N 的每一位並逐個入棧,之後出棧的順序就是從左到右列印的順序了。
  (let ((digits '()))
    (cond ((zerop n)
           (push 0 digits))
          (t
           (do ((n n (truncate n 10)))
               ((zerop n))
             (push (rem n 10) digits))))
    ;; 列印出填充用的空格。
    (when (and (integerp mincol) (> mincol (length digits)))
      (dotimes (i (- mincol (length digits)))
        (declare (ignorable i))
        (princ #\Space)))

    (dolist (digit digits)
      (princ (code-char (+ digit (char-code #\0)))))))

(format-decimal 233 :mincol 6)

如果要求用數字0而不是空格來填充左側的列,用format的寫法如下:

(format t "~6,'0D" 233)

format-decimal想要做到同樣的事情,可以這麼寫:

(defun format-decimal (n
                       &key
                         mincol
                         (padchar #\Space))
  "列印整數 N 到標準輸出。

MINCOL 如果不為 NIL,則表示所列印的內容至少要佔據的列數。
PADCHAR 表示式為了填充多餘的列時所用的字元。"
  (check-type mincol (or integer null))
  (check-type padchar character)
  ;; 通過取餘的方式得到 N 的每一位並逐個入棧,之後出棧的順序就是從左到右列印的順序了。
  (let ((digits '()))
    (cond ((zerop n)
           (push 0 digits))
          (t
           (do ((n n (truncate n 10)))
               ((zerop n))
             (push (rem n 10) digits))))
    ;; 列印出填充用的空格。
    (when (and (integerp mincol) (> mincol (length digits)))
      (dotimes (i (- mincol (length digits)))
        (declare (ignorable i))
        (princ padchar)))

    (dolist (digit digits)
      (princ (code-char (+ digit (char-code #\0)))))))

(format-decimal 233 :mincol 6 :padchar #\0)

-D預設是不會列印非負整數的符號的,可以用修飾符@來修改這個行為。例如,(format t "~6,'0@D" 233)會列印出00+233。稍微修改一下就可以在format-decimal中實現同樣的功能

(defun format-decimal (n
                       &key
                         mincol
                         (padchar #\Space)
                         signed)
  "列印整數 N 到標準輸出。

MINCOL 如果不為 NIL,則表示所列印的內容至少要佔據的列數。
PADCHAR 表示式為了填充多餘的列時所用的字元。"
  (check-type mincol (or integer null))
  (check-type padchar character)
  (flet ((to-digits (n)
           ;; 通過取餘的方式得到 N 的每一位並逐個入棧,之後出棧的順序就是從左到右列印的順序了。
           (let ((digits '()))
             (cond ((zerop n)
                    (push #\0 digits))
                   (t
                    (do ((n n (truncate n 10)))
                        ((zerop n))
                      (push (code-char (+ (rem n 10) (char-code #\0))) digits))))
             digits)))
    ;; 通過取餘的方式得到 N 的每一位並逐個入棧,之後出棧的順序就是從左到右列印的順序了。
    (let ((digits (to-digits (abs n))))
      (when (or signed (< n 0))
        (push (if (< n 0) #\- #\+) digits))
      ;; 列印出填充用的空格。
      (when (and (integerp mincol) (> mincol (length digits)))
        (dotimes (i (- mincol (length digits)))
          (declare (ignorable i))
          (princ padchar)))

      (dolist (digit digits)
        (princ digit)))))

(format-decimal 233 :mincol 6 :padchar #\0 :signed t)

除了@之外,:也是一個~D的修飾符,它可以讓format每隔3個數字就列印出一個逗號,方便閱讀比較長的數字。例如,下列程式碼會列印出00+23,333

(format t "~9,'0@:D" 23333)

為此,給format-decimal新增一個關鍵字引數comma-separated來控制這一行為。

(defun format-decimal (n
                       &key
                         comma-separated
                         mincol
                         (padchar #\Space)
                         signed)
  "列印整數 N 到標準輸出。

COMMA-SEPARATED 如果為 T,則每列印3個字元就列印一個逗號。
MINCOL 如果不為 NIL,則表示所列印的內容至少要佔據的列數。
PADCHAR 表示填充多餘的列時所用的字元。
SIGNED 控制是否顯示非負整數的加號。"
  (check-type comma-separated boolean)
  (check-type mincol (or integer null))
  (check-type padchar character)
  (check-type signed boolean)
  (flet ((to-digits (n)
           ;; 通過取餘的方式得到 N 的每一位並逐個入棧,之後出棧的順序就是從左到右列印的順序了。
           (let ((digits '()))
             (cond ((zerop n)
                    (push #\0 digits))
                   (t
                    (do ((count 0 (1+ count))
                         (n n (truncate n 10)))
                        ((zerop n))
                      (when (and comma-separated (> count 0) (zerop (rem count 3)))
                        (push #\, digits))
                      (push (code-char (+ (rem n 10) (char-code #\0))) digits))))
             digits)))
    ;; 通過取餘的方式得到 N 的每一位並逐個入棧,之後出棧的順序就是從左到右列印的順序了。
    (let ((digits (to-digits (abs n))))
      (when (or signed (< n 0))
        (push (if (< n 0) #\- #\+) digits))
      ;; 列印出填充用的空格。
      (when (and (integerp mincol) (> mincol (length digits)))
        (dotimes (i (- mincol (length digits)))
          (declare (ignorable i))
          (princ padchar)))

      (dolist (digit digits)
        (princ digit)))))

(format-decimal -23333 :comma-separated t :mincol 9 :padchar #\0 :signed t)

事實上,列印分隔符的步長,以及作為分隔符的逗號都是可以定製的。例如,可以改為每隔4個數字列印一個連字元

(format t "~9,'0,'-,4@:D" 23333)

對於format-decimal來說這個修改現在很簡單了

(defun format-decimal (n
                       &key
                         (commachar #\,)
                         (comma-interval 3)
                         comma-separated
                         mincol
                         (padchar #\Space)
                         signed)
  "列印整數 N 到標準輸出。

COMMACHAR 表示當需要列印分隔符時的分隔符。
COMMA-INTERVAL 表示當需要列印分隔符時需要間隔的步長。
COMMA-SEPARATED 如果為 T,則每列印3個字元就列印一個逗號。
MINCOL 如果不為 NIL,則表示所列印的內容至少要佔據的列數。
PADCHAR 表示填充多餘的列時所用的字元。
SIGNED 控制是否顯示非負整數的加號。"
  (check-type commachar character)
  (check-type comma-interval integer)
  (check-type comma-separated boolean)
  (check-type mincol (or integer null))
  (check-type padchar character)
  (check-type signed boolean)
  (flet ((to-digits (n)
           ;; 通過取餘的方式得到 N 的每一位並逐個入棧,之後出棧的順序就是從左到右列印的順序了。
           (let ((digits '()))
             (cond ((zerop n)
                    (push #\0 digits))
                   (t
                    (do ((count 0 (1+ count))
                         (n n (truncate n 10)))
                        ((zerop n))
                      (when (and comma-separated (> count 0) (zerop (rem count comma-interval)))
                        (push commachar digits))
                      (push (code-char (+ (rem n 10) (char-code #\0))) digits))))
             digits)))
    ;; 通過取餘的方式得到 N 的每一位並逐個入棧,之後出棧的順序就是從左到右列印的順序了。
    (let ((digits (to-digits (abs n))))
      (when (or signed (< n 0))
        (push (if (< n 0) #\- #\+) digits))
      ;; 列印出填充用的空格。
      (when (and (integerp mincol) (> mincol (length digits)))
        (dotimes (i (- mincol (length digits)))
          (declare (ignorable i))
          (princ padchar)))

      (dolist (digit digits)
        (princ digit)))))


(format-decimal -23333 :commachar #\- :comma-interval 4 :comma-separated t :mincol 9 :padchar #\0 :signed t)

全文完。

閱讀原文

相關文章