Skip to content

splittist/printfcl

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

printfcl

A configurable implementation of printf in Common Lisp.

Quickstart

Clone this repository into your quicklisp/local-projectss directory, then (ql:quickload "printfcl"), and switch into the PRINTFCL package.

PRINTFCL> (printf "pi = %.5f" (* 4 (atan 1.0)))

pi = 3.14159
12

PRINTFCL> (sprintf "%10s %-10s" "Hello" "World")

"     Hello World     "

PRINTFCL> (fprintf t "%A" 123.45)

0XF.6E666P+3
12

Introduction

Sometimes you just want to reproduce the effect of C-style printf output without having to produce your own CL:FORMAT string. printfcl reproduces the effect of printf conversion on lisp objects. Many of the standard conversion specifiers are supported.

printfcl is intended to be configurable. printfcl is not intended to be a replacement for CL:FORMAT.

printfcl has no dependencies and an MIT license.

Functions

The printf-family functions are:

function PRINTF format-string &rest arguments

Prints the arguments, lisp objects (but see below), to *STANDARD-OUTPUT* as converted in accordance with the format-string (see below). Returns the number of characters printed.

function SPRINTF format-string &rest arguments

As for PRINTF, but returns the result as a string.

function FPRINTF stream format-string &rest arguments

As for PRINTF, but prints to stream. If stream is T, prints to *STANDARD-OUTPUT*. If stream is NIL, returns a string (as for SPRINTF). Unless stream is NIL, returns the number of characters printed.

function PARSE-HEX-FLOAT string &key (start 0) end junk-allowed

Attempts to parse string as an %a-style hexidecimal float (possibly surrounded by whitespace). The other arguments are as for CL:PARSE-INTEGER. Returns a double-float and, as a second value, the position in string where parsing stopped.

Format

The format-string is simply copied verbatim, except when a conversion specification is encountered. The following description is valid for the STANDARD-CONVERTER. (See below at Configuring for more information on what this means.)

