Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

common-lisp

Let's say I have the floating point number 1234.9

I want to format it as 1.234,90

Is there a format directive combination for that? ~D ,which can handle the grouping and the group char, handles only integers. ~F doesn't handle grouping at all. And none as far as I know can change the decimal point from . to ,

The only solution I see is to use ~D for the integer part digit grouping and concatenate it with , and the decimal part. Any better ideas?

like image 765
Paralife Avatar asked Jan 26 '16 11:01

Paralife


1 Answers

You can define a function to be called with tilde-slash, which most of the other answers have already done, but in order to get output similar to ~F, but with comma chars injected, and with the decimal point replaced, I think it's best to call get the output produced by ~F, and then modify it and write it to the string. Here's a way to do that, using a utility inject-comma that adds a comma character at specified intervals to a string. Here's the directive function:

(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)))

Here are some examples:

(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 :)

Here are the examples from coredump-'s answer, which actually helped me catch a bug with negative numbers:

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

Here's inject-comma, with some examples:

(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"
like image 93
Joshua Taylor Avatar answered Nov 11 '22 01:11

Joshua Taylor