3

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?

madstap
  • 1,552
  • 10
  • 21
  • It's probably not a duplicate, but the answers to [In common lisp how can I format a floating point and specify grouping, group char and decimal separator char](http://stackoverflow.com/q/35012859/1281433) are probably relevant here. – Joshua Taylor Sep 06 '16 at 11:30

2 Answers2

6

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

coredump
  • 37,664
  • 5
  • 43
  • 77
  • I figured it'd be a simple format string! Something like "~:D$" or whatever. I guess I'll just use that java answer you linked to. Thanks. – madstap Sep 05 '16 at 18:04
  • 1
    @madstap Format does so many things that we might forget it does not do everything ;-) – coredump Sep 05 '16 at 18:12
0

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.

Mars
  • 8,689
  • 2
  • 42
  • 70
  • What bothers me is "round", which rounds to the nearest integer (down or *up*). With an integer greater than the original number, you will have a negative decimal part and the whole printed string will be a mess. – coredump Sep 05 '16 at 21:40
  • @coredump [truncate and ftruncate](http://www.lispworks.com/documentation/HyperSpec/Body/f_floorc.htm) round toward zero. That's the behavior you'd want, right? – Joshua Taylor Sep 06 '16 at 01:57
  • 1
    @Mars ROUND (and TRUNCATE, see previous comment) return multiple values. Instead of taking the difference, you can do `(multiple-value-bind (quot rem) (truncate x) ...)`. – Joshua Taylor Sep 06 '16 at 02:00
  • For even a bit more flexibility, see [In common lisp how can I format a floating point and specify grouping, group char and decimal separator char](http://stackoverflow.com/q/35012859/1281433). – Joshua Taylor Sep 06 '16 at 11:31
  • 1
    Thanks @coredump for pointing out the mistake of using `round`. – Mars Sep 07 '16 at 15:42
  • Thanks @JoshuaTaylor for those comments. I'd forgotten that these functions are multiple-value functions in CL. Since OP was mainly asking about Clojure, I include the CL def only for comparison and clarification, and don't want to provide alternatives that would be very unlike the Clojure version. However, I've added a reference to your comments so that people can follow up on other ways to do this in CL. – Mars Sep 07 '16 at 15:45
  • @Mars Thanks. Btw, I changed my answer thanks to your answer to avoid printing the leading zero. – coredump Sep 07 '16 at 16:11
  • That `make-money` function doesn't work. In the example's result ("123,456,7890.12") the group just before the decimal point has four digits! – Xavi Apr 03 '17 at 20:07
  • 1
    Thanks @Xavi. You're right! I thought I'd try to fix it before replying, but it's not something I'm going to have time to look at in the next month, at least, so I'll just take my downvotes if they come. I added warning, though. – Mars Apr 15 '17 at 04:50