A conversion specification begins with a % sign, followed by (in order):

  • Zero or more flag characters (-, +, 0, # and space).

  • An optional field width, either a positive decimal integer or an asterisk (indicating that the quantity should be taken from an argument).

  • An optional precision, a period (.) optionally followed by a decimal integer or an asterisk (indicating that the quantity should be taken from an argument.

  • An optional length modifier, being any number of letters in any order from the string *LENGTH-MODIFIERS*, the default being "hlqLjzZt".

  • The conversion specifier, a single character from the set a, A, c, d, e, E, f, F, g, G, i, o, s, u, x and X.

A % immediately followed by a % causes a single % to be copied to the output.

NOTE: Unlike C99, the STANDARD-CONVERTER accepts F (the upper-case version of f), but does not accept p (pointer) or n (which is discouraged in anyway) as being un-lispy.

The STANDARD-CONVERTER seeks to reproduce the effect of (mainly) C99 printf when applied to arguments of lisp objects. For this reason it snarfs up as many length modifiers as it can see, then ignores them.

For the effect of flag characters, field width, precision and conversion specifier please refer to the closest man page or language specification.

Configuring

Because not all printfs are C99, printfcl provides a variety of mechanisms for configuring the processing of conversion specifications. Configuration is primarily done by specialising generic functions on a converter class with an instance bound to *CONVERTER*. By default this is an instance of STANDARD-CONVERTER. A number of examples follow the description of the configuration protocol.

special variable *CONVERTER*

Bound to an instance of a class used to specialise generic functions of the configuration protocol. By default, an instance of STANDARD-CONVERTER.

special variable *LENGTH-MODIFIERS*

Bound to a sequence of characters used by the STANDARD-CONVERTER to accumulate length modifiers (prior to being ignored). By default "hlqLjzZt".

class STANDARD-CONVERTER

The class upon which the default behaviour of printfcl is specialised.

generic function COLLECT-LENGTH-MODIFIER converter format-string format-string-index

Given the entire format-string passed to the printf-family function and the index at which to begin parsing (format-string-index), return two values: the length modifier (in a form to be used by RETRIEVE-ARGUMENT) and the updated format-string-index (beyond the last character consumed).

generic function COLLECT-CONVERSION-SPECIFIER converter format-string format-string-index

Given the entire format-string passed to the printf-family function and the index at which to begin parsing (format-string-index), return two values: the conversion specifer (in a form to be used by CONVERT, usually a symbol) and the updated format-string-index (beyond the last character consumed).

generic function RETRIEVE-ARGUMENT converter conversion-specifier length-modifer arguments index

Given the conversion-specifier returned by COLLECT-CONVERSION-SPECIFIER, the length-modifier returned by COLLECT-LENGTH-MODIFIER, all the arguments passed to the printf-family function and the index of the appropriate argument in arguments to use, return the argument in a form to be used by CONVERT.

generic function CONVERT converter conversion-specifier argument flags field-width precision

Given the conversion-specifier returned by COLLECT-CONVERSION-SPECIFIER, the argument returned by RETRIEVE-ARGUMENT and the flags, field-width and precision specified in the format string passed to the printf-family function, return a string representing the argument as appropriately converted.

generic function FLOAT-NAN-P converter object

Return a generalised boolean indicating whether object should be treated as a floating point Not-a-Number.

generic function FLOAT-INFINITY-P converter object

Return a generalised boolean indicating whether object should be treated as a floating point Infinity.

generic function NEGATIVE-INFINITY-P converter object

Return a generalised boolean indicating whetner object should be treated as a floating point Negative Infinity.

Handy Utilities

The following items assist in writing converters.

macro WITH-FLAGS flags &body body

Within body, the following symbol macros are bound to functions extracting the status of the corresponding flag.

symbol macros MINUS-FLAG, PLUS-FLAG, SPACE-FLAG, HASH-FLAG, ZERO-FLAG

Within the body of a WITH-FLAGS, true or false depending on whether the corresponding flag has been set.

function SET-FLAG flags character

Return a modified version of flags with the flag corresponding to character (a character) set.

function STRCAT &rest strings

Return strings concatenated as a single string.

function SPACES length

Return a string of length spaces.

function ZEROS length

Return a string of length zeros.

Examples

IEEE Floats - using length modifiers

We have some IEEE double precision ("binary64") and extended precision ("extended 80 bit") floats (as integers) to be interpreted in accordance with the "L" length modifier. They might include infinities and NaNs. The format string includes "%Lf" and "%Le" conversion specifiers.

First, imagine we have the following decoding routines:

(defun decodeb64 (n)
  (let ((sign (ldb (byte 1 63) n))
        (exponent (ldb (byte 11 52) n))
        (significand (ldb (byte 52 0) n)))
    (when (= exponent 1023)
      (return-from decodeb64
        (cond ((not (zerop significand))
               :nan)
              ((zerop sign)
               :+inf)
              (t
               :-inf))))
    (if (zerop exponent)
        (setf exponent 1)
        (setf (ldb (byte 1 52) significand) 1))
    (scale-float (* significand (if (zerop sign) 1d0 -1d0))
                 (- exponent 1075))))

(defun decodee80 (n)
  (let ((sign (ldb (byte 1 79) n))
        (exponent (ldb (byte 15 64) n))
        (significand (ldb (byte 64 0) n)))
    (when (= exponent 32767)
      (return-from decodee80
        (cond ((= significand #.(ash 1 63))
               (if (zerop sign) :+inf :-inf))
              (t :nan))))
    (scale-float (* significand (if (zerop sign) 1d0 -1d0))
                 (- exponent 16446))))

We can see that these routines return :nan, :+inf and :-inf rather than our implementation's native NaN and infinity formats (if any). Therefore, in addition to properly dealing with the "L" length modifier, we will want to appropriately interpret these symbols.

First, we create a converter:

(defclass ieee-float-converter (standard-converter)
  ())

Now we can specialise RETRIEVE-ARGUMENT for our converter and the conversion specifiers that we care about. (We keep as much of the STANDARD-CONVERTER machinery as we can.)

(defmethod retrieve-argument ((converter ieee-float-converter) (cs (eql :|f|))
                              length-modifier arguments index)
  (let* ((n (elt arguments index))
         (extendedp (equal length-modifier '(#\L))))
    (if extendedp (decodee80 n) (decodeb64 n))))

(defmethod retrieve-argument ((converter ieee-float-converter) (cs (eql :|e|))
                              length-modifier arguments index)
  (let* ((n (elt arguments index))
         (extendedp (equal length-modifier '(#\L))))
    (if extendedp (decodee80 n) (decodeb64 n))))

Finally, we deal with the particular format used for infinities etc.

(defmethod float-nan-p ((converter ieee-float-converter) obj)
  (eq obj :nan))

(defmethod float-infinity-p ((converter ieee-float-converter) obj)
  (or (eq obj :+inf) (eq obj :-inf)))

(defmethod negative-infinity-p ((converter ieee-float-converter) obj)
  (eq obj :-inf))

And we test:

(let ((*converter* (make-instance 'ieee-float-converter)))
  (printf "%f %Lf %e %Le %Lf"
          #b0100100011111110001000100010111010000010011001101101001001111111
          #x400c8ca2000000000000
          #b0100100011111110001000100010111010000010011001101101001001111111
          #x400c8ca2000000000000
          #x7fff8000000000000000
         ))
=>
42000000000000000000000000000000000000000000.000000 9000.500000 4.200000e+43 9.000500e+03 inf
93

Yay.

Golike t/T - adding conversion specifiers

Go's version of printf has a t conversion specifier that prints the truth value of its corresponding argument, and a T conversion specifier that prints its type.

We can implement a pale simulacrum of this as follows:

(defclass golike-t-converter (standard-converter)
  ())

(defmethod convert ((converter golike-t-converter) (cs (eql :|T|))
                    argument flags field-width precision)
  (convert-string (prin1-to-string (type-of argument)) flags field-width nil))

(defmethod convert ((converter golike-t-converter) (cs (eql :|t|))
                    argument flags field-width precision)
  (let ((arg (if argument "true" "false")))
    (convert-string arg flags field-width nil)))

When we test this, remember that "t" is a standard length modifier, so we'll have to remove it.

(let ((*converter* (make-instance 'golike-T-converter))
      (*length-modifiers* (remove #\t *length-modifiers*)))
  (printf "%T %T %T %t %t" 1 "foo" *converter* t nil))
=>
BIT (SIMPLE-ARRAY CHARACTER (3)) GOLIKE-T-CONVERTER true false
62

Javaesque h/H - converters as mix-ins

Java's printf has h and H conversion specifiers that print the hashCode() of the argument (or "null"). (H is the upper-case version of h.)

In this example we combine these conversion specifiers with the IEEE converter, treating the Javaesque version as a mix-in class.

(defclass javaesque-h-converter ()
  ())

(defun hash-helper (argument upperp)
  (convert-integer (sxhash argument)
                   (set-flag 0 #\#)
                   0
                   nil
                   :radix 16
                   :upperp upperp))

(defmethod convert ((converter javaesque-h-converter) (cs (eql :|h|))
                    argument flags field-width precision)
  (convert-string (if (null argument)
                      "null"
                      (hash-helper argument nil))
                  flags field-width precision))

(defmethod convert ((converter javaesque-h-converter) (cs (eql :|H|))
                    argument flags field-width precision)
  (convert-string (if (null argument)
                      "NULL"
                      (hash-helper argument t))
                  flags field-width precision))

(defclass my-converter (javaesque-h-converter ieee-float-converter)
  ())

When testing this time we keep only the length modifier we need:

(let ((*converter* (make-instance 'my-converter))
      (*length-modifiers* "L"))
  (printf "%.e %h" #x48FE222E8266D27F *converter*))
=>
4e+43 0x1b969bf9c19f1840
24

(Your results will differ.)

Pythonish strings - overriding a conversion specifier

Python's s conversion specifier calls __str__ on its argument; its r conversion specifier calls __repr__. We can counterfeit this with calls to CL:PRINC[-TO-STRING] and CL:PRIN1[-TO-STRING], respectively.

(defclass pythonish-strings-converter (standard-converter)
  ())

(defmethod convert ((converter pythonish-strings-converter) (cs (eql :|s|))
                    argument flags field-width precision)
  (convert-string (princ-to-string argument)
                  flags field-width precision))

(defmethod convert ((converter pythonish-strings-converter) (cs (eql :|r|))
                    argument flags field-width precision)
  (convert-string (prin1-to-string argument)
                  flags field-width precision))

The test is simpele:

(let ((*converter* (make-instance 'pythonish-strings-converter)))
  (printf "%s %r" "Hello" "World"))
=>
Hello "World"
13

Javatime - a different form of conversion specifier

Java's version of printf contains a large number of conversion specifiers relating to dates and times. The system printfcl/javatime contins an implementation of these conversion specifiers using the local-time system (but assuming unix timestamps are given as arguments).

Because the time- and date-based conversion specifiers are made up of two characters (e.g. Ta or tC) we must customize COLLECT-CONVERSION-SPECIFIER. Here we handle the T/t case and otherwise defer to the STANDARD-CONVERTER with (call-next-method).

Because of the sheer number of suffix specifiers, we'll use a similary strategy by routing all CONVERT calls though a method specialised on our converter, handling the T\t case there (for the sake of the example) and handing the rest on. This method includes an example of the use of WITH-FLAGS and the padding logic.

The key printfcl items are below. (Other functions can be found in the javatime.lisp file.)

(defclass javatime-converter (standard-converter)
  ())

(defparameter *javatime-conversion-character-suffixes*
  "HIklMSLNpzZsQBbhAaCYyjMdeRTrDFc")

(defmethod collect-conversion-specifier ((converter javatime-converter)
                                         format-string
                                         format-string-index)
  (if (not (member (char format-string format-string-index) '(#\t #\T) :test #'char=))
      (call-next-method)
      (let ((tee (char format-string format-string-index)))
        (incf format-string-index)
        (let ((suffix (find (char format-string format-string-index)
                            *javatime-conversion-character-suffixes*)))
          (when (null suffix)
            (error "Unknown javatime suffix: '~C'" (char format-string format-string-index)))
          (values (intern (coerce (list tee suffix) 'string) :keyword)
                  (incf format-string-index))))))

(defmethod convert ((converter javatime-converter) conversion-specifier
                    argument flags field-width precision)
  (let* ((symbol-string (string conversion-specifier))
         (first-char (char symbol-string 0)))
    (if (member first-char (list #\T #\t))
        (let ((timestring (convert-time
                           conversion-specifier
                           (unix-to-timestamp argument))))
          (with-flags (flags)
            (let* ((field-width (or field-width 0))
                   (ts-length (length timestring))
                   (pad-length (- field-width ts-length))
                   (padp (plusp pad-length))
                   (field (cond ((and padp minus-flag)
                                 (strcat timestring (spaces pad-length)))
                                (padp
                                 (strcat (spaces pad-length) timestring))
                                (t
                                 timestring))))
              field)))
        (call-next-method))))

Test time:

(let ((*converter* (make-instance 'javatime-converter))
      (*length-modifiers* (remove #\t *length-modifiers*)))
  (printf "%s %tc" "The time is:" (timestamp-to-unix (now))))
=>
The time is: Sun Apr 04 12:21:58 UTC 2021
41

Comments, questions and suggestions

All comments, questions and suggestions are welcomed.

License

MIT

About

A configurable printf implementation for Common Lisp

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published