fmt

https://github.com/mmontone/fmt.git

git clone 'https://github.com/mmontone/fmt.git'

(ql:quickload :fmt)
6

FMT

FMT is an extensible text formatting facility for Common Lisp. It is meant to do the same things Common Lisp FORMAT function does, but instead of using a control-string for formatting directives, it uses s-expressions.

Invocation

with-fmt

A macro that expands its body to formatting commands.

destination can be:

If no destination is given, then *fmt-destination* is the default destination.

Body forms are either some lisp object, like strings, numbers, characters, etc; or some formatting operation. A formatting operation has the form of a list beggining with the operation keyword, like (:aesthetic message), (:join "," list), etc. Forms appearing in body are formatted one after the other.

Example:

(with-fmt ()
   "Hello world"
   #\newline
   (:join "," (list 1 2 3)))

prints:

Hello world
1,2,3

fmt

This macro is almost exactly to with-fmt, but with a different syntax, with makes it look very similar to Common Lisp format function.

Example:

(fmt nil "Hello" \#space "world")
(fmt t (:s (list 1 2 3)))

fmt*

Both fmt and with-fmt are macros and compile formatting directives to code that writes on the destination stream at compile-time. fmt* is a function, and interprets the formatting clauses given to it at run-time. This can be useful if you need a function to pass around, or you need extra flexibility in the formatting spec form.

Example:

(fmt* nil "Hello" #\space "world")
(fmt* nil (if t `(:d ,22) `(:f ,23.44)))

Note that some control flow operations (like :do and :if are not available in interpreted mode).

Printer operations

Aesthetic (:a, :aesthetic)

The aesthetic operation is the equivalent of Common Lisp FORMAT's \~A directive.

Example:

(fmt nil (:a (list :foo :bar :baz)))

returns "(FOO BAR BAZ)"

Standard (:s, :std, :standard)

The standard operation is the equivalent of Common Lisp FORMAT's \~S directive.

Example:

(fmt nil (:s (list :foo :bar :baz)))

returns "(:FOO :BAR :BAZ)"

Special operations

Escaping (:esc and :fmt)

Use the :esc directive for disabling formatting in a particular place.

For instance:

(fmt nil "Hello" #\space (:esc "beautiful" #\space) "world")

returns "hello world"

It's important to note that the code inside :esc is not removed completly, it is executed, but its result is not formatted. You can see that in the macroexpansion of the above code:

(WITH-FMT-DESTINATION (#:STREAM925 NIL)
  (MACROLET ((:FMT (&REST CLAUSES)
            `(FMT ,'#:STREAM925 ,@CLAUSES)))
  (WRITE-STRING "Hello" #:STREAM925)
  (WRITE-CHAR #\  #:STREAM925)
  (PROGN "beautiful" #\ )
  (WRITE-STRING "world" #:STREAM925)))

This is useful in combination with the :fmt directive, that reenables formatting inside escaped forms:

(fmt nil 
     (:a "start")
 #\newline
 (:esc 
   (loop for x in (list 1 2 3)
   do (:fmt (:s x))))
 #\newline
 (:a "end"))

In the above example the output of the loop is not formatted as it is enclosed in an :esc; but the :fmt operation inside the loops makes sure each of the elements of the list is formatted.

Control flow operations

Conditional (:when and :if)

Conditional control flow can be controlled via :when and :if operations.

:when is the simplest of the two and executes its body when the condition given is true.

Syntax:

(:when condition &body body)

Example:

(let ((cond t))
    (fmt nil (:when cond "yes"))) ;=> "yes"

(let ((cond nil))
 (fmt nil (:when cond "yes"))) ;=> ""

:if has an else branch.

Syntax:

(:if condition &body body)

The else branch is indicated with the :else keyword.

Example:

(let ((list (list 1 2 3)))
  (fmt nil (:if (not list)
                 "none"
         :else
         (:join "," list)))) ;=> "1,2,3"

 (let ((list (list)))
   (fmt nil (:if (not list)
          "none"
          :else
          (:join "," list)))) ;=> "none"

Note: :if is not implemented in interpreter mode, so it cannot be used in fmt* function.

Iteration (:do)

To iterate a list formatting its elements, there's the :do operation.

Syntax:

(:do (var list) &body body)

Example:

(fmt nil (:do (item (list 1 2 3))
              (:s item))) ;=> "123"

Note: :do is not implemented in interpreter mode, so it cannot be used in fmt* function.

Repetition (:times)

Repeat formatting N number of times

Syntax:

(:times clause n)

Example:

(fmt nil (:times #\newline 5))

More complex control flow

Just use lisp with :esc and :fmt for more complex control flow.

Example:

(let ((list (list 1 2 3)))
  (fmt nil (:esc (if (not list)
                   (:fmt "No elements")
                   (loop for x in (butlast list)
                         do (:fmt (:a x) "; ")
                         finally (:fmt (:a (car (last list)))))))))           

Other operations

Join (:join)

Joins the elements of its list argument using a separator.

Syntax:

(:join separator list &optional format)

separator can be either be a character or a string. list is of course the list of elements to join. format, if present, is a command for formatting the list elements. If it is not present :s is used. _ is bound to the list element.

Example:

(fmt nil (:join ", " (list "foo" "bar" "baz"))) ;=> "foo, bar, baz"
(fmt nil (:join #\, (list "foo" "bar"))) ;=> "foo,bar"
(fmt nil (:join (", " " and ")
             (list "foo" "bar" "baz"))) ;=> "foo, bar and baz"
(fmt nil (:join ", " (list "a" "b" "c") (:a _ :up))) ;=> "A, B, C"

Common Lisp format (:format)

It is possible to just invoke Common Lisp format function to write on the current destination.

Syntax:

(:format control-string &rest args)

Example:

(let ((list (list "foo" "bar" "baz")))
 (fmt nil (:format "~{~A~^, ~}" list))) ;=> "foo, bar, baz"

(let ((list (list :foo :bar :baz)))
 (fmt nil (:format "~{~S~^, ~}" list))) ;=> ":FOO, :BAR, :BAZ"

Filters

Filters are particular operations or functions that modify the input before it gets formatted.

aesthetic and standard operations support filters.

Filters appear at the end of the :a or :s operations:

(:a arg &rest filters)
(:s arg &rest filters)

Filters can be either a function reference or some previously defined filter.

Example:

(fmt nil (:a "foo" :upcase)) ;=> "FOO"
(fmt nil (:s "foo" #'string-upcase)) ;=> "FOO"
(fmt nil (:a "  foo  " (:trim #\ ) :up)) ;=> "FOO"

Some very common filters are :upcase or :up, :downcase or :down, :trim, etc

Radix control

Radix (:r, :radix)

Prints argument in radix. Equivalent to Common Lisp FORMAT's \~R

Syntax:

(:r n &optional (interpretation :cardinal))

interpretation can be :cardinal, :ordinal, :roman and :old-roman.

Examples:

(fmt nil (:r 4)) ;=> "four"
(fmt nil (:r 4 :cardinal)) ;=> "four"
(fmt nil (:r 4 :ordinal)) ;=> "fourth"
(fmt nil (:r 4 2)) ;=> "100"
(fmt nil (:r 4 :roman)) ;=> "IV"
(fmt nil (:r 4 :old-roman)) ;=> "IIII"

Extending FMT

Custom formatting operations definition

Custom formatting operations can be defined via define-format-operation macro.

It has the following syntax:

(define-format-operation operation-name
   (:keywords keyword-list)
   (:format (destination clause)
         &body body)
   (:compile (destination clause)
         &body body)
   (:documentation docstring))

Where:

For example, we can define a time formatting operation

(define-format-operation time
    (:keywords (:time))
    (:format (destination clause)
     (destructuring-bind (_ timestamp &optional (format local-time:+iso-8601-format+)) clause
         (declare (ignore _))
         (let ((local-time (etypecase timestamp
                 (integer
                  (local-time:universal-to-timestamp timestamp))
                 (local-time:timestamp
                  timestamp))))
           (local-time:format-timestring destination 
                         local-time
                         :format format))))
    (:compile (destination clause)
      (destructuring-bind (_ timestamp &optional (format 'local-time:+iso-8601-format+)) clause
          (declare (ignore _))
          (alexandria:with-unique-names (local-time)
            (alexandria:once-only (timestamp)
            `(let ((,local-time (etypecase ,timestamp
                      (integer
                       (local-time:universal-to-timestamp ,timestamp))
                      (local-time:timestamp
                       ,timestamp))))
           (local-time:format-timestring ,destination 
                         ,local-time
                         :format ,format))))))
    (:documentation "Time formatting"))

And then we can use the new operation like this:

(fmt* nil `(:time ,(get-universal-time)))

That goes through the operation's :format code.

(fmt nil (:time (get-universal-time)))

Which transforms code using the operation's :compile code.

Custom filters definition

Filters are defined via define-format-filter macro, very similarly to format operations.

Syntax:

(define-format-filter filter-name
   (:keywords keyword-list)
   (:apply (arg)
         &body body)
   (:compile (arg)
         &body body)
   (:documentation docstring))

Where:

For example, the :trim filter is defined like this:

(define-format-filter trim
   (:keywords (:trim))
   (:apply (arg &rest chars)
       (string-trim (or chars (list #\ )) arg))
   (:compile (arg &rest chars)
     (let ((chars-bag (or chars (list #\ ))))
       `(string-trim ',chars-bag ,arg)))
   (:documentation "String trim filter"))

Filters can be used in :a and :s operations afterwards:

(fmt nil (:a "  hello  " :trim)) ;=> "hello"
(fmt nil (:a "//hello" (:trim #\/))) ;=> "hello"