用 format-spec 格式化字符串

刚刚在学习 pcase 时遇到一个 format 使用场景:

(pcase '(1 1)
  (`(,x ,x) (format "Matching (%s %s) where %s eq %s" x x x x)))
     => "Matching (1 1) where 1 eq 1"

同一个 x 要写 4 次,不是很友好,想到 Emacs 自带的 format-spec.el,用 format-spec 写一次就够了:

(pcase '(1 1)
  (`(,x ,x) (format-spec "Matching (%x %x) where %x eq %x" `((?x . ,x)))))
     => "Matching (1 1) where 1 eq 1"

format-spec 的用法是:

(format-spec FORMAT SPECIFICATION)

其中 SPECIFICATION 是个 Alist,且它的 Key 是个字符。

撇开 pcase 再举个例子,假设要表达 a + b = b + a

(format "%s + %s = %s + %s" 1 2 2 1)
     => "1 + 2 = 2 + 1"

(format-spec "%a + %b = %b + %a" '((?a . 1) (?b . 2)))
     => "1 + 2 = 2 + 1"

明显这里用 format-spec 的代码更清楚些。

值得注意的是:format-spec.el 是个非常简单的库,仅相当于 (format "%s") 的封装,所以不能完全取代 format,如:

(format "%.2f" pi)
     => "3.14"

(format-spec "%.2x" (list (cons ?x pi)))
     => "3."

因为上面的 format-spec 实际等价于:

(format "%.2s" pi)
     => "3."

曾经用过一次,但是用来干吗了完全忘记了 :joy:

Emacs 26 的 format 现在支持指定位置了:

原来的

(format "%s + %s = %s + %s" 1 2 2 1)
     => "1 + 2 = 2 + 1"

现在可写成:

(format "%1$s + %2$s = %2$s + %1$s" 1 2)
     => "1 + 2 = 2 + 1"

(let ((name "John"))
  (princ #"Hello, ${name}!\n"))
;-> Hello, John!

其实如果能实现成这样应该更方便一些。

范例来自 CL21

单个字母的 placeholder 有点弱,而且 (format-spec "%c" '((?c . "string"))) 看起来有点迷惑,总会想起 fromat:

%s means print a string argument.  Actually, prints any object, with ‘princ’.
%d means print as signed number in decimal.
%o means print as unsigned number in octal, %x as unsigned number in hex.
%X is like %x, but uses upper case.
%e means print a number in exponential notation.
%f means print a number in decimal-point notation.
%g means print a number in exponential notation if the exponent would be
   less than -4 or greater than or equal to the precision (default: 6);
   otherwise it prints in decimal-point notation.
%c means print a number as a single character.
%S means print any object as an s-expression (using ‘prin1’).

其实 format-spec 还可以再完善一些,让 placeholder 更具可读性,我这里写了一个简陋的 format-spec+:

(format-spec+ ":%-10{key}s %{val}d" '((key . "foo") (val . 100)))
(format-spec+ "%10{val}d :%{key}s" '((key . "bar") (val . 200)))
(format-spec+ ":%-10{key}s %{val}d, :%-10{key}s %{val}d" '((key . "quux") (val . 300)))
;; => :foo        100
;; =>        200 :bar
;; => :quux       300, :quux       300
1 个赞

写成 #"Hello, ${name}!\n" 这样就得引入了新「语法」,或者说「概念」.我也试了下实现类似的效果:

(defmacro my-format (string)
  (let* ((vars ())
         (format-string
          (replace-regexp-in-string
           (rx "${" (group (1+ alnum)) "}")
           (lambda (substring)
             (push (intern (match-string 1 substring)) vars)
             "%s")
           string)))
    `(format ,format-string ,@(nreverse vars))))

(let ((name "John"))
  (my-format "hello, ${name}"))
     => "hello, John"

用 let 绑定变量是个不错的主意。

Ruby 有个 String interpolation 概念:

name = "Ada"
puts "Hello, #{name}!"

用 Emacs Lisp 感受了下:

(defmacro chunyang-string-interpolation (string)
  (let (format-string forms)
    (setq format-string
          (replace-regexp-in-string
           "#{\\(.+\\)}"
           (lambda (substring)
             (push (read (match-string 1 substring)) forms)
             "%s")
           string))
    `(format ,format-string ,@forms)))

(chunyang-string-interpolation "Hello, #{user-full-name}!")
     ↦ (format "Hello, %s!" user-full-name)
     ⇒ "Hello, Xu Chunyang!"

(chunyang-string-interpolation "1 + 2 = #{(+ 1 2)}")
     ↦ (format "1 + 2 = %s" (+ 1 2))
     ⇒ "1 + 2 = 3"
1 个赞

Python 风格:

'{} {}'.format('one', 'two')   # => one two
'{1} {0}'.format('one', 'two') # => two one︎
'{:04d}'.format(42)            # => 0042
'{num:04d}'.format(num=4)      # => 0042

{num:04d}%04{num}d 好看一些,而且处理也更方便。