madstap
madstap

Reputation: 1552

How to format money with cl-format (clojure implementation of common lisp format function)

I'm trying to use cl-format to format money. I want (f 12345.555) ;=> "12,345.56". I get the decimals with the format string "~$" and I get the comma separators with "~:D". How do I combine them?

Upvotes: 3

Views: 1587

Answers (2)

Mars
Mars

Reputation: 8854

Part of the problem is that the ~:d directive only adds commas when passed a whole number (whether it's a float or an integer), i.e. if there's anything other than zero after the decimal point, ~:d just prints out the number as is. That's true for CL's format as well as for Clojure's cl-format.

A solution is to split up the number into an integer and a decimal, and then format them separately. One way to do this would use a truncate function, which afaik, neither Clojure nor its standard libraries provides. Here's one way, using floor and ceil from clojure.math.numeric-tower. (Thanks to coredump for pointing out the bug in my earlier version.)

(defn truncate [x] 
  (if (neg? x)
    (ceil x)
    (floor x)))

(defn make-money [x]
  (let [int-part (truncate x)
        dec-part (- x int-part)]
    (cl-format nil "~:d~$" int-part dec-part)))

(make-money 123456789.123456789) ;=> "123,456,7890.12"

Note that this is only designed to work with positive numbers. (EDIT: As Xavi pointed out in a comment, this isn't a solution, since there's a 4-digit group after the last comment.)

That answers OP's question (EDIT: Not really--see above), but I'll note that in Common Lisp, ~$ behaves slightly differently; by default it prints out an initial zero before the decimal point (at least in the implementations I tried--not sure if this is standardized). This can be avoided by customizing the ~f directive--which works this way in Clojure, too (see Peter Seibel's introduction for details):

(defun make-money (x)
  (let* ((int-part (truncate x))
         (dec-part (- x int-part)))
    (format nil "~:d~0,2f" int-part dec-part)))

You can get unexpected results with this definition if the numbers are too big. I'm sure there are ways to avoid this problem by tweaking the definition, and in any event, as Joshua Taylor's comments point out, there are other, probably better ways to do this in Common Lisp.

Upvotes: 0

coredump
coredump

Reputation: 38924

With Common Lisp, I would recommand using cl-l10n which supports locales and defines ~N. Alternatively, you could roll your own:

(defun money (stream number colonp atsignp &optional (decimal-places 2))
  (multiple-value-bind (integral decimal) (truncate number)
    (format stream
            (concatenate 'string
                         "~"
                         (and colonp ":")
                         (and atsignp "@")
                         "D"
                         "~0,vf")
            integral
            decimal-places
            (abs decimal))))

(setf *read-default-float-format* 'double-float)

(format nil "~2:@/money/" 123456789.123456789)
=> "+123,456,789.12"

Now, for Clojure, it seems that ~/ is not yet supported by cl-format, so you can't directly replicate the above code. It is probably quicker to use a Java libray (see e.g. this question or this other one).

Upvotes: 6

Related Questions