fmt: add a function to recover the original format string given a State

Sometimes when implementing a Formatter it's helpful to use the fmt
package without invoking the formatter. This new function, FormatString,
makes that easier in some cases by recreating the original formatting
directive (such as "%3.2f") that caused Formatter.Format to be
called.

The original Formatter interface is probably not what we would
design today, but we're stuck with it. FormatString, although it
takes a State as an argument, compensates by making Formatter a
little more flexible.

The State does not include the verb so (unlike in the issue), we
must provide it explicitly in the call to FormatString. Doing it there
minimizes allocations by returning the complete format string.

Fixes #51668
Updates #51195

Change-Id: Ie31c8256515864b2f460df45fbd231286b8b7a28
Reviewed-on: https://go-review.googlesource.com/c/go/+/400875
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Russ Cox <rsc@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
Run-TryBot: Russ Cox <rsc@golang.org>
This commit is contained in:
Rob Pike 2022-04-19 14:06:41 +10:00
parent 27f1246b85
commit d75e186e2c
3 changed files with 107 additions and 0 deletions

1
api/next/51668.txt Normal file
View file

@ -0,0 +1 @@
pkg fmt, func FormatString(State, int32) string #51668

View file

@ -9,6 +9,7 @@ import (
"io"
"os"
"reflect"
"strconv"
"sync"
"unicode/utf8"
)
@ -71,6 +72,31 @@ type GoStringer interface {
GoString() string
}
// FormatString returns a string representing the fully qualified formatting
// directive captured by the State, followed by the argument verb. (State does not
// itself contain the verb.) The result has a leading percent sign followed by any
// flags, the width, and the precision. Missing flags, width, and precision are
// omitted. This function allows a Formatter to reconstruct the original
// directive triggering the call to Format.
func FormatString(state State, verb rune) string {
var tmp [16]byte // Use a local buffer.
b := append(tmp[:0], '%')
for _, c := range " +-#0" { // All known flags
if state.Flag(int(c)) { // The argument is an int for historical reasons.
b = append(b, byte(c))
}
}
if w, ok := state.Width(); ok {
b = strconv.AppendInt(b, int64(w), 10)
}
if p, ok := state.Precision(); ok {
b = append(b, '.')
b = strconv.AppendInt(b, int64(p), 10)
}
b = utf8.AppendRune(b, verb)
return string(b)
}
// Use simple []byte instead of bytes.Buffer to avoid large dependency.
type buffer []byte

80
src/fmt/state_test.go Normal file
View file

@ -0,0 +1,80 @@
// Copyright 2022 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package fmt_test
import (
"fmt"
"testing"
)
type testState struct {
width int
widthOK bool
prec int
precOK bool
flag map[int]bool
}
var _ fmt.State = testState{}
func (s testState) Write(b []byte) (n int, err error) {
panic("unimplemented")
}
func (s testState) Width() (wid int, ok bool) {
return s.width, s.widthOK
}
func (s testState) Precision() (prec int, ok bool) {
return s.prec, s.precOK
}
func (s testState) Flag(c int) bool {
return s.flag[c]
}
const NO = -1000
func mkState(w, p int, flags string) testState {
s := testState{}
if w != NO {
s.width = w
s.widthOK = true
}
if p != NO {
s.prec = p
s.precOK = true
}
s.flag = make(map[int]bool)
for _, c := range flags {
s.flag[int(c)] = true
}
return s
}
func TestFormatString(t *testing.T) {
var tests = []struct {
width, prec int
flags string
result string
}{
{NO, NO, "", "%x"},
{NO, 3, "", "%.3x"},
{3, NO, "", "%3x"},
{7, 3, "", "%7.3x"},
{NO, NO, " +-#0", "% +-#0x"},
{7, 3, "+", "%+7.3x"},
{7, -3, "-", "%-7.-3x"},
{7, 3, " ", "% 7.3x"},
{7, 3, "#", "%#7.3x"},
{7, 3, "0", "%07.3x"},
}
for _, test := range tests {
got := fmt.FormatString(mkState(test.width, test.prec, test.flags), 'x')
if got != test.result {
t.Errorf("%v: got %s", test, got)
}
}
}