Commit 0dd1bbb0 authored by Philipp Stephani's avatar Philipp Stephani

Implement field numbers in format strings

A field number explicitly specifies the argument to be formatted.
This is especially important for potential localization work, since
grammars of various languages dictate different word orders.

* src/editfns.c (Fformat): Update documentation.
(styled_format): Implement field numbers.

* doc/lispref/strings.texi (Formatting Strings): Document field numbers.

* lisp/emacs-lisp/bytecomp.el (byte-compile-format-warn): Adapt.

* test/src/editfns-tests.el (format-with-field): New unit test.
parent 404273ae
......@@ -864,7 +864,8 @@ below, as the first argument, and the string as the second, like this:
(format "%s" @var{arbitrary-string})
@end example
If @var{string} contains more than one format specification, the
If @var{string} contains more than one format specification and none
of the format specifications contain an explicit field number, the
format specifications correspond to successive values from
@var{objects}. Thus, the first format specification in @var{string}
uses the first such value, the second format specification uses the
......@@ -961,6 +962,25 @@ operation} error.
@end group
@end example
@cindex field numbers in format spec
A specification can have a @dfn{field number}, which is a decimal
number after the initial @samp{%}, followed by a literal dollar sign
@samp{$}. If you provide a field number, then the argument to be
printed corresponds to the given field number instead of the next
argument. Field numbers start at 1.
You can mix specifications with and without field numbers. A
specification without a field number that follows a specification with
a field number will convert the argument after the one specified by
the field number:
@example
(format "First argument %2$s, then %s, then %1$s" 1 2 3)
@result{} "First argument 2, then 3, then 1"
@end example
You can't use field numbers in a @samp{%%} specification.
@cindex field width
@cindex padding
A specification can have a @dfn{width}, which is a decimal number
......@@ -996,9 +1016,14 @@ is not truncated.
@end group
@end example
If you want to use both a field number and a width, place the field
number before the width. For example, in @samp{%2$7s}, @samp{2} is
the field number and @samp{7} is the width.
@cindex flags in format specifications
Immediately after the @samp{%} and before the optional width
specifier, you can also put certain @dfn{flag characters}.
After the @samp{%} and before the optional width specifier, you can
also put certain @dfn{flag characters}. The flag characters need to
come directly after a potential field number.
The flag @samp{+} inserts a plus sign before a positive number, so
that it always has a sign. A space character as flag inserts a space
......
......@@ -368,6 +368,9 @@ libraries: 'find-library-other-window' and 'find-library-other-frame'.
** The new variable 'display-raw-bytes-as-hex' allows to change the
display of raw bytes from octal to hex.
** You can now provide explicit field numbers in format specifiers.
For example, '(format "%2$s %1$s" 1 2)' produces "2 1".
* Editing Changes in Emacs 26.1
......
......@@ -1375,10 +1375,15 @@ extra args."
(let ((nfields (with-temp-buffer
(insert (nth 1 form))
(goto-char (point-min))
(let ((n 0))
(let ((i 0) (n 0))
(while (re-search-forward "%." nil t)
(unless (eq ?% (char-after (1+ (match-beginning 0))))
(setq n (1+ n))))
(backward-char)
(unless (eq ?% (char-after))
(setq i (if (looking-at "\\([0-9]+\\)\\$")
(string-to-number (match-string 1) 10)
(1+ i))
n (max n i)))
(forward-char))
n)))
(nargs (- (length form) 2)))
(unless (= nargs nfields)
......
......@@ -48,6 +48,7 @@ along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>. */
#include <float.h>
#include <limits.h>
#include <c-ctype.h>
#include <intprops.h>
#include <stdlib.h>
#include <strftime.h>
......@@ -3856,7 +3857,7 @@ The first argument is a format control string.
The other arguments are substituted into it to make the result, a string.
The format control string may contain %-sequences meaning to substitute
the next available argument:
the next available argument, or the argument explicitly specified:
%s means print a string argument. Actually, prints any object, with `princ'.
%d means print as signed number in decimal.
......@@ -3873,13 +3874,17 @@ the next available argument:
The argument used for %d, %o, %x, %e, %f, %g or %c must be a number.
Use %% to put a single % into the output.
A %-sequence may contain optional flag, width, and precision
specifiers, as follows:
A %-sequence may contain optional field number, flag, width, and
precision specifiers, as follows:
%<flags><width><precision>character
%<field><flags><width><precision>character
where flags is [+ #-0]+, width is [0-9]+, and precision is a literal
period "." followed by [0-9]+
where field is [0-9]+ followed by a literal dollar "$", flags is
[+ #-0]+, width is [0-9]+, and precision is a literal period "."
followed by [0-9]+.
If field is given, it must be a one-based argument number; the given
argument is substituted instead of the next one.
The + flag character inserts a + before any positive number, while a
space inserts a space before any positive number; these flags only
......@@ -4032,14 +4037,19 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message)
{
/* General format specifications look like
'%' [flags] [field-width] [precision] format
'%' [field-number] [flags] [field-width] [precision] format
where
field-number ::= [0-9]+ '$'
flags ::= [-+0# ]+
field-width ::= [0-9]+
precision ::= '.' [0-9]*
If a field-number is specified, it specifies the argument
number to substitute. Otherwise, the next argument is
taken.
If a field-width is specified, it specifies to which width
the output should be padded with blanks, if the output
string is shorter than field-width.
......@@ -4048,6 +4058,29 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message)
digits to print after the '.' for floats, or the max.
number of chars to print from a string. */
char *field_end;
uintmax_t raw_field = strtoumax (format, &field_end, 10);
bool has_field = false;
if (c_isdigit (*format) && *field_end == '$')
{
if (raw_field < 1 || raw_field >= PTRDIFF_MAX)
{
/* doprnt doesn't support %.*s, so we need to copy
the field number string. */
ptrdiff_t length = field_end - format;
eassert (length > 0);
eassert (length < PTRDIFF_MAX);
char *field = SAFE_ALLOCA (length + 1);
memcpy (field, format, length);
field[length] = '\0';
error ("Invalid field number `%s'", field);
}
has_field = true;
/* n is incremented below. */
n = raw_field - 1;
format = field_end + 1;
}
bool minus_flag = false;
bool plus_flag = false;
bool space_flag = false;
......@@ -4090,7 +4123,13 @@ styled_format (ptrdiff_t nargs, Lisp_Object *args, bool message)
memset (&discarded[format0 - format_start], 1,
format - format0 - (conversion == '%'));
if (conversion == '%')
goto copy_char;
{
if (has_field)
/* FIXME: `error' doesn't appear to support `%%'. */
error ("Field number specified together with `%c' conversion",
'%');
goto copy_char;
}
++n;
if (! (n < nargs))
......
......@@ -177,4 +177,22 @@
(format-time-string "%Y-%m-%d %H:%M:%S.%3N %z" nil
(concat (make-string 2048 ?X) "0")))))
(ert-deftest format-with-field ()
(should (equal (format "First argument %2$s, then %s, then %1$s" 1 2 3)
"First argument 2, then 3, then 1"))
(should (equal (format "a %2$s %d %1$d %2$S %d %d b" 11 "22" 33 44)
"a 22 33 11 \"22\" 33 44 b"))
(should (equal (format "a %08$s %s b" 1 2 3 4 5 6 7 8 9) "a 8 9 b"))
(should (equal (should-error (format "a %999999$s b" 11))
'(error "Not enough arguments for format string")))
(should (equal (should-error (format "a %$s b" 11))
;; FIXME: there shouldn't be two % in the error
;; string!
'(error "Invalid format operation %%$")))
(should (equal (should-error (format "a %0$s b" 11))
'(error "Invalid field number `0'")))
(should (equal
(should-error (format "a %1$% %s b" 11))
'(error "Field number specified together with `%' conversion"))))
;;; editfns-tests.el ends here
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment