在常见的 lisp 中,如何格式化浮点数并指定分组、群聊和小数点分隔符 char

In common lisp how can I format a floating point and specify grouping, group char and decimal separator char

假设我有浮点数 1234.9

我想将其格式化为1.234,90

是否有格式指令组合? ~D 可以处理分组和组字符,只处理整数。 ~F 根本不处理分组。据我所知 none 可以将小数点从 . 更改为 ,

我看到的唯一解决方案是将 ~D 用于整数部分数字分组并将其与 , 和小数部分连接起来。有更好的想法吗?

正如 jkiiski 的评论所建议的,您可以使用 ~/func/ 指令。

这只是一个示例,您可以使用函数进行更详细的说明:

CL-USER> (defun q(stream arg &rest args)
           (declare (ignore args))
           (format stream 
                   "~,,'.,:D,~a" 
                   (truncate arg)
                   (let ((float-string (format nil "~f" arg)))
                     (subseq float-string (1+ (position #\. float-string))))))
Q
CL-USER> (format t "~/q/~%" 1024.36)
1.024,36
NIL
CL-USER> (format t "~/q/~%" -1024.36)
-1.024,36
NIL

已编辑

第一个版本有 round,这是错误的,truncate 是正确的运算符。

如果您不介意拆分整数和小数部分,您可以执行以下操作:

(multiple-value-bind (int rest) (floor 1234.56)
   (let ((rest (round (* rest 1000))))
      (format t "~,,'.,:D,~D~%" int rest)))

1.234,560

四舍五入前的乘法表示您要打印逗号后的位数。不确定这种方法是否能很好地实现精确打印的自动控制,即 1.5 打印为“1,5”而不是“1,500”。

其他答案目前使用 round,这可能不是向上舍入(正数)或向下舍入(负数)时的预期行为。这是 ~/custom/ 指令的另一种方法,主要来自 Renzo 的回答。

(defun custom (stream number &rest args)
  (declare (ignore args))
  (multiple-value-bind (integer decimal) (truncate number)
    (format stream "~,,'.,:D~@[,~a~]"
            integer
            (unless (zerop decimal)
              (let ((decimal-string (princ-to-string (abs decimal))))
                (subseq decimal-string (1+ (position #\. decimal-string))))))))

测试

(loop for i in '(1034.34 -223.12 -10.0 10.0 14 324 1020231)
      collect (custom nil i))

=> ("1.034,33996582" "-223,11999512" "-10" "10" "14" "324" "1.020.231")

您可以定义一个用 tilde-slash 调用的函数,大多数其他答案已经完成了,但是为了获得类似于 ~F 的输出,但注入了逗号字符,并使用了小数点替换,我觉得最好调用get ~F产生的输出,然后修改写入字符串。这是一种方法,使用实用程序 inject-comma 以指定的间隔向字符串添加逗号字符。这是指令函数:

(defun print-float (stream arg colonp atp
                    &optional
                      (point-char #\.)
                      (comma-char #\,)
                      (comma-interval 3))
  "A function for printing floating point numbers, with an interface
suitable for use with the tilde-slash FORMAT directive.  The full form
is 

    ~point-char,comma-char,comma-interval/print-float/

The point-char is used in place of the decimal point, and defaults to
#\.  If : is specified, then the whole part of the number will be
grouped in the same manner as ~D, using COMMA-CHAR and COMMA-INTERVAL.
If @ is specified, then the sign is always printed."
  (let* ((sign (if (minusp arg) "-" (if (and atp (plusp arg)) "+" "")))
         (output (format nil "~F" arg))
         (point (position #\. output :test 'char=))
         (whole (subseq output (if (minusp arg) 1 0) point))
         (fractional (subseq output (1+ point))))
    (when colonp
      (setf whole (inject-comma whole comma-char comma-interval)))
    (format stream "~A~A~C~A"
            sign whole point-char fractional)))

这里有一些例子:

(progn 
  ;; with @ (for sign) and : (for grouping)
  (format t "~','.2@:/print-float/ ~%" 12345.6789) ;=> +1.23.45,679

  ;; with no @ (no sign) and : (for grouping)
  (format t "~'.'_3:/print-float/ ~%" 12345.678)   ;=>  12_345.678

  ;; no @ (but sign, since negative) and : (for grouping)
  (format t "~'.'_3:/print-float/ ~%" -12345.678)  ;=> -12_345.678

  ;; no @ (no sign) and no : (no grouping)
  (format t "~'.'_3@/print-float/ ~%" 12345.678))  ;=> +12345.678 (no :)

以下是来自 的例子,它实际上帮助我发现了负数的错误:

CL-USER> (loop for i in '(1034.34 -223.12 -10.0 10.0 14 324 1020231)
            do (format t "~','.:/print-float/~%" i))
1.034,34
-223,12
-10,0
10,0
14,0
324,0
1.020.231,0
NIL

这里是inject-comma,还有一些例子:

(defun inject-comma (string comma-char comma-interval)
  (let* ((len (length string))
         (offset (mod len comma-interval)))
    (with-output-to-string (out)
      (write-string string out :start 0 :end offset)
      (do ((i offset (+ i comma-interval)))
          ((>= i len))
        (unless (zerop i)
          (write-char comma-char out))
        (write-string string out :start i :end (+ i comma-interval))))))
(inject-comma "1234567" #\, 3)
;;=> "1,234,567"

(inject-comma "1234567" #\. 2)
;;=> "1.23.45.67"

我已经找到了这个正数的小解决方案。

(defun comma-point (stream arg &rest args)
  (declare (ignore args))
  (multiple-value-bind (i r) (truncate arg)
    (format stream "~,,',,:D.~2,'0D" i (truncate (* 100 r)))))
;;                      ^   ^
;;                      |   `Decimal point
;;                      `Thousands separator

(defun point-comma (stream arg &rest args)
  (declare (ignore args))
  (multiple-value-bind (i r) (truncate arg)
    (format stream "~,,'.,:D,~2,'0D" i (truncate (* 100 r)))))

(defun space-comma (stream arg &rest args)
  (declare (ignore args))
  (multiple-value-bind (i r) (truncate arg)
    (format stream "~,,' ,:D,~2,'0D" i (truncate (* 100 r)))))

检测人数:

(dolist (value '(1034.34 -223.12 -10.0 10.0 14 324 1020231.099))
  (format t "~16@A" (format nil "~/comma-point/" value))
  (format t "~16@A" (format nil "~/point-comma/" value))
  (format t "~16@A~%" (format nil "~/space-comma/" value)))

;;        1,034.33        1.034,33       1 034,33
;;        -223.-11        -223,-11       -223,-11
;;          -10.00          -10,00         -10,00
;;           10.00           10,00          10,00
;;           14.00           14,00          14,00
;;          324.00          324,00         324,00
;;    1,020,231.12    1.020.231,12   1 020 231,12

第二个测试数字表明它不适用于负数 (-223.11 => -223,-11)。此外,使用 truncate(或 other similar functions)意味着会出现准确性损失,如上次测试编号所示(1020231.099 => 1.020.231,12)。